第十三章 拷贝控制 466
作者:互联网
13-1拷贝、赋值与销毁
1、拷贝构造函数
拷贝构造函数的第一个参数必须是引用类型,一般都是const引用,也可以是非const引用
合成拷贝构造函数
如果我们没有为一个类定义拷贝构造函数,编译器会为我们定义一个。即使我们定义了其它构造函数,编译器也会为我们合成一个拷贝构造函数
对某些类来说,合成拷贝构造函数用来组织我们拷贝该类类型的对象
合成的拷贝构造函数会将其参数的成员逐个拷贝到正在创建的对象中
编译器从给定对象中依次将每个非static成员拷贝到正在创建的对象中
类类型成员会使用拷贝构造函数来拷贝,内置类型则是直接拷贝
Sales_data类的合成拷贝构造函数等价于:
拷贝初始化
直接初始化和拷贝初始化的区别:
如果一个类有一个移动构造函数,拷贝初始化有时会使用移动构造函数而非拷贝构造函数来完成
某些类类型会对它们分配的对象进行拷贝初始化:初始化标准库容器或调用其insert成员或push成员时,容器会对其元素进行拷贝初始化
用emplace创建的元素都进行直接初始化
参数和返回值
在函数调用过程中,具有非引用类型的参数要进行拷贝初始化
当一个函数具有非引用返回类型时,返回值被用来初始化调用方的结果
拷贝初始化的限制
加了关键字explicit的构造函数只能直接初始化
编译器可以绕过拷贝构造函数
即使编译器略过了移动或拷贝构造函数,移动或拷贝构造函数也也要是存在且可访问的
2、拷贝赋值运算符
重载赋值运算符
重载运算符本质上是函数,其名字由operator关键字后接表示要定义的运算符的符号组成,运算符函数也有一个返回类型和一个参数列表
赋值运算符就是一个名为operator=的函数
重载运算符的参数表示运算符的运算对象。某些运算符必须定义为成员函数
如果一个运算符是一个成员函数,其左侧运算对象就绑定到隐式this参数,右侧运算对象作为显式参数传递
赋值运算符通常返回一个指向其左侧运算对象的引用
合成拷贝赋值运算符
如果一个类未定义一个自己的拷贝赋值运算符,编译器会为它生成一个合成拷贝赋值运算符
合成拷贝赋值运算符用来禁止该类型对象的赋值
等价于Sales_data的合成拷贝赋值运算符的代码:
3、析构函数
构造函数初始化对象的非static数据成员
析构函数释放对象使用的资源,并销毁对象的非static数据成员
析构函数是类的一个成员函数,名字由波浪号接类名构成,没有返回值,也不接受参数:
析构函数没有参数,所以不能被重载,一个给定的类只有一个析构函数
析构函数完成什么工作
析构函数有一个函数体和析构部分
在一个析构函数中,首先指向函数体,然后销毁成员
成员按初始化顺序的逆序销毁
析构函数释放对象在生存期分配的所有资源
在析构函数中,析构部分是隐式的,成员销毁时发生什么完全依赖于成员的类型
智能指针是类类型,所有具有析构函数
什么时候会调用析构函数
例如:
合成析构函数
当一个类未定义自己的析构函数时,编译器会为它定义一个合成析构函数
合成析构函数用来阻止该类型的对象被销毁
4、三/五法则
需要析构函数的类也需要拷贝和赋值操作
HasPtr类在构造函数中分配动态内存,它需要定义一个析构函数来释放分配的内存
如果定义了析构函数,但仍使用合成的拷贝、赋值:
会导致多个HasPtr对象可能指向相同的内存:
f返回时,hp和ret都被销毁,一个指针被delete两次
需要拷贝的类也需要赋值操作,反之亦然
5、使用=default
我们可以通过将拷贝控制成员定义为=default来显式地要求编译器生成合成的版本:
在类内用=default修饰成员的声明时,合成的函数将隐式声明为内联函数
如果不希望是内联的,就要在类外定义时使用=default
6、阻止拷贝
如果不定义拷贝控制成员,编译器会会生成合成的版本,不能达到阻止拷贝的目的
定义删除的函数
可以通过将拷贝构造函数和拷贝赋值运算符定义为删除的函数来阻止拷贝
在函数的参数列表后加上=delete来指出我们希望将它定义成删除的:
=delete必须出现在函数第一次声明的时候
可以对任何函数指定=delete
只能对编译器合成的默认构造函数或拷贝控制成员使用=default
析构函数不能是删除的成员
如果一个成员的析构函数是删除的,则该成员无法被销毁,导致对象整体也无法被销毁
合成的拷贝控制成员可能是删除的
对某些类来说,编译器将合成的成员定义为删除的函数:
这些规则本质上的含义是:如果一个类有数据成员不能默认构造、拷贝、赋值或销毁,则对应的成员函数将被定义为删除的
private拷贝控制
将拷贝构造函数和拷贝赋值运算符声明为private的来阻止拷贝:
声明但不定义一个成员函数是合法的
13-2拷贝控制和资源管理
通常,管理类外资源的类必须定义拷贝控制成员
1、行为像值的类
为了提供类值的行为,对于类管理的资源,每个对象都应该拥有一份自己的拷贝
为了实现类值行为,HasPtr需要:
类值版本的HasPtr:
类值拷贝赋值运算符
赋值运算符通常组合了析构函数和拷贝构造函数的操作
赋值操作从右侧对象拷贝数据,销毁左侧运算对象的资源
自赋值情况:
如果先释放后拷贝:
2、定义行为像指针的类
对于行为类似指针的类,我们定义拷贝和赋值,值用来拷贝指针本身而不是它指向的对象
析构函数不能单方面地释放关联的string,只有当最后一个指向string的HasPtr销毁时,它才可以释放string
希望直接管理资源时,不使用shared_ptr而是设计自己的引用计数
引用计数
创建p3时,可以把p1中的计数器递增并拷贝到p3中,但p2的计数器难以更新
定义一个使用引用计数的类
使用引用计数,编写指针类的HasPtr版本:
use数据成员记录有多少个对象共享相同的string,接受一个string的构造函数将其初始化为1,指出当前有一个用户使用本对象的string成员
类指针的拷贝成员“篡改”引用计数
拷贝或赋值一个HasPtr对象时,我们希望拷贝的是指针本身,并且递增计数器
拷贝构造函数拷贝给定HasPtr的三个数据成员,递增use成员,指出ps和p.ps指向的string又有一个新用户
处理自赋值时,先递增rhs中的计数然后再递减左侧运算对象中的计数:
13-3交换操作
如果一个类定义了自己的swap,那么算法将使用类自定义版本,否则,使用标准库定义的swap
交换对象两次拷贝一次赋值:
这样做占用了不必要的内存
swap交换指针:
编写我们自己的swap函数
swap定义为friend以便访问HasPtr中的数据,并将其声明成inline
swap函数应该调用swap而不是std::swap
为Foo编写一个swap函数:
错误的swap函数:
正确的swap函数:
在赋值运算符中使用swap
拷贝并交换:将左侧运算对象与右侧运算对象的一个副本进行交换:
使用拷贝和交换的赋值运算符自动就是异常安全的,且能处理自赋值情况
13-4拷贝控制示例
命名两个类:Massage和Folder分别表示消息和消息目录
每条Massage可以出现在多个folder中,但相同的massage只有一个副本,如果给定的massage被改变,则对应folder中的massage也会改变
每个massage都保存它所在的folder的指针的set,每个folder都保存它包含的massage的指针的set
massage有save和remove操作,用于从对应folder添加或删除massage
Massage类
两个数据成员:
contents:保存文本
folders:保存指向包含massage的Folder的指针
save和remove成员
Massage可拷贝控制成员
Massage的析构函数
Massage的拷贝赋值运算符
Massage的swap函数
13-5动态内存管理类
StrVec类的设计
StrVec类:标准库vector类的简化版本,只用于string
StrVec有一个名为alloc的静态成员,类型为allocator,用于分配内存
四个工具函数:
StrVec类定义
使用construct
在push back函数中使用construct:
alloc_n_copy成员
free成员
拷贝控制成员
在重新分配内存的过程中移动而不是拷贝元素
移动构造函数和std::move
move是标准库函数,定义在头文件utility中,直接用std::move而不是move
reallocate成员
13-6对象移动
1、右值引用
常规引用(左值引用)不能绑定到要求转换的表达式,字面常量或返回右值的表达式,但右值引用可以
不能将右值引用直接绑定到一个左值上:
左值持久,右值短暂
变量是左值
变量表达式都是左值
不能将一个右值引用绑定到一个右值引用类型的变量上:
标准库move函数
通过move获得绑定到左值上的右值引用:
调用std::move而不是move
2、移动构造函数和移动赋值运算符
为StrVec类定义移动构造函数:
移动构造函数接管内存之后,将给定对象中的指针都置为nullptr,最终,源对象会被销毁
移动操作、标准库容器和异常
noexcept承诺函数不抛出异常,出现在参数列表和初始化列表开始的冒号之间:
class StrVec {
必须在类头文件的声明和定义中都指定noexpect
移动操作通常不抛出异常,但抛出异常也是允许的
标准库容器能对异常发生时其自身的行为提供保障:如果调用push back时发生异常,vector能保障自身不会发生改变
移动赋值运算符
移动赋值运算符标记为noexcept,能处理自赋值情况:
移动后源对象必须可析构
合成的移动操作
如果一个类定义了自己的拷贝、赋值或析构,则系统不会合成移动构造函数和移动赋值运算符
如果一个类没有移动操作,会使用拷贝操作来代替移动操作:
如果一个类定义了一个移动构造函数或移动赋值运算符,则该类的合成拷贝和赋值被定义为删除的
移动右值,拷贝左值
如果一个类既有移动也有拷贝,则编译器使用函数匹配规则来确定用哪个构造函数:
如果没有移动构造函数,右值也被拷贝:
用拷贝构造函数代替移动构造函数几乎肯定是安全的
拷贝并交换赋值运算符和移动操作
HasPtr定义了一个拷贝并交换赋值运算符,如果为它添加一个移动构造函数,它实际上也会获得一个移动赋值运算符:
Massage类的移动操作
更新folder指针:
移动迭代器
标准库没有将对象移动到未构造的内存中的函数
移动迭代器的解引用运算符生成一个右值引用
make_move_iterator函数接受一个普通迭代器,返回一个移动迭代器
将移动迭代器传递给uninitiallized_copy:
3、右值引用和成员函数
移动和拷贝一个接受指向const的左值引用,另一个接受指向非const的右值引用
左值和右值引用成员函数
重载和引用函数
引用限定符也区分重载版本:
编译器会根据调用sorted的对象是左值还是右值来觉定使用哪个sort版本:
如果一个函数有引用限定符,则相同参数列表的所有版本都必须有引用限定符
标签:析构,函数,466,运算符,第十三章,拷贝,赋值,构造函数 来源: https://blog.csdn.net/m0_54235152/article/details/113417667