拷贝控制

​ 当定义一个类时,我们显式地或隐式地指定在此类型的对象拷贝、移动、赋值和销毁时做什么。一个类通过定义五种特殊的成员函数来控制这些操作,包括:拷贝构造函数(copy constructor)、拷贝赋值运算符(copy-assignment operator)、移动构造函数(move constructor)、移动赋值运算符(move-assignment operator)和析构函数(destructor)。拷贝和移动构造函数定义了当用同类型的另一个对象初始化本对象时做什么。拷贝和移动赋值运算符定义了将一个对象赋予同类型的另一个对象时做什么。析构函数定义了当此类型对象销毁时做什么。我们称这些操作为拷贝控制操作(copy control)。

​ 如果一个类没有定义所有这些拷贝控制成员,编译器会自动为它定义缺失的操作。因此,很多类会忽略这些拷贝控制操作。但是,对一些类来说,依赖这些操作的默认定义会导致灾难。通常,实现拷贝控制操作最困难的地方是首先认识到什么时候需要定义这些操作。

1. 拷贝、赋值与销毁

拷贝构造函数、拷贝赋值运算符和析构函数

1. 拷贝构造函数

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

image-20221009154927208

拷贝构造函数的第一个参数必须是一个引用类型。虽然我们可以定义一个接受非const引用的拷贝构造函数,但此参数几乎总是一个const的引用。拷贝构造函数在几种情况下都会被隐式地使用。因此,拷贝构造函数通常不应该是explicit的。

1. 合成拷贝构造函数

​ 如果我们没有为一个类定义拷贝构造函数,编译器会为我们定义一个。与合成默认构造函数不同,即使我们定义了其他构造函数,编译器也会为我们合成一个拷贝构造函数。

对某些类来说,**合成拷贝构造函数(synthesized copy constructor)**用来阻止我们拷贝该类类型的对象。

而一般情况,合成的拷贝构造函数会将其参数的成员逐个拷贝到正在创建的对象中。

编译器从给定对象中依次将每个非static成员拷贝到正在创建的对象中。

每个成员的类型决定了它如何拷贝:对类类型的成员,会使用其拷贝构造函数来拷贝;内置类型的成员则直接拷贝。虽然我们不能直接拷贝一个数组,但合成拷贝构造函数会逐元素地拷贝一个数组类型的成员。如果数组元素是类类型,则使用元素的拷贝构造函数来进行拷贝。

作为一个例子,我们的Sales_data类的合成拷贝构造函数等价于:

image-20221009155216464

2. 拷贝初始化

现在,我们可以完全理解直接初始化和拷贝初始化之间的差异了

image-20221009155246767

当使用直接初始化时,我们实际上是要求编译器使用普通的函数匹配(参见6.4节,第209页)来选择与我们提供的参数最匹配的构造函数。当我们使用拷贝初始化(copy initialization)时,我们要求编译器将右侧运算对象拷贝到正在创建的对象中,如果需要的话还要进行类型转换(参见7.5.4节,第263页)。拷贝初始化通常使用拷贝构造函数来完成。但是,如我们将在13.6.2节(第473页)所见,如果一个类有一个移动构造函数,则拷贝初始化有时会使用移动构造函数而非拷贝构造函数来完成。但现在,我们只需了解拷贝初始化何时发生,以及拷贝初始化是依靠拷贝构造函数或移动构造函数来完成的就可以了。拷贝初始化不仅在我们用=定义变量时会发生,在下列情况下也会发生· 将一个对象作为实参传递给一个非引用类型的形参· 从一个返回类型为非引用类型的函数返回一个对象· 用花括号列表初始化一个数组中的元素或一个聚合类中的成员(参见7.5.5节,第266页)

某些类类型还会对它们所分配的对象使用拷贝初始化。例如,当我们初始化标准库容器或是调用其insert或push成员(参见9.3.1节,第306页)时,容器会对其元素进行拷贝初始化。与之相对,用emplace成员创建的元素都进行直接初始化(参见9.3.1节,第308页)

3. 参数和返回值

在函数调用过程中,具有非引用类型的参数要进行拷贝初始化(参见6.2.1节,第188页)。类似的,当一个函数具有非引用的返回类型时,返回值会被用来初始化调用方的结果(参见6.3.2节,第201页)。拷贝构造函数被用来初始化非引用类类型参数,这一特性解释了为什么拷贝构造函数自己的参数必须是引用类型。如果其参数不是引用类型,则调用永远也不会成功——为了调用拷贝构造函数,我们必须拷贝它的实参,但为了拷贝实参,我们又需要调用拷贝构造函数,如此无限循环。

4. 拷贝初始化的限制

如前所述,如果我们使用的初始化值要求通过一个explicit的构造函数来进行类型转换(参见7.5.4节,第265页),那么使用拷贝初始化还是直接初始化就不是无关紧要的了:

image-20221009155427570

直接初始化v1是合法的,但看起来与之等价的拷贝初始化v2则是错误的,因为vector的接受单一大小参数的构造函数是explicit的。出于同样的原因,当传递一个实参或从函数返回一个值时,我们不能隐式使用一个explicit构造函数。如果我们希望使用一个explicit构造函数,就必须显式地使用,像此代码中最后一行那样。

5. 编译器可以绕过拷贝构造函数

在拷贝初始化过程中,编译器可以(但不是必须)跳过拷贝/移动构造函数,直接创建对象。即,编译器被允许将下面的代码