第十三章. 拷贝控制

拷贝赋值与销毁

类中的五中特殊成员函数: 拷贝构造函数、 拷贝赋值运算符、移动构造函数、移动赋值运算符、析构函数。

称这些操作为 拷贝控制操作。

拷贝构造函数

  • 一个构造函数,第一个参数是自身类类型的引用,其他参数都有默认值,则它是一个拷贝构造函数。

    • 第一个参数必须是引用。否则会循环拷贝。
    • 拷贝初始化时可能会有类型转换。
    • 发生情景: 使用” = “定义变量; 参数传递时(传递给非引用类型的形参); 函数返回一个非引用类型的对象;花括号列表初始化数组或者聚合类。
    • 接受大小参数的构造函数是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之后可区分重载版本;同名同参数列表的函数必须都加引用限定符或都不加

注意事项

  1. 移动构造函数和移动赋值运算符都不需要重新分配地址

  2. 一般要声明noexcept 。 成员函数的声明和定义都要写。

  3. 移动赋值运算符要点: 1. 返回类型是类类型的引用; 2. 参数是非常量右值引用; 3. 首先显式判断是不是自赋值的情况 if(this!=&rhs) 4. 操作完成后要把传入的参数置于可析构状态(指针为nullptr)

  4. 合成的拷贝控制成员

    1. 当一个类定义了自己的拷贝构造函数,拷贝赋值运算符或者析构函数,则编译器不会为他合成移动赋值运算符。
    2. 当一个类没有定义任何自己版本的拷贝控制成员,且类中每个非static数据成员都可以移动时,才会合成移动构造函数和移动赋值运算符。

发布了29 篇原创文章 · 获赞 3 · 访问量 7137

猜你喜欢

转载自blog.csdn.net/liu432linux/article/details/79337698