类(class)的使用分为两种——基于对象(object Based)和面向对象(object oriented)
基于对象是指,程序设计中单一的类,和其他类没有任何关系
单一的类又分为:不带指针的类(class without pointer members)和带指针的类(class with pointer members)
面向对象则是类(class)中涉及了类之间的关系:复合(composition)、委托(delegation)、继承(inheritance)
#ifndef xxx #define xxx ... #endif
在编写头文件时应该有这样的一种习惯
目的是避免多次重复包含同一个头文件,否则会引起变量及类的重复定义
class A { public: static A & getInstance(); setup() {...} private: A(); A(const A & rhs); ... } A & A::getInstance() { static A a; return a; } ... //外部接口 A::getInstance().setup();
原理:将构造函数设置为私有属性,同时设置一个静态函数接口返回一个该类对象
作用:保证每一个类仅有一个实例,并为它提供一个全局访问点
单例模式(Singleton)的主要特点不是根据用户程序调用生成一个新的实例,而是控制某个类型的实例唯一性。它拥有一个私有构造函数,这确保用户无法通过new直接实例它。除此之外,该模式中包含一个静态私有成员变量instance与静态公有方法Instance()。Instance()方法负责检验并实例化自己,然后存储在静态成员变量中,以确保只有一个实例被创建。
这种模式主要有以下特征或条件:
- 有一个私有的无参构造函数,这可以防止其他类实例化它,而且单例类也不应该被继承,如果单例类允许继承那么每个子类都可以创建实例,这就违背了Singleton模式“唯一实例”的初衷。
- 单例类被定义为sealed,就像前面提到的该类不应该被继承,所以为了保险起见可以把该类定义成不允许派生,但没有要求一定要这样定义。
- 一个静态的变量用来保存单实例的引用。
- 一个公有的静态方法用来获取单实例的引用,如果实例为null即创建一个。
参考:
设计模式详解:Singleton(单例类)_singleton类_p_帽子戏法的博客-CSDN博客
单例模式(Singleton)的6种实现 - JK_Rush - 博客园 (cnblogs.com)
如果一个成员函数不改变类的数据成员时,就把它声明为常函数,这是一个好的习惯
当实例化一个常对象时,常对象要求不能改变数据成员,如果成员函数不加const,将无法调用此成员函数,编译器不会通过,即使此函数确实没有改变数据成员;同时,即使成员函数被声明为了常函数,实例化一个普通对象时依然可以调用。
简单来说,不声明为常成员函数可能不会有问题,但声明为常成员函数能确保一定不出问题
问:如何解释一个类的成员函数在接收同类对象的参数(比如拷贝构造函数)可以直接调用该对象的任何成员,明明既不是友元也不是嵌套?
答:相同class的各个objects互为friends(友元)
两个案例
class A { int value; ... }; ... A& fun1(A* x, const A& y) { x.value += y.value; //第一参数会改变,第二参数不会改变 return *x }
class B { int value; ... }; ... B fun2(const B& x, const B& y) { //第一参数和第二参数都不会改变 return B(x.value + y.value); }
e.x.
inline complex& _doapl(comlex* ths, const complex& r) { ... return *ths; } inline complex& complex::operator += (const comlex& r) { return _doapl(this, r); } ... comlex c1(2,1), c2(3), c3; //c2 += c1; //c3 += c2 += c1;
当重载一个二元运算符为成员函数时,我们知道重载函数除了右操作数是我们传递的,函数还会默认用一个this指针,来接收左操作数
那么可能会有疑问,我们想要改变的是左操作数,而且由于传递的是指针,函数内也确实可以改变,那返回值又有什么用呢,声明为空不就行了。
当我们使用重载运算符时只是像被注释的第一行代码一样,那么返回值确实不重要,但是当我们使用的形式像被注释的第二行代码时,那么返回值就很重要,因为c2.+=(c1) 这个函数的返回值就是 c3 += () 函数的参数
只能重载为非成员函数
左操作数固定为系统定义的 ostream 类型,且为非常量引用
最好加返回值且为引用,原因前面已经说明,且我们对于连续调用<<的频率要大得多
连续调用时的调用顺序
complex c1, c2; cout << c1 << c2; //先执行 <<(cout, c1) 函数 //返回的 ostream类型的cout的引用 又作为<<(ostream &, c2)的第一参数
在语法上我们当然也可以重载为成员函数,只要左操作数为自定义类型即可,但这样并不符合我们通常的书写习惯
三大件:拷贝构造、拷贝赋值、析构函数
解释:当一个类需要我们去主动设计析构函数时,那它很大概率也需要一个拷贝构造函数和赋值运算符重载成员函数
应用:当一个类具有指针成员时(class with point member)或者说当我们设计了一个有动态内存管理的类时
原因:
析构函数角度:默认析构函数会仅删除指向对象的指针,而删除一个指针不会释放指针指向对象占用的内存,最终会导致内存泄露。
拷贝构造角度:默认的构造函数是浅拷贝,复制的只是指针也就是地址值,这样导致两个对象共享一个内存空间,这是十分危险的,当其中一个对象被删除后,析构函数将释放那片共享的内存空间,接下来对这片已经释放了内存的任何引用都将会导致不可遇见的后果。
赋值运算角度:
赋值相比于拷贝构造要考虑更多
首先是自我赋值判断,如果不判断,当左右操作数指向的是同一个地址时,会造成将左操作数对象的元素删除并释放其占用的内存,同时由于左右操作数指向同一对象,导致右操作数同时被删除,但接下来还要将右操作对象复制,这会造成不可预知的结果。这也被称为证同测试。
其次是进行三步必要操作:
代码示例:
#ifndef __MYSTRING__ #define __MYSTRING__ class String { public: String(const char* cstr=0);//构造函数 String(const String& str);//拷贝构造函数 String& operator=(const String& str);//重载=运算符 ~String();//析构函数 char* get_c_str() const { return m_data; }//成员函数,返回指向字符数组首地址的指针 private: char* m_data;//字符数组指针 }; #include <cstring> //构造函数 inline String::String(const char* cstr) { //开辟内存、计算长度、内容拷贝 if (cstr) { m_data = new char[strlen(cstr)+1]; strcpy(m_data, cstr); } else { m_data = new char[1]; *m_data = '\0'; } } //析构函数 inline String::~String() { delete[] m_data;//释放指针指向空间 } //重载= inline String& String::operator=(const String& str) { //检测自我赋值(self assignment) if (this == &str) return *this; delete[] m_data; m_data = new char[ strlen(str.m_data) + 1 ]; strcpy(m_data, str.m_data); return *this; } //构造函数 inline String::String(const String& str) { m_data = new char[ strlen(str.m_data) + 1 ]; strcpy(m_data, str.m_data); } #include <iostream> using namespace std; //重载<< ostream& operator<<(ostream& os, const String& str) { os << str.get_c_str(); return os; } #endif
当我们使用new创建了一个指向类的对象的指针时
这里的new干了三件事:
红色部分是 cookie ,记录内存分配的总大小,就是图中的41,其最低位用于表示是否已分配(1表示已分配,0表示已回收),之所以最低位可以变,是因为分配的内存总空间一定是16的倍数,其16进制表示时最低位一定为0,也就是说这个位置是空出来的,刚好用来表示内存状态。每一个 new 的对象都会有上下两个 cookie,来预先申请一块内存池,然后供对象实例化。
绿色部分是调用malloc()时向系统申请的内存,该函数返回时,也会返回这块区域开头的指针。
绿色部分上下两块 gap 预先被填充为了0xfdfdfdfd,用来分隔客户可以使用的内存区和不可使用的内存区,同时,当这块内存被归还时,编辑器也可以通过下gap的值区判断当前内存块是否被越界使用了
从gap向上连续的7个内存空间共同组成了debug header,从上向下标号为1-7
- 1、2两块空间保存了两根指针,目的是使多个内存块连接成链表。
- 3空间保存了申请本内存块的文件名
- 4空间保存了申请本内存块的代码行数
- 5空间记录了本内存块中实际可以被用户使用的内存空间的大小
- 6空间记录了当前内存块的流水号,即是链表中的第几个,从1开始
- 7空间记录了当前内存块被分配的形式
填补区pad
参考:
https://zhuanlan.zhihu.com/p/492161361
https://www.cnblogs.com/zyb993963526/p/15682014.html#_label2
https://blog.csdn.net/qq_61500888/article/details/122170203
动态分配数组时要注意的:
类的每一个非静态成员函数(包括构造函数、拷贝构造等)都隐含着一个指针形参名为this,当对象调用成员函数时就会隐含传递该对象的地址给它,这也是为什么一个类的成员函数虽然只有一份但也会根据接收的消息不同产生不同的行为,而静态成员函数不隐含this指针,所以即使调用它的对象不同维护的依然是同一段代码
三个案例: