c++ primer 笔记第十三章拷贝控制

13章 拷贝控制

一个类定义五中特殊成员函数:拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符和析构函数。叫做拷贝控制操作。

13.1 拷贝、赋值与销毁

13.1.1 拷贝构造函数

拷贝函数第一个参数是自身类型的引用,一般是常量引用,且额外参数都有默认值。

拷贝函数通常不是explicit的。

未定义拷贝构造函数时,编译器自动合成一个合成拷贝构造函数。操作是直接拷贝每个成员或者调用成员的拷贝构造函数。

拷贝初始化与直接初始化的区别。拷贝初始化在使用=定义变量时候使用,以及一些其它情况。

函数调用非引用参数和函数返回非引用类型会调用拷贝初始化。解释了拷贝函数参数是引用的原因。

不能隐式调用一个explicit形式的构造函数。

编译器有时候可以跳过拷贝/移动构造函数,直接创建对象。但这时候拷贝/移动构造函数需要可访问。

13.1.2 拷贝赋值运算符

如果类未定义拷贝赋值运算符,编译器会为其合成。

某些运算符,包括赋值运算符,必须定义为成员函数。接受一个与其类型相同的参数。返回同一个指向左侧对象的引用。

扫描二维码关注公众号,回复: 3647357 查看本文章

合成拷贝赋值运算符将右侧运算对象每个非static成员赋予左侧运算对象对应成员,或者禁止对该类型对象赋值。

13.1.3 析构函数

析构函数释放对象使用的资源,并销毁对象的非static数据成员。没有返回值不接受参数的成员函数。不能被重载。

析构函数首先执行函数体,然后销毁成员。成员按初始化顺序逆序销毁。通常释放生存期分配的所有资源。

成员销毁时发生什么依赖于成员类型。类类型成员销毁时执行析构函数,内置类型销毁时什么都不做。智能指针是类类型。

调用析构函数:一、变量离开作用域;二、对象被销毁时,它的成员调用;三、容器被销毁时,元素也被销毁;四、delete指向动态分配的对象的指针时;五、临时对象在创建它的表达式结束时。

合成析构函数为阻止析构或者为空。析构函数本身不销毁成员,而是在析构函数体之后隐含的析构阶段进行销毁。

13.1.4 三/五法则

如果一个类需要析构函数,那么几乎肯定需要拷贝构造函数和拷贝赋值运算符。

如果一个类需要拷贝构造函数,那么几乎肯定也需要拷贝赋值运算符。反之也是。

13.1.5 使用=default

使用=default修饰成员声明,合成的函数将隐式声明为内联的。

13.1.6 阻止拷贝

某些情况需要类阻止拷贝或赋值。比如iostream类。

使用=delete声明表示成员函数不能以任何方式使用。只能在第一次声明的时候。可以对任意函数使用。

删除析构函数的类型,编译器不允许定义该类型变量或者创建该类临时对象。可以动态分配但不能释放。

合成的拷贝控制成员可能是删除的。

新标准之前通过将拷贝构造函数和拷贝赋值运算符声明为private来阻止拷贝。且可以声明但不定义来防止友元拷贝。

13.2 拷贝控制和资源管理

一般管理类外资源的类必须定义拷贝控制函数。定义拷贝操作可以使类的行为类似于值或者指针。

13.2.1 行为像值的类

类似值行为的类需要对于类管理的资源,每个类的对象都有一份自己的拷贝。

赋值运算符考虑两点:一、将一个对象赋予它本身,赋值运算符必须能正确工作。二、大多数赋值运算符组合了析构函数和拷贝构造函数的工作。

13.2.2 定义行为像指针的类

最好是使用shared_ptr共享指针来管理类中资源。

需要直接管理资源时使用引用计数。在共享内存中动态分配空间存放引用计数。

引用计数记录的是与当前对象共享资源的对象的个数。

13.3 交换操作

除了定义拷贝控制成员,管理资源的类通常还定义一个名为swap的函数。

swap函数尽量交换指针而不是交换资源。一般定义为inline形式的友元函数。

std::swap函数应用于内置类型,对于类类型使用std::swap函数可能会增加拷贝。

在赋值运算符中使用swap,参数是非引用类型,直接与本对象进行swap。且自动处理自赋值和天然就是异常安全的。

13.6 对象移动

移动可以将一个类对象中的资源移动到一个新的对象中而不发生拷贝。新标准中容器可以保存不可拷贝但能被移动的类型。

13.6.1 右值引用

右值引用是指必须绑定到右值的引用。&&来表示,且只能绑定到一个将要销毁的对象。

常规引用(左值引用,&)不能绑定到要求转换的表达式、字面常量或者返回右值的表达式。右值引用相反。

左值有持久的状态,右值要么是字面常量或者是表达式求值过程中创建的临时对象。

变量表达式都是左值,因此不能将右值引用绑定到右值引用类型的变量上。

std::move函数将左值当做右值处理,除了对其赋值或者销毁不再使用。

13.6.2 移动构造函数和移动赋值运算符

完成了资源移动之后,需要确保移后源对象进行销毁是无害的。特别是源对象不再指向被移动的资源,这些资源已经归属于新创建的对象。

移动操作窃取资源而不分配任何资源,因此通常不抛出异常。

在构造函数中参数列表和初始化列表添加noexcept: 表示该函数不会抛出异常。不抛出异常的移动函数必须标记noexcept。

一、移动操作通常不抛出异常但是允许抛出。二、标准库容器需要对异常发生时的行为提供保障。需要noexcept告诉编译器使用移动构造函数或者移动赋值运算符是可以安全使用的,否则可能不会被使用。

移动赋值运算符执行与析构函数和移动构造函数相同的操作。需要检测自赋值之后释放左侧对象。

移后源对象需要可析构,一般使其指针为nullptr。但是移动操作必须保证对象依然是有效的。但不能假设移后源对象的值。

只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时编译器才会为其合成。

移动操作不会隐式定义为删除的函数。现实要求编译器=default移动操作,且编译器不能移动所有成员则会将其定义为删除的。

如果类定义了一个移动构造函数和/或一个移动赋值运算符,该类的合成拷贝两函数定义为删除的。

拷贝并赋值运算符可以同时得到拷贝赋值运算符和一个移动赋值运算符。

移动迭代器的解引用生成一个右引用。make_move_iterator函数将一个普通迭代器转换为一个移动迭代器。

标准库不保证哪些算法适用移动迭代器。确信算法在赋值之后不再访问它,才能使用移动迭代器。

13.6.3 右值引用和成员函数

允许使用移动操作的成员函数一般有两个版本,一个接受const 左值引用,另一个接受右值引用。

成员函数可以类似于const形式定义调用的对象必须为左值或者右值。但是必须在const之后。&代表左值,&&代表右值。

如果定义两个或者两个以上具有相同参数列表的成员函数,必须对所有函数加上引用限定符或者都不加。

猜你喜欢

转载自blog.csdn.net/qq_25037903/article/details/82769230