第十三章 拷贝控制
13.1 拷贝、赋值与销毁
1、拷贝构造函数
①、基本概念
拷贝构造函数的第一个参数是自身的引用,并且任何额外的参数都有默认值。
合成拷贝构造函数:如果没有为类定义拷贝构造函数,则编译器会为我们定义一个,称之为合成拷贝构造函数。一般情况,合成的拷贝构造函数会将其参数的成员逐个拷贝到正在创建的对象中
②、拷贝初始化
直接初始化:使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数
拷贝初始化:将右侧运算对象拷贝到正在创建的对象中
拷贝初始化发生的情况:
③、为什么拷贝构造函数的参数必须是引用类型
2、拷贝赋值运算符
①、基本概念
重载运算符本质上是函数,其名字由operator关键字后接表示要定义的运算符的符号组成。
拷贝赋值运算符接受一个与其所在类相同类型的参数,赋值运算符返回一个指向其左侧运算对象的引用
②、合成拷贝构造函数
如果一个类未定义自己的拷贝赋值运算符,则编译器会自动生成一个合成的拷贝赋值运算符。
一般来说,它会将右侧运算对象的每个非static成员赋予左侧运算对象的对应成员。
这一工作通过成员类型的拷贝赋值运算符来完成,对于数组类型的成员,逐个赋值数组元素
3、析构函数
析构函数释放对象使用的资源,并销毁对象的非static数据成员
- 析构函数是类的一个成员函数,名字由波浪号接类名构成。它没有返回值,也不接受参数
- 析构函数首先执行函数体,然后销毁成员。成员顺序按初始化顺序的逆序销毁。
- 成员销毁时执行成员自己的析构函数。内置类型没有析构函数,因此销毁内置类型成员什么也不需要做
①、什么时候调用析构函数
析构函数自动运行,程序可以按照需要分配资源,无须担心何时释放这些资源
当指向一个对象的引用或指针离开作用域时,析构函数不会执行
②、合成析构函数
当一个类未定义自己的析构函数时,编译器会为它自动创建一个合成析构函数。
析构函数自身并不直接摧毁成员,成员是在析构函数体之后隐含的析构阶段中被销毁。在整个对象销毁过程中,析构函数体是作为成员销毁步骤之外的另一部分而进行的。
4、三/五法则
三个控制类的拷贝操作:拷贝构造函数、拷贝赋值运算符、析构函数
新方法:移动构造函数、移动赋值运算符
• 需要析构函数的类也需要拷贝和赋值操作
• 需要拷贝操作的类也需要赋值操作,反之亦然
5、=defualt
6、阻止拷贝
iostream类阻止拷贝,以避免多个对象写入或读取相同的IO缓冲。为了阻止拷贝,我们需要定义删除的函数
①、删除的函数
删除的函数:我们虽然声明了它们,但不能以任何方式使用它们。在函数的参数列表后面加上=delete来指出我们希望将它定义为删除的;
析构函数不能是删除的成员,如果析构函数被删除,就无法销毁此类型的对象了。
②、合成的拷贝控制成员(默认拷贝、默认拷贝赋值、析构)可能是删除的
本质上,当不可能拷贝、赋值或销毁类的成员时,类的合成拷贝控制成员就被定义为删除的
13.2 拷贝控制和资源管理
1、行为像值的类
在执行拷贝的时候,对于指针或引用成员,拷贝一份副本(与传入对象的指针或引用成员指向的对象不一样)
①、类值拷贝赋值运算符
赋值运算符组合了析构函数和构造函数。析构函数销毁左侧运算对象的资源。拷贝构造函数从右侧运算对象拷贝数据。
2、行为像指针的类
拷贝的时候,对于指针或引用引用成员,进行深拷贝。(与传入对象的指针或引用成员共用同一个值)
13.3 交换操作
交换两个对象的值:
①、编写我们自己的swap函数
指定使用的是系统提供的swap版本
②、在赋值运算符中使用swap
定义swap函数的类通常用swap函数来定义它们的赋值运算符。这些运算符使用了一种名为拷贝并交换的技术。
13.6 对象移动
1、右值引用
为了以示区别,将以前的引用称之为左值引用
①、左值是持久的,右值是短暂的
②、标准move函数
显式地将左值引用转换为右值引用
2、移动构造函数和移动赋值运算符
①、移动构造函数
• 移动构造函数的第一个参数是该类类型的一个右值引用。并且其它任何额外的参数都必须有默认实参。
• 移动构造函数必须确保移动后的源对象处于销毁无害状态
• 由于移动操作“窃取”资源,它通常不分配任何资源,所以通常不会抛出任何异常。所以我们需要显式指明noexcept
• 左值被拷贝,右值被引用
②、移动赋值运算符
• 移动赋值运算符执行与析构函数和移动构造函数相同的工作。
• 不抛出异常,需要标记为noexcept。
• 需要正确处理自赋值
移后源对象必须可析构:
有时在移动操作完成后,源对象会被摧毁。我们必须确保移后源对象处于可析构状态。
为了满足这一要求,我们可以将移后源对象的指针成员置为nullptr来实现。
③、合成的移动操作
合成:
如果我们没有定义,则编译器会合成移动构造函数和移动赋值运算符。
但是,如果一个类定义了自己的拷贝构造函数、拷贝赋值运算符或者析构函数。编译器就不会自动合成移动构造函数和移动赋值运算符。
如果类没有移动操作,则会调用拷贝版本的函数来实现相应操作
删除:
移动操作永远不会隐式定义为删除的函数。
如果我们显式的要求编译器生成=default的移动操作,且编译器不能移动所有成员,则编译器会将移动操作定义为删除的函数。
如果类定义了一个移动构造函数和/或一个移动赋值运算符,则该类的合成拷贝构造函数和拷贝赋值运算符会被定义为删除的。
一般来说,拷贝一个资源会导致一些额外开销。在这种拷贝并非必要的情况下,定义移动构造函数和移动赋值运算符的类就可以避免这种问题。
④、移动迭代器
3、右值引用和成员函数
引用限定符:指定拷贝赋值运算符只能作用于左值(&)或者右值(&&)