C++ 拷贝控制简述

本文主要参考《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等语言的内存控制进行对比。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注