一个类通过定义五种特殊的成员函数来控制对象的拷贝、移动、赋值和销毁这些操作在做什么。
- 拷贝构造函数
- 拷贝赋值运算符
- 移动构造函数
- 移动赋值运算符
- 析构函数
一般情况下,这些操作应该看作为一个整体,只需要其中一个操作的时候很少见。
拷贝构造函数
拷贝构造函数的第一个参数是自身类型的引用,一般会是一个const引用,且任何额外的参数都有默认值;拷贝构造几种情况下都会被隐式使用,因此拷贝构造不应该是explicit,需要进行类型转换。
与合成的默认的构造函数不同,即使定义了拷贝构造函数,编译器也会为我们合成一个拷贝构造函数。一般情况下,合成的拷贝构造函数会将参数的成员逐个拷贝到正在创建的对象中;有例外就是有的会用来阻止我们拷贝该类。
直接初始化和拷贝初始化
string dots(10, 'c'); //直接初始化
string s(dots); //直接初始化
string s2 = dots; //拷贝初始化
string nines = string(100, '9'); //拷贝初始化
拷贝初始化一般用拷贝构造函数和移动构造函数来完成。除用=定义时会发生,下列情况下也会发生拷贝初始化:
- 非引用参数传递
- 非引用返回
- 列表初始化一个数组中的元素或者一个类成员
此外,使用标准库容器时调用insert或push也会进行拷贝初始化,用emplace成员创建的元素进行直接初始化。
拷贝赋值运算符
在对象已经创建之后(默认构造),对其赋初始值时会用到拷贝赋值运算符;
如果该运算符是一个成员函数,那运算对象左侧就绑定到隐式this参数,返回值是左侧对象的引用;若是一个二元运算符,其右侧运算对象作为显示参数传递。
如果未定义自己的拷贝赋值运算符,编译器也会为其合成拷贝赋值运算符;对于某些类,合成的拷贝赋值运算符用来禁止该类型对象的赋值,若并非出于此目的,它会将右侧运算对象的每个非static成员赋予左侧运算对象的对应的成员,对于数组则是逐个赋值数组元素。
析构函数
析构函数没有返回值也不接受参数;在析构函数中,首先执行函数体,然后销毁对象,成员按照初始化顺序的逆序销毁。
析构函数的函数体自身并不直接销毁成员,而是在析构函数体之后隐含的析构阶段中销毁的。
隐式销毁一个内置指针类型的成员不会delete它所指向的对象。
智能指针是类类型,所以具有析构函数,智能指针成员在析构阶段会被自动销毁
什么时候会调用析构函数:
- 变量离开其作用域时被销毁;
- 当一个对象被销毁时,其成员被销毁;
- 容器被销毁时,其元素被销毁;
- 动态分配的对象,当对指向它的指针运用delete运算符时被销毁;
- 临时对象,当创建它的完整表达式结束时被销毁;
阻止拷贝
有的类会定义拷贝控制函数来阻止拷贝,例如iostream,我们可以将拷贝构造函数和拷贝构造运算符定义为删除的函数来阻止拷贝;=delete通知编译器,我们不希望定义这些成员。
Foo(const Foo&) = delete;
Foo &operator=(const Foo&) = delete;
与=default不同的是,我们可以指定任何函数为delete的,而只能对编译器合成的默认构造函数或者拷贝控制函数使用default。
如果一个类有数据成员不能默认构造、拷贝、复制或销毁,则对应的成员函数将被定义为删除的。本质上如果不可能拷贝、赋值或销毁类的成员时, 类的合成拷贝控制成员就被定义为删除的。
对象移动
在某些情况下,对象拷贝后就立即被销毁了,这时候移动而非拷贝对象会大幅度提升性能。IO类、unique_ptr这些类都包含不能被共享的资源,所以这些对象不能被拷贝但是可以移动。
右值引用
可以通过 && 来获取右值引用,字面意思就是必须要绑定到右值的引用,其有个很重要的性质就是只能绑定到一个将要销毁的对象。使用右值引用的代码就可以自由接管所引用的对象的资源/一般而言,一个左值表达式表示的是一个对象的身份,而一个右值表达式表示的是对象的值。
变量是左值,所以不能将一个右值引用绑定到一个变量上,即使这个变量是右值引用类型也不行;
int &&rr1 = 42; //正确:字面值常量是右值
int &&rr2 = rr1; //错误:表达式rr1是左值
头文件utility中定义了move函数,此函数用来告诉编译器:有一个左值,但想像右值一样去处理。调用了move就相当于做出承诺,除了对rr1赋值或销毁它外,将不再使用它;
int &&rr3 = std::move(rr1); //正确
移动构造函数和移动赋值运算符
类似于拷贝构造函数,移动构造函数的第一个参数是该类型的一个引用,但这个引用参数是一个右值引用,此外还要确保移动后原对象被销毁。
移动构造函数不分配新内存,它接管原对象的内存。释放左侧运算对象所使用内存,接管给定对象的内存。
移动赋值运算符也和拷贝构造运算符类似,需要右侧运算对象的一个右值,我们不能在使用右侧运算对象的资源之前就释放左侧运算对象的资源,因为左右两边可能指向相同的资源。
只有当一个类没有定义任何自己版本的拷贝控制成员,且它的所有数据成员都能移动构造或移动赋值时,编译器才会为它合成移动构造函数或移动赋值运算符。
有关定义为删除的问题:
- 与拷贝构造函数不同,有类成员定义了自己的拷贝构造函数且未定义移动构造函数,或是有类成员未定义自己的拷贝构造函数且编译器不能为其合成移动构造函数,则移动构造函数被定义为删除的。
- 如果有类成员的移动构造函数或移动赋值运算符被定义为删除的或是不可访问的,则类的移动构造函数或移动赋值运算符被定义为删除的 。
- 如果类的析构函数被定义为删除的或不可访问的,则类的移动构造函数被定义为删除的。
- 如果有类成员是const的或是引用,则类的移动赋值运算符被定义为删除的。
定义了一个移动构造函数或移动赋值运算符的类必须也定义自己的 拷贝操作。否则,这些成员默认被定义为删除的。