本文主要参考《C++ primer第五版(中文版)》第13章。若有理解错误,恳请指正。
C++ 11有五种特殊成员函数来控制对象的拷贝、移动和销毁:
C++ 11 拷贝控制
|
+--拷贝 -+- 拷贝构造函数
| +- 拷贝赋值构造函数
|
+--移动 -+- 移动构造函数
| +- 移动赋值构造函数
|
+--销毁 --- 析构函数
如下表,去掉析构函数的四类拷贝控制函数可以按两个维度进行分类。本文分别从两个维度进行区分讲解。
初始化 | 赋值 | |
---|---|---|
拷贝(左值) | 拷贝构造 | 拷贝赋值构造 |
移动(右值) | 移动构造 | 移动赋值构造 |
1. 初始化和赋值
注意,以等号"="连接的不一定是赋值,也可能是初始化。这取决于是在变量声明时还是声明后使用“=”。
以“=”连接的赋值调用拷贝/移动赋值构造函数;以“=”连接的初始化调用拷贝/移动构造函数。例:
string s1, s2;
s2 = s1; // 赋值
string s3 = s1; // 初始化
1.1. 初始化:直接初始化和拷贝初始化
相对赋值,初始化概念更加复杂。因为,对象赋值操作只对应于拷贝赋值或者移动赋值两种特殊成员函数;而对象初始化除了涉及拷贝构造、移动构造两种特殊成员函数,也涉及到各种被重载的“普通”构造函数。下面说明初始化时,什么样的构造函数会被匹配和调用。
首先初始化被分为两类:
直接初始化要求编译器使用普通的函数匹配选择构造函数,这些“普通的构造函数”并不在刚才提到的五种特殊拷贝控制成员函数之列。
拷贝初始化则要求编译器将右侧运算对象拷贝到正在创建的对象中,必要时还会进行类型转换。例:
string dots(10, '.'); // ①直接初始化
string s(dots); // ②直接初始化
string null_book("9-999-99999-9"); // ③直接初始化
string s2 = dots; // ④拷贝初始化
string null_book2 = "9-999-99999-9"; // ⑤拷贝初始化
string nines = string(100, '9') // ⑥拷贝初始化
注意:
-
②和④虽然分别为直接初始化和拷贝初始化,但是都会调用拷贝构造函数。
-
直接初始化会直接去尝试匹配所有构造函数,除非直接初始化匹配到我们的拷贝构造函数(②),与我们在本文所关注的五类特殊拷贝控制成员函数无关。
-
拷贝初始化不一定调用拷贝构造函数,也可能调用移动构造函数。根据要拷贝对象的左右值,左值调用拷贝(④),右值调用移动(⑤⑥)。
-
⑤中可能会被编译器优化为
string null_book2("9-999-99999-9");
来略过拷贝/移动构造函数,前提是拷贝/移动构造函数是可以访问的(不是private的)。
2. 拷贝和移动
前边的“注意”中也提到过,根据被拷贝/移动对象的左右值属性,左值将被拷贝,右值将被移动。
左值拷贝对应于拷贝/拷贝赋值构造函数;
右值移动对应于移动/移动赋值构造函数。例:
StrVec v1, v2;
v1 = v2; // v2为左值;拷贝赋值
StrVec getVec(istream &); // 一个返回右值的函数声明
v2 = getVec(cin); // 函数返回值为右值;移动赋值
2.1. 拷贝:深/浅拷贝
对于存在类外资源的类(如所分配的大块内存buf等),深拷贝和浅拷贝的概念对应书中(chap. 13.2)定义的“行为像值的类”和“行为像指针的类”。通常我们需要自定义拷贝构造函数、拷贝赋值构造函数和析构函数让这些类正常工作。
行为像值的类(深拷贝) 需要拷贝对象真正的资源,而非成员指针,析构函数也需要释放资源。
行为像指针的类(浅拷贝) 拷贝时拷贝指针成员本身而非指针指向的资源。还要特别注意的是,只有在最后一个值被销毁时才可以真正销毁资源,这就需要在类中手动实现一种拷贝的“引用计数”,或者利用智能指针shared_ptr来管理。(shared_ptr会自己记录有多少用户共享指向的对象,当没有用户使用对象时,shared_ptr会负责资源你的释放。)
2.2. 移动
-
当没有定义移动相关的函数时,右值也可能被拷贝。
-
为把左值“变为”右值进行移动,可以对对象使用
std::move()
函数。例:
Foo x;
Foo y(x); // x为左值;拷贝构造
Foo z(std::move(x)); // std::move(x)为右值;移动构造
hp = hp2; // hp2为左值;拷贝赋值
hp = std::move(hp2) // std::move(x)为右值;移动赋值
3. 总结
书中也提到,如果需要定义5个中的一个拷贝控制函数,最好将5个都定义全。C++拷贝控制的显式或隐式规则如此之复杂,这既给予了程序员足够的拷贝控制灵活度,也带来了类设计和资源管理的更大挑战,所谓权限越大,责任越大。本文只关注5种函数的区分以及一些相关概念之间的区分,没有详述函数定义默认函数合成、权限等很多细节,这些都可以从书中找到。我还可能在以后的博客中将C++与Rust、Python、Java等语言的内存控制进行对比。