C++Primer 5th:第十三章 拷贝控制
本文最后更新于:2021年4月27日 上午
第十三章 拷贝控制(copy control)
- 类如何控制在对象拷贝、赋值、移动和销毁时做什么
- 拷贝构造函数(copy constructor)
- 拷贝赋值运算符(copy-assignment operator)
- 移动构造函数(move constructor)
- 移动赋值运算符(move-assignment operator)
- 析构函数(destructor)
- 新标准:右值引用和移动操作
13.1 拷贝、赋值与销毁
13.1.1 拷贝构造函数
- 第一个参数是自身类类型的引用,且任何额外参数都有默认值
- 几乎总是一个const的引用
- 拷贝构造函数不应该是explicit的(拷贝构造函数在几种情况下会被隐式地使用)
1 |
|
1. 合成拷贝构造函数
- 阻止我们拷贝该类类型的对象
- 编译器从给定对象中依次将每个非static成员拷贝到正在创建的对象中
1 |
|
2. 拷贝初始化
1 |
|
拷贝初始化何时发生:
用
=
定义变量时将一个对象作为实参传递给一个非引用类型的形参
- 从一个返回类型为非引用类型的函数返回一个对象
用花括号列表初始化一个数组的元素或一个聚合类中的成员
当初始化标准库容器或调用其
insert
或push
成员时,容器会对其元素进行拷贝初始化- 用
emplace
成员创建的元素都进行直接初始化
3. 参数和返回值
- 在函数调用过程中,具有非引用类型的参数要进行拷贝初始化
- 当函数具有非引用的返回类型时,返回值会被用来初始化调用方的结果
- !拷贝构造函数被用来初始化非引用类类型参数,如果其参数不是引用类型,则调用永远不会成功:为了调用拷贝构造函数,我们必须拷贝它的实参,但为了拷贝实参,又需要调用拷贝构造函数。
4. 拷贝初始化的限制
1 |
|
5. 编译器可以绕过拷贝构造函数
- 拷贝构造函数必须是存在且可访问的
1 |
|
13.1.2 拷贝赋值运算符
1. 重载赋值运算符
赋值运算符是一个名为
operator=
的函数赋值运算符通常应该返回一个指向其左侧运算对象的引用
1 |
|
2. 合成拷贝赋值运算符
- 对于某些类,用来禁止该类型对象的赋值
1 |
|
13.1.3 析构函数
- 没有返回值,也不接受参数
- 不能被重载
1 |
|
1. 析构函数完成什么工作
- 构造函数中,成员的初始化是在函数体执行之前完成的,且按照它们在类中出现的顺序进行初始化
- 析构函数中,首先执行函数体,然后按初始化顺序的逆序销毁成员
- 隐式销毁一个内置指针类型的成员不会delete它所指向的对象
2. 什么时候调用析构函数
- 变量在离开其作用域时被销毁
- 当一个对象被销毁时,其成员被销毁
- 容器(无论是标准库容器还是数组)被销毁时,其元素被销毁
- 对于动态分配的对象,当对指向它的指针应用delete运算符时被销毁
- 对于临时对象,当创建它的完整表达式结束时被销毁
1 |
|
- 当指向一个对象的引用或指针离开作用域时,析构函数不会执行
3. 合成析构函数
对于某些类,合成析构函数被用来阻止该类型的对象被销毁
析构函数体自身并不直接销毁成员
- 成员是在析构函数体之后隐含的析构阶段中被销毁的
1 |
|
13.1.4 三/五法则
1. 需要析构函数的类也需要拷贝和复制操作
为HasPtr
定义一个析构函数,但使用合成版本的拷贝构造函数和拷贝赋值运算符
1 |
|
这些函数简单拷贝指针成员,多个HasPtr
对象可能指向相同的内存
1 |
|
析构函数 delete ret 和 hp中的指针成员,但这两个对象包含相同的指针值,代码会导致此指针被 delete 两次。
2. 需要拷贝操作的类也需要赋值操作,反之亦然
13.1.5 使用=default
- 将拷贝控制成员定义为
=default
来显式的要求编译器生成合成的版本 - 只能对编译器可以合成的默认构造函数或拷贝控制成员使用
=default
1 |
|
13.1.6 阻止拷贝
iostream
类组值了拷贝,以避免多个对象写入或读取相同的 IO 缓冲
1. 定义删除的函数(deleted function)
- 虽然声明了它们,但不能以任何方式使用它
- 在函数的参数列表后面加上
=delete
=delete
必须出现在函数第一次声明的时候- 可以对任何函数指定
=delete
1 |
|
2. 析构函数不能是删除的成员
对于析构函数已删除的类型,不能定义该类型的变量或释放指向该类型动态分配对象的指针,但可以动态分配这种类型的对象
1 |
|
3. 合成的拷贝控制成员可能是删除的
- 如果一个类有数据成员不能默认构造、拷贝、赋值和销毁,则对应的成员函数将被定义为删除的
4. private拷贝控制
- 通过声明(但不定义)private 的拷贝构造函数,可以预先阻止任何拷贝该类型对象的企图:试图拷贝对象的用户代码将在编译阶段标记为错误;成员函数或友元函数中的拷贝操作将会导致链接时错误
- 希望阻止拷贝的类应该使用
=delete
来定义它们自己的拷贝构造函数和拷贝赋值运算符,而不应该将他们声明为private
的
13.2 拷贝控制和资源管理
如何拷贝指针成员决定了像HasPtr
这样的类是具有类值行为还是类指针行为。
13.2.1 行为像值
的类
对于类管理的资源,每个对象都应该拥有一份自己的拷贝。
为了实现类值行为,HasPtr
需要
- 定义一个拷贝构造函数,完成 string 的拷贝,而不是拷贝指针
- 析构函数释放 string
- 拷贝赋值运算符释放当前对象的 string,并从右侧对象拷贝 string
1 |
|
1. 类值拷贝赋值运算符
先拷贝右侧运算对象,处理自赋值情况,并保证在异常发生时代码也是安全的
1 |
|
编写赋值运算符
时,记住两点
- 如果将一个对象赋予它自身,赋值运算符必须能正确工作
- 大多数赋值运算符组合了析构函数和拷贝构造函数的工作
13.2.2 定义行为像指针
的类
- 令一个类展现类似指针的行为的最好方法:使用
shared_ptr
来管理类中的资源 - 直接管理资源时,使用
引用计数
1. 引用计数
计数器不能直接作为HasPtr
对象的成员
1 |
|
解决方法:将计数器保存在动态内存中
2. 定义一个使用引用计数的类
1 |
|
3. 类指针的拷贝成员篡改引用计数
析构函数
- 递减引用计数,但计数器变为0,释放 ps 和 use 指向的内存
1 |
|
拷贝赋值运算符
- 递增右侧运算对象的引用计数,递减左侧运算对象的引用计数,在必要时释放使用的内存
- 处理自赋值
1 |
|
13.3 交换操作
交换两个类值HasPtr
对象的代码可能像这样:
1 |
|
理论上这些内存分配是不必要的,我们更希望 swap 交换指针
1 |
|
1. 编写自己的 swap 函数
对于分配了资源的类,定义swap
可能是一种很重要的优化手段
1 |
|
2. swap 函数应该调用 swap,而不是 std::swap
标准库 swap 对 HasPtr 管理的 string 进行了不必要的拷贝
3. 在赋值运算符中使用 swap
拷贝并交换(copy and swap)
- 自动处理了自赋值情况,且天然就是异常安全的
1 |
|
13.4 拷贝控制示例(邮箱处理应用)
(重要实践 - 待补充)
13.5 动态内存管理类(StrVec类)
(重要实践 - 待补充)
13.6 对象移动
- 新标准的一个最重要的特性:可以移动而非拷贝对象的能力
- IO 类或 unque_ptr 这样类都包含不能被共享的资源(如指针或IO 缓冲),因此这些类型的对象不能拷贝但可以移动
13.6.1 右值引用
- 必须绑定到右值的引用
- 通过
&&
来获得 - 只能绑定到一个将要销毁的对象
- 一个左值表达式表示的是一个对象的身份,一个右值表达式表示的是对象的值
1 |
|
1. 左值持久:右值短暂
右值引用只能绑定到临时对象
- 所引用的对象将要被销毁
- 该对象没有其他用户
使用右值引用的代码可以自由的接管所引用的对象的资源
2. 变量是左值
不能将一个右值引用直接绑定到一个变量上,即使这个变量是右值引用类型也不行
1 |
|
3. 标准库 move 函数
显式的将一个左值转换为对应的右值引用类型
头文件:utility
1 |
|
- 调用 move 意味着承诺:除了对 rr1 赋值或销毁它外,将不再使用它
- 使用 move 的代码应该使用 std::move 而不是 move,这样可以避免潜在的名字冲突
13.6.2 移动构造函数和移动赋值运算符
(待补充)
13.6.3 右值引用和成员函数
(待补充)
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!