拷贝赋值与销毁
类中的五中特殊成员函数: 拷贝构造函数、 拷贝赋值运算符、移动构造函数、移动赋值运算符、析构函数。
称这些操作为 拷贝控制操作。
拷贝构造函数
一个构造函数,第一个参数是自身类类型的引用,其他参数都有默认值,则它是一个拷贝构造函数。
- 第一个参数必须是引用。否则会循环拷贝。
- 拷贝初始化时可能会有类型转换。
- 发生情景: 使用” = “定义变量; 参数传递时(传递给非引用类型的形参); 函数返回一个非引用类型的对象;花括号列表初始化数组或者聚合类。
- 接受大小参数的构造函数是explicit的。
拷贝赋值运算符
- 重载运算符是一个函数,由operator关键字后接表示要定义的运算符的符号组成。 参数表示运算符的运算对象。返回值是左侧运算对象的引用。
- 合成拷贝赋值运算符: 一般 会将右侧运算对象的每个非static的成员赋予左侧运算对象的对应成员。
- 注意两点: 1、自拷贝时的正确操作。 2、返回类类型的引用。
return *this;
析构函数
- 析构函数释放对象使用的资源,并销毁对象的非static数据成员。它没有返回值也不接受参数,不能被重载;析构函数先执行函数体,然后销毁成员。顺序与构造顺序相反。
- 析构是隐式的,销毁类类型的成员要执行成员自己的析构函数。 隐式销毁一个内置指针类不会detele它所指向的对象。
三/五 法则
- 如果一个类需要自定义析构函数(要手动detele动态内存), 它一定也需要自定义拷贝赋值运算符和拷贝构造函数(避免浅拷贝)。 (充分条件)
- 如果一个类需要拷贝构造函数,也一定需要拷贝赋值运算符。 (充分必要条件)
default
- 只能对具有合成版本的成员函数使用(默认构造函数或拷贝控制成员)
- 在类内使用=default,合成的函数隐式声明为内联的, 在类外使用则不会。
阻止拷贝
- 在函数的参数列表后加一个 =delete (C++11),可以将函数定义为删除的。 不能以任何方式使用它。 =detele必须出现在函数第一次声明的时候。可以对任何函数指定delete ;不能删除析构函数;
- 自动合成的拷贝控制成员可能是删除的
- 如果某个成员的析构函数 / 拷贝构造函数 / 拷贝赋值运算符是删除的或者是不可访问的(private),则对应的拷贝控制函数是删除的
- ……..很多情况
- 如果一个类有数据成员不能默认构造、拷贝、复制、销毁, 则对应的成员函数会被定义成删除的。
- 避免创造出无法销毁的对象; 避免拷贝构造函数给一个const成员赋值; 避免给一个引用对象赋值。
- C++11之前,对于要声明删除的成员函数,通常声明且不定义 成private。
拷贝控制和资源管理
类的拷贝行为大致分为 值 和 指针。 如何拷贝指针成员决定了这个类是具有类值行为还是类指针行为。
IO类型和unique_ptr不允许拷贝或赋值,因此它们的行为既不像值也不像指针。
- 类值的行为
- 类中的每个成员都应有一份自己的拷贝。 指针应采用深拷贝。
- 应注意自拷贝会出现异常。
- 定义拷贝赋值运算符时,要注意执行顺序,要先把右侧运算对象拷贝,再销毁左侧运算对象原始占用的资源。返回类型是原类类型的引用,最后要加return *this;
- 编写赋值运算符时要注意: 1. 将一个对象赋予自身,必须能正确工作。 2. 大多数赋值运算符组合了析构函数和拷贝构造函数的工作。
- 先将右侧运算对象拷贝到局部临时对象,然后销毁左侧对象,然后将临时对象拷贝给左侧对象。
- 类指针的行为
- 指针采用浅拷贝; 指针所指的内存是共享的,析构函数不能单方面释放内存。 一般使用share_ptr管理类中的资源; 或者自己定义引用计数。
- 不需要考虑自拷贝出现异常,只要正确加减引用计数即可。
- 引用计数
- 构造函数创建; 拷贝构造函数递增;拷贝赋值运算符递增右侧,递减左侧;析构函数递减。
- 一般存放在动态内存,可以实时更新。
交换操作swap
- 定义自己的swap不是必要的,但可以提高效率(标准库的swap会进行很多不必要的拷贝,而大多数情况,只需要交换指针即可。)。 一般声明为inline。
- 如果在一个类中定义了自己的swap函数,则会优先调用这个。 在函数中使用using std::swap, 则会调用标准库中的swap。
- 拷贝并交换 的技术,自动处理了自赋值情况,且天生就是异常安全的
对象移动
右值引用
通过&&来获得右值引用,它只能绑定到一个将要销毁的对象。
左值引用就是普通的引用,不能绑定到要求转换的表达式、字面常量,或是返回右值的表达式。
任何变量都是左值。
通过调用std::move (头文件utility)可以将一个右值引用绑定在一个左值上
移动构造函数、移动赋值运算符
- 移动构造函数的第一个参数是类类型的非const的右值引用,其他参数都必须有默认值。它接管给定的类对象的内存,并且将原对象中的指针都设为nullptr。
- 在参数列表之后初始化列表之前写noexcept,通知标准库不抛出异常。
- 移动赋值运算符:参数与移动构造函数一致。返回类型为类类型的引用;必须处理自赋值(显式处理
if(this!=&rhs)
) - 移动完成后,要将原指针都置为nullptr,否则析构函数会将移动后的内存释放。
- 移动,拷贝构造函数的选取规则: 移动右值,拷贝左值;如果没有移动构造函数,右值也被拷贝。
- std::make_move_iterator:将普通迭代器转化为移动迭代器,解引用得到右值引用;与uninitialized_copy结合可调用移动构造替代拷贝构造
- 引用限定:用来指出一个非stastic成员函数可用于左值(&)或右值(&&)对象;放在参数列表或const之后可区分重载版本;同名同参数列表的函数必须都加引用限定符或都不加
注意事项
移动构造函数和移动赋值运算符都不需要重新分配地址 。
一般要声明noexcept 。 成员函数的声明和定义都要写。
移动赋值运算符要点: 1. 返回类型是类类型的引用; 2. 参数是非常量右值引用; 3. 首先显式判断是不是自赋值的情况
if(this!=&rhs)
4. 操作完成后要把传入的参数置于可析构状态(指针为nullptr)合成的拷贝控制成员
- 当一个类定义了自己的拷贝构造函数,拷贝赋值运算符或者析构函数,则编译器不会为他合成移动赋值运算符。
- 当一个类没有定义任何自己版本的拷贝控制成员,且类中每个非static数据成员都可以移动时,才会合成移动构造函数和移动赋值运算符。