第 13 章 拷贝控制
2019-06-27 本文已影响0人
cb_guo
- 当定义一个类时,我们显式地或隐式地指定在此类型的对象拷贝,移动,赋值和销毁时做什么
- 一个类通过五种特殊的成员函数来控制这些操作,包括:拷贝构造函数,拷贝赋值运算符,移动构造函数,移动赋值运算符 和 析构函数
- 拷贝和移动构造函数定义了当用同类型的另一个对象初始化本对象时做什么。拷贝和移动赋值运算符定义了将一个对象赋予同类型的另一个对象时做什么。析构函数定义了当此类型对象销毁时做什么
- 我们称这些操作为拷贝控制操作
13.1 拷贝、赋值与销毁
- 以最基本操作 拷贝构造函数,拷贝赋值运算符 和 析构函数 作为开始。移动操作在 13.6 节讲述
13.1.1 拷贝构造函数
- 如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则称此构造函数是拷贝构造函数
拷贝初始化
- 当使用直接初始化时,我们实际上时要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数
- 当我们使用拷贝初始化时,我们要求编译器将右侧运算对象拷贝到正在创建的对象中,如果需要的话还要进行类型转换
string dots(10, '.'); // 直接初始化
string s(dots); // 直接初始化
string s2 = dots; // 拷贝初始化
string null_book = "9-999-99999-9"; // 拷贝初始化
string nines = string(100, '9'); // 拷贝初始化
参数和返回值
- 在函数调用过程中,具有非引用类型的参数要进行拷贝初始化
- 当一个函数具有非引用的返回类型时,返回值会被用来初始化调用方的结果
13.1.2 拷贝赋值运算符
- 与类控制其对象如何初始化一样,类也可以控制其对象如何赋值
Sales_data trans, accum;
trans = accum; // 使用 Sales_data 的拷贝赋值运算符
- 与拷贝构造函数一样,如果类未定义自己的拷贝赋值运算符,编译器会为它合成一个
重载赋值运算符
- 重载运算符本质上是函数,其名字由 operator 关键字后接表示要定义的运算符的符号组成。因此,赋值运算符就是一个名为 operator= 的函数。类似于任何其他函数,运算符函数也有一个返回类型和一个参数列表
- 赋值运算符通常应该返回一个指向其左侧运算对象的引用
class Foo{
public:
Foo& operator=(const Foo&); // 赋值运算符
// ....
};
A& operator= (const A& a){ //拷贝赋值运算符
val = a.val;
return *this;
}
- 与处理拷贝构造函数一样,如果一个类未定义自己的拷贝赋值运算符,编译器会为它生成一个合成拷贝赋值运算符
13.1.3 析构函数
- 析构函数执行与构造函数相反的操作:构造函数初始化对象的非 static 数据成员,还可能做一些其他工作;析构函数释放对象使用的资源,并销毁对象的非 static 数据成员
- 析构函数是类的一个成员函数,名字由波浪号接类名构成。它没有返回值也不接受参数
class Foo{
public:
~Foo(); // 析构函数
// ...
};
- 由于析构函数不接受参数,因此它不能被重载。对于一个给定类,只会有唯一一个析构函数
- 无论何时一个对象被销毁,就会自动调用其析构函数
1, 变量在离开其作用域时被销毁
2, 当一个对象被销毁时,其成员被销毁
3, 容器被销毁时,其元素被销毁
4, 对于动态分配的对象,当对指向它的指针应用 delete 运算符时被销毁
5, 对于临时对象,当创建它的完整表达式结束时被销毁 - 析构函数体自身并不直接销毁成员。成员是在析构函数体之后隐含的析构阶段中被销毁的
13.1.4 三/五法则
- 如果一个类需要一个析构函数,我们几乎可以肯定它也需要一个拷贝构造函数和一个拷贝赋值运算符
- 如果一个类需要一个拷贝构造函数,几乎可以肯定它也需要一个拷贝赋值运算符。反之亦然 - 如果一个类需要一个拷贝赋值运算符,几乎可以肯定它也需要一个拷贝构造函数。然而,无论是需要拷贝构造函数还是需要拷贝赋值运算符都不必然意味着也需要析构函数
13.1.5 使用 =default
- 我们可以通过将拷贝控制成员定义为 =default 来显式地要求编译器生成合成的版本。合成的版本就是默认版本
13.1.5 阻止拷贝
- 大多数类应该定义默认构造函数、拷贝构造函数和拷贝赋值运算符,无论是隐式地还是显式地
- 虽然大多数类应该定义拷贝构造函数和拷贝赋值运算符,但对某些类来说,这些操作没有合理的意义。在此情况下,定义类时必须采用某种机制阻止拷贝或赋值
定义删除的函数
- 在新标准下,我们可以将拷贝构造函数和拷贝赋值运算符定义为删除的函数来阻止拷贝。
- 删除的函数是这样一种函数: 我们虽然声明了它们,但不能以任何方式使用它们。
- 在函数的参数列表后面加上 =delete 来指出我们希望将它定义为删除的。=delete 通知编译器(以及我们代码的读者),我们不希望定义这些成员
struct NoCopy{
NoCopy () = default; // 使用合成的默认构造函数
NoCopy (const NoCopy&) = delete; // 阻止拷贝
NoCopy &operator=(const NoCopy&) = delete; // 阻止赋值
~NoCopy() = default; // 使用合成的析构函数
};
析构函数不能是删除的函数
- 对于析构函数已删除的类型,不能定义该类型的变量或释放指向该类型动态分配对象的指针
struct NoDtor{
NoDtor () = default; // 使用默认构造函数
~NoDtor() = delete; // 我们不能销毁 NoDtor 类型的对象
}
NoDtor nd; // 错误:NoDtor 的析构函数是删除的
NoDtor *p = new NoDtor(); // 正确: 但我们不能 delete p
delete p; // 错误: NoDtor 的析构函数是删除的
合成的拷贝控制成员可能是删除的
- 本质上,当不可能拷贝、赋值或销毁类的成员时,类的合成拷贝控制成员就被定义为删除的
private 拷贝控制
- 在新标准发布之前,类是通过将其拷贝构造函数和拷贝赋值运算符声明为 private 来阻止拷贝
class PrivateCopy{
// 无访问说明符;接下来的成员默认是 private
// 拷贝控制成员是 private 的,因此普通用户代码无法访问
PrivateCopy(const PrivateCopy&);
PrivateCopy &operator=(const PrivateCopy&);
// 其他成员
public:
PrivateCopy() = default; // 使用合成的默认构造函数
~PrivateCopy(); // 用户可以定义此类型的对象,但无法使用它们
};
- 由于析构函数是 public 的,用户可以定义 PrivateCopy 类型的对象。但是,由于拷贝构造函数和拷贝赋值运算符是 private 的,用户代码将不能拷贝这个类型的对象
建议:希望阻止拷贝的类应该使用 =delete 来定义它们自己的拷贝构造函数和拷贝赋值运算符,而不应该将它们声明为 private 的
13.2 拷贝控制和资源管理
- 类的行为像一个值,意味着它应该也有自己的状态。当我们拷贝一个像值的对象时,副本和原对象是完全独立的。改变副本不会对原对象有任何影响,反之亦然
- 行为像指针的类则共享状态。当我们拷贝一个这种类的对象时,副本和原对象使用相同的底层数据。改变副本也会改变原对象,反之亦然
13.2.1 行为像值的类 重点
- C++ Primer 453页,这一节太完美了,直接去看书吧,都是重点
- 讲的是深拷贝
13.2.2 定义行为像指针的类 重点
- C++ Primer 455页,这一节太完美了,直接去看书吧,都是重点
- 可以理解为 shared_ptr 的底层实现。(腾讯音乐面试就问到了,可惜当时太菜)
13.3 交换操作
- C++ Primer 457页,这一节太完美了,直接去看书吧,都是重点
- 简而言之,直接交换两个对象的话,涉及到深拷贝 (重新分配一块空间,将将新内容放到这块空间,原空间内容释放) 这一系列的操作,很是没必要。这节讲的是直接交换指向两块空间的指针。
#include<iostream>
using namespace std;
class AA{
// 友元, 以便访问 AA 的 private 数据成员
friend void swap(AA &l, AA &r);
public:
// 构造函数
AA(int bb):aa(bb) {
std::cout << aa << " 构造函数" << endl;
}
// 拷贝构造函数
AA(const AA &temp){
std::cout << temp.aa << " 拷贝构造函数" << endl;
this->aa = temp.aa;
}
// 拷贝赋值运算符
AA& operator=(const AA &temp){
std::cout << " 拷贝赋值运算符" << endl;
this->aa = temp.aa;
return *this;
}
~AA(){
std::cout << aa << " 析构函数" << endl;
}
private:
int aa;
};
// 内联函数
inline void swap(AA &l, AA &r){
std::cout << "交换之前, l.aa = " << l.aa << ", r.aa = " << r.aa << endl;
std::swap(l.aa, r.aa);
std::cout << "交换完成, l.aa = " << l.aa << ", r.aa = " << r.aa << endl;
}
int main(){
AA ff(11); // 调用构造函数
AA gg = ff; // 调用拷贝构造函数
AA hh(ff); // 调用拷贝构造函数
AA kk(77); // 调用构造函数
kk = ff; // 调用拷贝赋值运算符
AA a(11111);
AA b(22222);
swap(a, b);
}
11 构造函数
11 拷贝构造函数
11 拷贝构造函数
77 构造函数
拷贝赋值运算符
11111 构造函数
22222 构造函数
交换之前, l.aa = 11111, r.aa = 22222
交换完成, l.aa = 22222, r.aa = 11111
11111 析构函数
22222 析构函数
11 析构函数
11 析构函数
11 析构函数
11 析构函数
13.4 拷贝控制示例
- 讲了一个拷贝控制的小例子,挺好的,建议看看
13.5 动态内存管理类
- 实现标准库 vector 的一个简化版本。功能是不使用模板来实现
- 类在运行时分配可变大小的内存空间
13.6 对象移动
- 新标准的一个最主要特性是可以移动而非拷贝对象的能力。很多情况下都会发生对象的拷贝。在其中某些情况下,对象拷贝后就立即被销毁了。在这种情况下,移动而非拷贝对象会大幅度提升性能
- 在上一节中看到,我们的 StrVec 类是这种不必要的拷贝的一个很好的例子。在重新分配内存的过程中,从旧内存将元素拷贝到新内存是不必要的,更好的方式是移动元素
13.6.1 右值引用
- 去看书吧
13.6.2 移动构造函数和移动赋值运算符
- 去看书吧
13.6.3 对象移动
- 去看书吧