知识点:
拷贝控制操作:拷贝、移动、赋值和销毁
在定义一个类时,我们可以显式或隐式的定义在此类型的对象拷贝、赋值、移动、销毁是做什么,主要通过五种特殊的成员函数来完成这些操作:拷贝构造函数、拷贝复制运算符、移动构造函数、移动复制运算符和析构函数
1.拷贝构造函数:
- 定义: 一个构造函数的第一个参数是自身类型的引用,且任何额外参数都有默认值
- 拷贝构造函数在几种情况下都会被隐式使用,因此拷贝构造函数通常不应该是explicit的
- 如果我们未自定义拷贝构造函数,则编译器为我们定义一个,称为合成拷贝构造函数(一般情况下,合成拷贝构造函数将其参数的成员逐个拷贝到正在创建的对象中)
- 每个成员的类型决定了它的拷贝方式,对于类类型,将调用其拷贝构造函数进行拷贝;对于内置类型,则会直接拷贝;对于数组的拷贝是逐个元素的拷贝,若数组的元素是类类型,则使用拷贝构造函数来拷贝
- 直接初始化与拷贝初始化:
string s(dots); //直接初始化
string s2 = dots; //拷贝初始化
- 拷贝初始化发生的情况:
/*1.用=定义变量时发生
2.将一个对象作为实参传递给一个非引用类型的形参
3.从一个返回类型为非引用类型的函数返回一个对象
4.用花括号列表初始化一个数组中的元素或一个聚合类中的成员*/
- 拷贝初始化的限制:
vector<int> v1(10); //直接初始化
vector<int> v2 = 10; //错误:接受大小参数的构造函数是explicit的
void f(vector<int>); //f的参数进行拷贝初始化
f(10); //错误:不能用一个explicit的构造函数拷贝一个实参
f(vector<int> 10); //从一个int直接构造一个临时vector
2.拷贝赋值运算符:
- 赋值运算符就是一个名为operator=的函数;类似其他函数,运算符函数也有一个返回类型和一个参数列表
class Foo {
public:
Foo& operator=(const Foo&); //赋值运算符
//...
};
- 赋值运算符通常应该返回一个指向其左侧运算对象的引用
- 标准库通常要求保存在容器中的类型要具有赋值运算符,且其返回值是左侧运算对象的引用
- 合成拷贝赋值运算符:将右侧运算对象的每个非static成员赋予左侧运算对象的对应成员
3.析构函数:
- 析构函数:类的一个成员函数,名字由波浪号接类名构成
class Foo {
public:
~Foo(); //析构函数
//...
};
- 作用: 释放对象使用的资源,并销毁对象的非static数据成员 (成员按初始化顺序的逆序销毁)
- 销毁类类型的成员需要执行自己的析构函数; 隐式销毁一个内置指针类型的成员不会delete它所指向的对象
- 调用析构函数:
/*1.变量在离开其作用域时被销毁
2.当一个对象被销毁时,其成员被销毁
3.容器被销毁时,其元素被销毁
4.对于动态分配的对象,当对指向它的指针应用delete运算符时被销毁
5.对于临时对象,当创建它的完整表达式结束时被销毁*/
//当指向一个对象的引用或指针离开作用域时,析构函数不会执行
- 合成析构函数:函数体为空
4.三/五法则和使用=default:
- 如果一个类需要自定义析构函数,几乎可以肯定它也需要自定义拷贝赋值运算符和拷贝构造函数
- 需要拷贝操作的类也需要赋值操作,反之亦然
- 当在类内用=default修饰成员的声明时,合成的函数将隐式地声明为内联的
- 只能对具有合成版本的成员函数使用=default(即,默认构造函数或拷贝控制成员)
5.阻止拷贝i:
- 通过将拷贝构造函数和拷贝赋值运算符定义为删除的函数来阻止拷贝:
struct NoCopy{
//....
NoCopy(const NoCopy&) = delete; //阻止拷贝
NoCopy &operator=(const NoCopy&) = delete; //阻止赋值
//....
};
//可以对任何函数指定=delete
- 对于一个删除了析构函数的类型,编译器将不允许定义该类型的变量或创建该类的临时对象
- 对于析构函数已删除的类型,不能定义该类型的变量或释放指向该类型动态分配对象的指针
- 本质上,当不可能拷贝、赋值或销毁类的成员时,类的合成拷贝控制成员就被定义为删除的
6.拷贝控制和资源管理:
- 管理类外资源的类必须定义拷贝控制成员;一旦一个类需要析构函数,那么它几乎肯定也需要一个拷贝构造函数和一个拷贝赋值运算符
- 为了定义拷贝控制成员,可以定义拷贝操作,使类的行为看起来像一个值或者像一个指针
- 类的行为像一个值,拷贝发生时,副本和原对象是完全独立的,改变副本不会对原对象产生影响
- 类的行为像一个指针,拷贝发生时,副本和原对象公用底层数据,改变副本也会改变原对象
- 标准库容器和string类的行为像一个值,shared_ptr类就是像值的类,IO类和unique_ptr不允许拷贝和赋值,所以不是
- 行为像值的类:
/*1.类值版本,可能需要动态分配其成员的副本
2.赋值运算符通常组合了析构函数和构造函数的操作;首先销毁左侧运算对象的资源,再从右侧运算符对象拷贝对象
3.对于存在这样一个顺序,所以我们必须保证这样的拷贝赋值运算符是正确的;所以先将右侧运算符对象拷贝到一个临时对象中,再销毁左侧的运算对象的现有成员,之后将临时对象中的数据成员拷贝至左侧对象中(防范自赋值的情况发生—首先就销毁了自身的成员,再进行拷贝自身则会访问到已经释放的内存中)*/
- 行为像指针的类:
/*1.定义行为像指针的类,在希望直接管理资源的情况使用自己设计的引用计数来判断是否释放内存
2.除了初始化对象外,每个构造函数还要创建一个引用计数用来记录共享状态
3.拷贝构造函数不分配新的计数器,而是拷贝给定对象的数据成员,包括计数器;拷贝构造函数递增计数器
4.析构函数递减计数器,指出共享状态的用户少了一个;如果计数器变为0,则析构函数释放状态
5.拷贝赋值运算符递增右侧运算对象的计数器,递减左侧运算对象的计数器*/
7.swap函数:
- 如果类定义了自己的swap,那么算法将使用类自定义版本
- 在进行两个对象交换的时候,我们不希望进行新的内存分配,只希望将其指针进行拷贝赋值,省去不必要的内存分配,可以定义自己的swap函数
- 与拷贝控制成员不同,swap并不是必要的。但是,对于分配了资源的类,定义swap可能是一种很重要的优化手段
- 在赋值运算符中使用swap,以传值的方式传入新对象,再进行拷贝赋值,在一定程度上比较安全
8.拷贝控制示例与动态内存管理类:
对于这两节,书中的Message和Folder类,StrVec类一定要自己亲手写一遍并且理解程序的每个功能。。。
9.对象移动:
- 在重新分配内存的过程中,从旧元素将元素拷贝到新内存是不必要的,更好的方式是移动元素;标准库容器、string、shared_ptr既支持移动也支持拷贝,IO类和unique_ptr类可以移动但不能拷贝
- 右值引用:必须绑定到右值的引用,通过&&来获得右值引用;只绑定到一个将要销毁的对象,因此,我们可以自由的将右值引用的资源“移动”到另一个对象中
- 一个右值引用本质上也只是一个对象的另外一个名字而已。对于常规的引用:称之为左值引用,我们不能将其绑定到所要转换的表达式,而右值引用可以绑定:
int i = 42;
int &r = i; //正确:r引用i
int &&rr = i; //错误:不能将一个右值引用绑定到一个左值上
int &r2 = i * 42; //错误:i * 42是一个右值
const int &r3 = i * 42; //正确:我们可以将一个const的引用绑定到一个右值上
int &&rr2 = i * 42; //正确:rr2绑定到乘法结果上
- 变量是左值,因此我们不能将一个右值引用直接绑定到一个变量上,即使这个变量是右值引用类型也不行
- 标准库定义了一个名为move的函数,可以显式地将一个左值转换为对应的右值引用类型,该函数定义在头文件utility中,使用时应该使用std::move
int &&rr3 = std::move(rr1); //显式转换
- 可以销毁一个移后源对象,也可以赋予新值,但不能使用一个源对象的值
10.移动构造函数和移动赋值运算符:
- 移动构造函数:与拷贝构造函数类似,不过第一个参数为一个右值引用;一旦资源被移动,源对象对移动之后的资源已经不再有控制权,最后源对象会被销毁。注意,移动构造函数是“窃取”资源,并不会分配资源。
- 不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept-----新标准引入,必须在头文件的声明中和定义中都指定noexcept
class StrVec;
StrVec(StrVec &&s) noexcept;
- 通过将移后源对象的指针成员置为nullptr来确保移后源对象可析构
- 只有当一个类没有定义任何自己版本的拷贝控制成员,且它的所有数据成员都能移动构造或移动赋值时,编译器才会为它合成移动构造函数或移动赋值运算符;定义了一个移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作,不然这些成员默认地定义为删除的
- 移动右值,拷贝左值,但如果没有移动构造函数,右值也被拷贝
class Foo {
public:
Foo() = default;
Foo(const Foo&); //拷贝构造函数
//其他成员定义,但Foo未定义移动构造函数
};
Foo x;
Foo y(x); //拷贝构造函数;x是一个左值
Foo z(std::move(x)); //拷贝构造函数,因为未定义移动构造函数
- 所有五个拷贝控制成员应该在类中一起出现!
- 移动赋值运算符必须销毁左侧运算对象的旧状态
11.右值引用和成员函数:
- 区分拷贝和移动的重载函数通常有一个版本接收const T &(左值引用),一个版本接受T&&(右值引用参数)——引用限定符,这样我们调用函数时,实参的类型决定了新元素是进行拷贝还是移动操作
void push_back(const string&); //拷贝元素
void push_back(string&&); //移动元素
- 在函数的参数列表之后加上&或者&&表示该函数只能用于左值或者右值
Foo sorted() &&; //可用于改变的右值
Foo sorted() const &; //可用于任何类型的Foo
- 当我们定义两个或两个以上的具有同名且参数列表相同的成员函数,必须对所有函数都加上引用限定符或者都不加