cppprimer第十三章

第十三章 拷贝控制

拷贝控制操作(copy control):

  • 拷贝构造函数(copy constructor)
  • 拷贝赋值运算符(copy-assignment operator)
  • 移动构造函数(move constructor)
  • 移动赋值函数(move-assignement operator)
  • 析构函数(destructor)

拷贝、赋值和销毁

拷贝构造函数

  • 如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数
  • 引用的原因:传值的方式会调用该类的拷贝构造函数,从而造成无穷递归地调用拷贝构造函数。
  • class Foo{ public: Foo(const Foo&); }
  • 合成的拷贝构造函数(synthesized copy constructor):会将参数的成员逐个拷贝到正在创建的对象中。
  • 拷贝初始化
    • 将右侧运算对象拷贝到正在创建的对象中,如果需要,还需进行类型转换。
    • 通常使用拷贝构造函数完成。
    • string book = "9-99";
    • 出现场景:
      • = 定义变量时。
      • 将一个对象作为实参传递给一个非引用类型的形参。
      • 从一个返回类型为非引用类型的函数返回一个对象。
      • 用花括号列表初始化一个数组中的元素或者一个聚合类中的成员。花括号内元素个数是拷贝构造次数。

拷贝赋值运算符

  • 重载赋值运算符
    • 重写一个名为operator=的函数.
    • 通常返回一个指向其左侧运算对象的引用。
    • Foo& operator=(const Foo&);
  • 合成拷贝赋值运算符
    • 一个类未定义自己的拷贝赋值运算符,默认生成。
    • 将右侧运算对象的每个非static成员赋予左侧运算对象的对应成员。

析构函数

  • 释放对象所使用的资源,并销毁对象的非static数据成员。
  • 名字由波浪号接类名构成。没有返回值,也不接受参数。
  • ~Foo();
  • 调用时机:
    • 变量在离开其作用域时。
    • 当一个对象被销毁时,其成员被销毁。
    • 容器被销毁时,其元素被销毁。
    • 动态分配的对象,当对指向它的指针应用delete运算符时。
    • 对于临时对象,当创建它的完整表达式结束时。
  • 合成析构函数
    • 空函数体执行完后,成员会被自动销毁。
    • 注意:析构函数体本身并不直接销毁成员。
    • 合成析构函数并不删除指针成员所指向的对象,它需要程序员显式编写析构函数去处理。

三/五法则

  • 需要析构函数的类也需要拷贝和赋值操作。

    • 一个对象拥有额外的资源(指针指向的内存),但另一个对象使用默认的拷贝构造函数也同时拥有这块资源。当一方对象被销毁后,析构函数释放了资源,这时另一个对象便失去了这块资源。
  • 需要拷贝操作的类也需要赋值操作,反之亦然。

  • 13.1.4练习

    • 合成拷贝构造函数被调用时简单复制序号,使得三个对象具有相同的序号。1 1 1

    • 定义变量和函数传参时,两次调用拷贝构造函数。4 5 6

    • 引用,不拷贝。1 2 3

    • ```cpp
      #include
      int unique;

      class numbered {
      public:
      numbered() {mysn = ++unique;}
      // numbered(numbered &d): mysn(++unique) {}

        int mysn;

      };

      void f(numbered s) {
      std::cout << s.mysn << std::endl;
      }

      int main() {
      numbered a, b = a, c = b;
      f(a);
      f(b);
      f(c);
      }

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60

      ### 使用=default

      - 可以通过将拷贝控制成员定义为`=default`来显式地要求编译器生成合成的版本。
      - 合成的函数将隐式地声明为内联的。

      ### 阻止拷贝

      - 大多数类应该定义默认构造函数、拷贝构造函数和拷贝赋值运算符,无论是隐式地还是显式地。
      - 定义删除的函数:`=delete`。
      - 虽然声明了它们,但是不能以任何方式使用它们。
      - 析构函数不能是删除的成员。
      - 如果一个类有数据成员不能默认构造、拷贝、复制或者销毁,则对应的成员函数将被定义为删除的。
      - 老版本使用`private`声明来阻止拷贝。

      ## 拷贝控制和资源管理

      - 类的行为可以像一个值,也可以像一个指针。
      - 行为像值:对象有自己的状态,副本和原对象是完全独立的。
      - 编写赋值运算符:如果将一个对象赋予它自身,赋值运算符必须能正确工作。
      - 行为像指针:共享状态,拷贝一个这种类的对象时,副本和原对象使用相同的底层数据。

      ## 交换操作

      - 管理资源的类通常还定义一个名为`swap`的函数。
      - 经常用于重排元素顺序的算法。
      - 用`swap`而不是`std::swap`。

      ## 对象移动

      - 很多拷贝操作后,原对象会被销毁,因此引入移动操作可以大幅度提升性能。
      - 在新标准中,我们可以用容器保存不可拷贝的类型,只要它们可以被移动即可。
      - 标准库容器、`string`和`shared_ptr`类既可以支持移动也支持拷贝。`IO`类和`unique_ptr`类可以移动但不能拷贝。

      ### 右值引用

      - 新标准引入右值引用以支持移动操作。
      - 通过`&&`获得右值引用。
      - 只能绑定到一个将要销毁的对象,右值引用变量不能直接绑定到右值引用上。
      - 常规引用可以称之为左值引用。
      - 左值持久,右值短暂。

      **move函数**:

      - `int &&rr2 = std::move(rr1);`
      - `qmove`告诉编译器,我们有一个左值,但我希望像右值一样处理它。
      - 调用`move`意味着:除了对`rr1`赋值或者销毁它外,我们将不再使用它。

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

      - **移动构造函数**:

      - 第一个参数是该类类型的一个引用,关键是,这个引用参数是一个**右值引用**。

      - ```cpp
      StrVec::StrVec(StrVec &&s) noexcept : elements(s.elements), first_free(s.first_free), cap(s.cap) {
      // 置于可析构状态
      s.elements = s.first_free = s.cap = nullptr;
      // 然后析构
      }

    • 不分配任何新内存,只是接管给定的内存。

  • 移动赋值运算符

    • StrVec& StrVec::operator=(StrVec && rhs) noexcept{}
    • 检测自赋值的情况
  • 移动右值(移动构造函数),拷贝左值(拷贝构造函数)。

  • 如果没有移动构造函数,右值也被拷贝。

  • 拷贝并交换赋值运算符:根据实参类型,既可拷贝又可移动。

  • 更新三/五法则:如果一个类定义了任何一个拷贝操作,它就应该定义所有五个操作。

  • 移动迭代器:

    • make_move_iterator函数将一个普通迭代器转换为一个移动迭代器。
  • 建议:小心地使用移动操作,以获得性能提升。

右值引用和成员函数

  • 区分移动和拷贝的重载函数通常有一个版本接受一个const T&,而另一个版本接受一个T&&
  • 引用限定符:
    • 在参数列表后面加一个&,限定只能向可修改的左值赋值而不能向右值赋值。加&&同理。