class Foo { public: Foo& operator=(const Foo&); };
与拷贝构造函数一样,如果没有给类定义拷贝赋值运算符,编译器将为它合成一个。
4,析构函数
析构函数是由波浪线接类名构成,它没有返回值,也不接受参数。因为没有参数,所以它不存在重载函数,也就是说一个类只有一个析构函数。
析构函数做的事情与构造函数相反,那么我们先回忆一个构造函数都做了哪些事:
1,按成员定义的顺序创建每个成员。
2,根据成员初始化列表初始化每个成员。
3,执行构造函数函数体。
而析构函数中不存在类似构造函数中初始化列表的东西来控制成员如何销毁,析构部分是隐式的。成员如何销毁依赖于成员自身的类型,如果是类类型则调用本身的析构函数,如果是内置类型则会自动销毁。而如果是一个指针,则需要手动的释放指针指向的空间。与普通指针不同的是,智能指针是一个类,它有自己的析构函数。
那么什么时候会调用析构函数呢?在对象销毁的时候:
- 变量在离开其作用域时被销毁;
- 当一个对象被销毁时,其成员被销毁。
- 容器被销毁时,成员被销毁。
- 对于动态分配的对象,当对指向它的指针应用delete运算符时被销毁。
- 对于临时对象,当创建它的赛事表达式结束时被销毁。
值得注意的析构函数是自动运行的。析构函数的函数体并不直接销毁成员,成员是在析构函数体之后隐含的析构阶段中被销毁的。在整个对象销毁过程中,析构函数体是作为成员销毁步骤之外的另一部分而进行的。
5,定义拷贝控制操作的原则
在第1点里有提过,在定义类的时候处理拷贝控制最困难的在于什么时候需要自己定义,什么时候让编译器自己合成。
那么我们可以有下面2点原则:
如果一个类需要定义析构函数,那么几乎可以肯定它也需要一个拷贝构造函数和一个拷贝赋值函数,反过来不一定成立。
如果一个类需要一个拷贝构造函数,几乎可以肯定它也需要一个拷贝赋值函数,反之亦然。
为什么析构函数与拷贝构造函数与赋值函数关系这么紧密呢,或者说为什么我们在讨论拷贝控制(5种)的时候要把析构函数一起放进来呢?
首先,我们思考什么时候我们一定要自己来定义析构函数,比如:类里面有动态分配内存。
class HasPtr { public: HasPtr(const string&s = string()) :ps(new string(s), i(0)){} ~HasPtr(){ delete ps; } private: int i; string* ps; };
我们知道如果是编译器自动合成的析构函数,则不会去delete指针变量的,所以ps指向的内存将无法释放,所以一个主动定义的析构函数是需要的。那么如果没有给这个类定义拷贝构造函数和拷贝赋值函数,将会怎么样?
编译器自动合成的版本,将简单的拷贝指针成员,这意味着多个HasPtr对象可能指向相同的内存。
HasPtr p("some values"); f(p); // 当f结束时,p.ps指向的内存被释放 HasPtr q(p);// 现在p和q都指向无效内存
6,使用=default和=delete
我们可以使用=default来显式地要求编译器生成合成的版本。合成的函数将隐式地声明为内联的,如果我们不希望合成的成员是内联的,应该只对成员的类外定义使用=default。