拷贝控制操作:

  • 拷贝构造函数
  • 拷贝赋值运算符
  • 移动构造函数
  • 移动赋值运算符
  • 析构函数

拷贝和移动构造函数定义了当同类型的另一个对象初始化本对象时做什么;拷贝和移动赋值运算符定义了将一个对象赋予同类型的另一个对象时做什么。

拷贝、赋值与销毁

拷贝构造函数

如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数

  1. class Foo {
  2. public:
  3. Foo(); // 默认构造函数
  4. Foo(const Foo&); // 拷贝构造函数
  5. };

拷贝构造函数在几种情况下都会被隐式地使用,因此拷贝构造函数通常不应该是 explicit 的。

下列情况下会发生拷贝初始化

  • 使用 = 定义变量
  • 将一个对象作为实参传递给一个非引用类型的形参
  • 从一个返回类型为非引用类型的函数返回一个对象
  • 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
    1. string dots(10, '.'); // 直接初始化
    2. string s(dots); // 直接初始化
    3. string s2 = dots; // 拷贝初始化
    4. string null_book = "9-999-99999-9"; // 拷贝初始化
    5. string nines = string(100, '9'); // 拷贝初始化
    拷贝构造函数被用来初始化非引用类类型参数,这一特性解释了为什么拷贝构造函数自己的参数必须是引用类型,如果其参数不是引用类型,会出现调用的无限循环。

三/五法则:拷贝构造函数、拷贝赋值运算符和析构函数;移动构造函数、移动赋值运算符;通常定义三个或五个,而不是定义其中的一部分。

对象移动

使用移动而不是拷贝的原因:

  • 若对象拷贝后会被立即销毁,移动相对拷贝会大幅提升性能
  • IO 类和 unique_ptr 类可以移动但不能拷贝

    右值引用

    一般而言,一个左值表达式表示的是一个对象的身份,而一个右值表达式表示的是对象的值。右值引用有一个重要的性质 — 只能绑定到一个将要销毁的对象。
    1. int i = 42;
    2. int &r = i; // 正确,r 引用 i
    3. int &&rr = i; // 错误,不能将一个右值引用绑定到左值上
    4. int &r2 = i * 42; // 错误,i*42 是一个右值
    5. const int &r3 = i * 42; // 正确,可以将一个 const 的引用绑定到右值上
    6. int &&rr2 = i * 42; // 正确,将 rr2 绑定到乘法结果上
    虽然不能将一个右值引用直接绑定到一个左值上,但可以通过调用一个名为 move 的标准库函数来获得绑定到左值上的右值引用。
    1. int &&rr3 = std::move(rr1); // ok
    可以销毁一个移后源对象,也可以赋予它新值,但不能使用一个移后源对象的值。

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

    1. StrVec::StrVec(StrVec &&s) noexcept // 移动操作不应抛出任何异常
    2. // 成员初始化接管 s 中的资源
    3. :elements(s.elements), first_free(s.first_free), cap(s.cap)
    4. {
    5. // 令 s 进入这样的状态 -- 对其运行析构函数是安全的
    6. s.elements = s.first_free = s.cap = nullptr;
    7. }