本章术语:
- 拷贝构造函数(copy constructor):定义了当用同类型的另一个对象初始化本对象时做什么
- 拷贝赋值运算符(copy-assignment operator):定义了将一个对象赋予同类型的另一个对象时做什么
- 移动构造函数(move constructor):定义了当用同类型的另一个对象初始化本对象时做什么
- 移动赋值运算符(move-assignment operator):定义了将一个对象赋予同类型的另一个对象时做什么
- 析构函数(destructor):定义了当此类型对象销毁时做什么
- 拷贝控制操作(copy control):上述 5 种操作的统称
- 合成拷贝构造函数(synthesized copy constructor):编译器自动生成的拷贝构造函数
- 合成拷贝赋值运算符(synthesized copy-assignment operator):编译器自动生成的拷贝赋值运算符
- 合成析构函数(synthesized destructor):编译器自动生成的析构函数
- 删除的函数(deleted function):在声明后面加上
=delete
表示该函数不允许调用
如果程序员不显式定义拷贝控制操作,编译器会自动定义这些操作。
拷贝构造函数的形式:
- 是一个构造函数
- 第一个参数是自身类类型的引用
- 任何额外的参数都有默认值
class Foo {
public:
Foo(); // 默认构造函数
Foo(const Foo &); // 拷贝构造函数(第一个参数总是 const 的引用,通常不应该是 explicit 的)
};
合成拷贝构造函数会将其参数的每个非 static 成员逐个拷贝到正在创建的对象中。
在某些情况下,合成拷贝构造函数用来阻止拷贝该类类型的对象。
直接初始化和拷贝初始化的区别:
- 直接初始化:要求编译器使用普通的函数匹配来选择参数最匹配的构造函数
- 拷贝初始化:要求编译器使用拷贝构造函数,将右侧运算对象拷贝到正在创建的对象中
拷贝初始化发生的情况:
- 使用
=
运算符定义变量 - 将一个对象作为实参传递给一个非引用类型的形参
- 从一个返回类型为非引用类型的函数返回一个对象
- 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
- 初始化标准库容器或调用 insert 或 push 成员(与之相对,用 emplace 成员创建的元素都进行直接初始化)
explicit 的构造函数必须显式调用。
vector<int> v1(10); // 直接初始化:显式调用接受 vector 大小的 explicit 构造函数
vector<int> v2 = 10; // 错误:explicit 的构造函数不能隐式调用
void f(vector<int>); // 函数 f 的声明
f(vector<int>(10)); // 正确:显式调用 explicit 构造函数创建对象并拷贝给形参
f(10); // 错误:不能隐式调用 explicit 构造函数创建对象
编译器可以将拷贝初始化优化为直接初始化,但是拷贝构造函数必须是存在且可以访问的(即:不能没有拷贝构造函数)
// 代码中所展示的
string null_book = "9-999-99999-9"; // 理应执行拷贝构造函数
// 编译器实际调用的
string null_book("9-999-99999-9"); // 编译器略过了拷贝构造函数,执行直接初始化
运算符左侧的运算对象绑定到隐式的 this 参数,运算符右侧的运算对象作为显式参数传递。
拷贝赋值运算符接受一个与其所在类相同类型的参数:
class Foo {
public:
Foo &operator=(const Foo &);
};
赋值运算符通常应该返回一个指向其左侧运算对象的引用。
合成拷贝赋值运算符会将右侧运算对象的每个非 static 成员赋予左侧运算对象的对应成员。
在某些情况下,合成拷贝赋值运算符用来禁止该类型对象的赋值。
- 构造函数:初始化对象的非 static 数据成员,以及做一些其他工作
- 析构函数:释放对象使用的资源,销毁对象的非 static 数据成员
析构函数的形式:是类的一个成员函数,名字由波浪号接类名构成,没有返回值,也不接受参数:
class Foo {
public:
~Foo(); // 析构函数
};
- 构造函数首先执行初始化部分,然后执行函数体(按照成员在类中出现的顺序进行初始化)
- 析构函数首先执行函数体,然后销毁成员(按照成员初始化顺序的逆序进行销毁)
隐式销毁一个内置指针类型的成员不会 delete 它所指向的对象。
析构函数被调用的情况:
- 变量在离开其作用域时被销毁
- 当一个对象被销毁时,其成员被销毁
- 容器被销毁时,其元素被销毁
- 对于动态分配的对象,当对指向它的指针使用 delete 运算符时被销毁
- 对于临时对象,当创建它的完整表达式结束时被销毁
当指向一个对象的引用或指针离开作用域时,析构函数不会执行。
合成析构函数可能会被用来阻止该类型的对象被销毁。如果不是这种情况,合成析构函数的函数体为空。
如果一个类需要自定义析构函数,那么几乎可以肯定它也需要自定义拷贝赋值运算符和拷贝构造函数。
如果一个类需要一个拷贝构造函数,那么几乎可以肯定它也需要一个拷贝赋值运算符。
反之亦然
如果一个类需要一个拷贝赋值运算符,那么几乎可以肯定它也需要一个拷贝构造函数。
无论是需要拷贝构造函数,还是需要拷贝赋值运算符,都不必然意味着也需要析构函数。
使用 =default
显式要求编译器生成合成的拷贝控制成员:
class Sales_data {
public:
Sales_data() = default;
Sales_data(const Sales_data &) = default;
Sales_data &operator=(const Sales_data &);
~Sales_data() = default;
};
Sales_data &Sales_data::operator=(const Sales_data &) = default;
在类内用 =default
修饰成员的声明时,合成的函数被隐式地声明为内联函数。
在类外用 =default
时,合成的函数则不是内联函数。
大多数类应该定义默认构造函数、拷贝构造函数和拷贝赋值运算符,无论是隐式地还是显式地。
定义删除的函数以阻止拷贝(在函数声明后添加 =delete
):
struct NoCopy {
NoCopy() = default;
NoCopy(const NoCopy &) = delete; // 阻止拷贝
NoCopy &operator=(const NoCopy &) = delete; // 阻止赋值
~NoCopy() = default;
};
不能将析构函数声明为删除的函数。
合成的拷贝控制成员有可能是删除的,具体的情形有:
- 合成的默认构造函数是删除的函数:
- 类的某个成员的析构函数是删除的或不可访问的(即 private 的)
- 类有一个引用成员,它没有类内初始化器
- 类有一个 const 成员,它没有类内初始化器并且其类型未显式定义默认构造函数
- 合成的析构函数是删除的函数:
- 类的某个成员的析构函数是删除的或不可访问的
- 合成的拷贝构造函数是删除的函数:
- 类的某个成员的拷贝构造函数是删除的或不可访问的
- 类的某个成员的析构函数是删除的或不可访问的
- 合成的拷贝赋值运算符是删除的函数:
- 类的某个成员的拷贝赋值运算符是删除的或不可访问的
- 类有一个 const 成员
- 类有一个引用成员
总之:当不可能拷贝、赋值或销毁类的成员时,类的合成拷贝控制成员就被定义为删除的。
将拷贝控制成员声明为 private 的,并且不定义他们,这种做法的效果等价于删除的函数。
但是:
希望阻止拷贝的类应该使用 =delete
来定义拷贝构造函数和拷贝赋值运算符,而不是将它们声明为 private 的。