第13章 拷贝控制
- 拷贝控制操作:拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符、析构函数。
- 当类中没有声明构造函数时,编译器会在其需要时生成合成默认构造函数。当类中没有定义拷贝构造函数时,编译器生成合成拷贝构造函数。合成拷贝赋值运算符、合成析构函数与合成拷贝构造函数类似。当类中没有自定义拷贝控制成员,且每个非static数据成员都可以移动时,编译器才会合成移动构造函数或移动赋值运算符。
- 若一个类需要析构函数,则几乎肯定需要拷贝构造函数、拷贝赋值运算符;若一个类需要拷贝构造函数,则几乎肯定需要拷贝赋值运算符;若一个类需要拷贝赋值运算符,则几乎肯定需要拷贝构造函数。若一个类定义任意一个拷贝控制,则应该定义所有的5个拷贝控制操作。
13.1 拷贝、赋值与销毁
1. 拷贝构造函数
- 拷贝构造函数的第一个参数是自身类型的引用,且任意额外参数都有默认值。拷贝构造函数通常不是
explicit
,第一个参数几乎总是const
。 - 直接初始化要求编译器通过函数匹配选择构造函数,拷贝初始化要求编译器将右侧运算对象拷贝到左侧运算对象,必要时可进行类型转换。
- 使用拷贝初始化的情况:使用
=
定义变量;将实参传递给非引用形参;返回类型为非引用类型的函数返回对象;花括号列表初始化数组元素或聚合类成员;某些类类型会对其分配的对象进行拷贝初始化,如vector
的insert
和push
进行拷贝初始化,emplace
进行直接初始化。 - 拷贝构造函数的第一个参数必须是引用类型,因为函数调用过程中,非引用类型的形参通过拷贝构造函数进行拷贝初始化。若第一个参数不是引用类型,函数调用时非引用类型的形参使用拷贝构造函数初始化,而拷贝构造函数的第一个参数是非引用类型,第一个参数又需要调用拷贝构造函数,如此会无限循环。
- 虽然编译器可以略过拷贝/移动构造函数,但依然要求拷贝/移动构造函数必须存在且可访问。
class Foo{
public:
Foo(); // 默认构造函数
Foo(const Foo&); // 拷贝构造函数
}
string s = "1"; // 拷贝初始化,等价于string temp("1"); string s = temp; //使用拷贝构造函数
string s("1"); // "1":const char *,略过拷贝构造函数
2. 拷贝赋值运算符
- 赋值运算符通常返回一个指向其左侧运算对象的引用。标准库通常要求保存在容器中的类型要具有赋值运算符,且其返回值是左侧运算对象的引用。
- 大多数赋值运算符会结合析构函数和拷贝构造函数的工作。编写赋值运算符时需注意自赋值情况,最好是在销毁左侧运算对象资源之前拷贝右侧运算对象。
3. 析构函数
- 在构造函数中,成员初始化在函数体之前完成,且按照它们在类中出现的顺序初始化。在析构函数中,先执行函数体,再按照初始化顺序的逆序销毁成员。
- 若成员是内置指针类型,析构函数不会自动delete其所指的对象。若成员是智能指针,因智能指针是类类型,故会执行类成员自己的析构函数实现自动销毁。
- 调用析构函数的情况:变量离开作用域时被销毁;当一个对象被销毁时,其成员被销毁;标准库容器或数组被销毁时,其元素被销毁;对于动态分配的对象,当delete指向该对象的指针时被销毁;对于临时对象,当创建它的完整表达式结束时被销毁。
- 当指向一个对象的引用或指针离开作用域时不会执行析构函数。
4. =default
和=delete
- 使用
=default
可显式要求编译器生成合成版本的拷贝控制成员。在类内使用=default
,合成的函数是内联的,在类外使用=default
,合成的函数就不是内联的。只能对具有合成版本的成员函数(默认构造函数,拷贝控制成员)使用=default
。 - 使用
=delete
表示不能以任意方式调用该成员函数。=delete
必须出现在函数第一次声明的时候。可对任意函数使用=delete
,不局限于默认构造函数和拷贝控制成员。 - 最好不要对析构函数使用
=delete
。对于析构函数已删除的类型,不能定义该类型的变量,可动态分配但不能释放该类型的对象。 - 将拷贝控制成员声明为
private
但不定义,可阻止用户代码、友元函数、成员函数进行拷贝控制。
=delete |
原因 |
---|---|
合成默认构造函数 | 1、类成员的析构函数是删除(=delete )或不可访问(private )。2、类中含有引用成员,该成员没有类内初始化器。 3、类中含有const成员,该成员没有类内初始化器,且其类型未显式定义默认构造函数。 |
合成拷贝构造函数 | 1、类成员的拷贝构造函数是删除或不可访问。 2、类成员的析构函数是删除或不可访问。 |
合成拷贝赋值运算符 | 1、类成员的拷贝赋值运算符是删除或不可访问。 2、类中含有const成员或引用成员。 |
合成析构函数 | 1、类成员的析构函数是删除或不可访问。 |
13.2 拷贝控制和资源管理
- 可定义拷贝操作使类的行为看起来像一个值或一个指针。类的行为像一个值,意味着类有自己的状态,副本与原对象无关,修改副本不会改变原对象。类的行为像一个指针,则类共享状态,副本与原对象使用相同底层数据,修改副本会改变原对象。
- 指针成员的拷贝决定类的行为像值或像指针。
- 令类的行为像指针,最好是使用
share_ptr
管理类中的资源。若想直接管理资源,则需使用引用计数。可将引用计数保存在动态内存中。
13.3 交换操作
- 当作用域有
using std::swap
,若存在类型特定的swap
版本,swap
调用会与之匹配,若不存在类型特定版本,则会使用std::swap
- 对于行为类值的类,赋值运算符通过拷贝并交换技术(形参是非引用类型,
swap
定义赋值运算符)可自动处理自赋值情况且天然就是异常安全的。
13.4 拷贝控制示例
- 当类需要分配资源、簿记工作(类似于邮件处理应用中的
Message
和Folder
)等操作时,通常需要拷贝控制。
13.5 动态内存管理类
- 当类需要在运行时分配可变大小的内存空间时,通常使用标准库容器保存其数据。
- 若类需要自己进行内存分配,则必须定义自己的拷贝控制成员来管理所分配的内存。
13.6 对象移动
- 移动而非拷贝对象的情况:对象拷贝后就立即被销毁;
IO
、unique_ptr
等类中包含不能被共享的资源(如IO缓冲、指针)。 - 标准库容器、
string
和shared_ptr
类同时支持拷贝和移动,IO
和unique_ptr
类只支持移动。
1. 左值引用
- 右值引用
&&
即必须绑定到右值的引用。右值引用只能绑定到一个将要销毁的对象。 - 左值和右值是表达式的属性,左值表达式表示一个对象的身份,右值表达式表示对象的值。左值有持久的状态,右值只能是字面常量或表达式求值过程中创建的临时对象。
- 非const左值引用可以绑定赋值、下标、解引用、前置递增/递减、返回左值引用的函数;const左值引用和右值引用可以绑定算术、关系、位、后置递增/递减、要求转换的表达式、字面常量、返回右值的表达式。
-
std::move
可将左值转换为对应的右值引用类型。移后源对象(使用std::move
后的对象)可以被销毁或赋值,但不能使用其值。
int i = 42;
int &r1 = i;
const int &r2 = 42;
int && r3 = 42;
int &&r4 = r3; // 错误,r3是变量,变量是左值
int &&r5 = std::move(i);
2. 移动构造函数和移动赋值运算符
- 移动构造函数第一个参数是非const的右值引用,任何额外参数必须有默认实参。移动构造函数需完成资源移动,保证移后源对象可被销毁和赋值。
- 不抛出异常的移动构造函数和移动赋值函数必须标记
noexcept
。 - 当类中没有自定义拷贝控制成员,且每个非static数据成员都可以移动时,编译器才会合成移动构造函数或移动赋值运算符。
- 定义了一个移动构造函数或移动赋值运算符的类必须定义自己的拷贝操作,否则合成拷贝构造函数和合成拷贝赋值运算符会被定义为删除的。
- 若类中拷贝操作与移动操作同时存在,则进行函数匹配,实参是左值的函数会使用拷贝操作,实参是右值的函数会使用移动操作。若类中只有拷贝操作且实参是右值,则会调用拷贝操作。
-
make_move_iterator
可将普通迭代器转换为移动迭代器。 - 不要随便使用移动操作。移后源对象具有不确定的状态,对其调用
std::move
很危险。当我们调用move
时,必须绝对确认移后源对象没有其它用户。
=delete |
原因 |
---|---|
移动构造函数 | 1、类成员定义自己的拷贝构造函数且未定义移动构造函数 2、类成员未定义自己的拷贝构造函数且编译器不能合成移动构造函数 3、类成员的移动构造函数被定义为删除的或不可访问的 4、类的析构函数被定义为删除的或不可访问的 |
移动赋值运算符 | 1、类成员定义自己的拷贝赋值运算符且未定义移动赋值运算符 2、类成员未定义自己的拷贝赋值运算符且编译器不能合成移动赋值运算符 3、类成员的移动赋值运算符被定义为删除的或不可访问的 4、类成员是 const 或引用 |
3. 右值引用和成员函数
- 除构造函数和赋值运算符外,其它成员函数也可同时提供拷贝和移动版本,拷贝版本接受一个指向const的左值引用,移动版本接受一个指向非const的右值引用。
- 通常我们可以在一个对象上调用成员,而不用管该对象是左值或右值。C++允许向右值赋值,若想阻止该用法,强制使左侧运算对象必须是左值,可在参数列表后放置引用限定符。
(s1+s2) = "abc"; // 虽然s1+s2返回右值,但它依旧可以调用拷贝赋值运算符来实现赋值。
- 引用限定符可以是
&
或&&
,&
指出this
可以指向一个左值,&&
指出this
可以指向一个右值。若const
与引用限定符同时存在,则const
必须在前,引用限定符必须在后。 - 若一个成员函数有引用限定符,则具有相同参数列表的所有版本都必须有引用限定符。