Skip to content

Latest commit

 

History

History
210 lines (157 loc) · 8.82 KB

File metadata and controls

210 lines (157 loc) · 8.82 KB

13.1 拷贝、赋值与销毁

本章术语:

  • 拷贝构造函数(copy constructor):定义了当用同类型的另一个对象初始化本对象时做什么
  • 拷贝赋值运算符(copy-assignment operator):定义了将一个对象赋予同类型的另一个对象时做什么
  • 移动构造函数(move constructor):定义了当用同类型的另一个对象初始化本对象时做什么
  • 移动赋值运算符(move-assignment operator):定义了将一个对象赋予同类型的另一个对象时做什么
  • 析构函数(destructor):定义了当此类型对象销毁时做什么
  • 拷贝控制操作(copy control):上述 5 种操作的统称
  • 合成拷贝构造函数(synthesized copy constructor):编译器自动生成的拷贝构造函数
  • 合成拷贝赋值运算符(synthesized copy-assignment operator):编译器自动生成的拷贝赋值运算符
  • 合成析构函数(synthesized destructor):编译器自动生成的析构函数
  • 删除的函数(deleted function):在声明后面加上 =delete 表示该函数不允许调用

如果程序员不显式定义拷贝控制操作,编译器会自动定义这些操作。

13.1.1 拷贝构造函数

拷贝构造函数的形式:

  1. 是一个构造函数
  2. 第一个参数是自身类类型的引用
  3. 任何额外的参数都有默认值
class Foo {
public:
    Foo(); // 默认构造函数
    Foo(const Foo &); // 拷贝构造函数(第一个参数总是 const 的引用,通常不应该是 explicit 的)
};

合成拷贝构造函数会将其参数的每个非 static 成员逐个拷贝到正在创建的对象中。
在某些情况下,合成拷贝构造函数用来阻止拷贝该类类型的对象。

直接初始化和拷贝初始化的区别:

  • 直接初始化:要求编译器使用普通的函数匹配来选择参数最匹配的构造函数
  • 拷贝初始化:要求编译器使用拷贝构造函数,将右侧运算对象拷贝到正在创建的对象中

拷贝初始化发生的情况:

  • 使用 = 运算符定义变量
  • 将一个对象作为实参传递给一个非引用类型的形参
  • 从一个返回类型为非引用类型的函数返回一个对象
  • 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
  • 初始化标准库容器或调用 insert 或 push 成员(与之相对,用 emplace 成员创建的元素都进行直接初始化)

explicit 的构造函数必须显式调用。

vector<int> v1(10);  // 直接初始化:显式调用接受 vector 大小的 explicit 构造函数
vector<int> v2 = 10; // 错误:explicit 的构造函数不能隐式调用

void f(vector<int>); // 函数 f 的声明
f(vector<int>(10));  // 正确:显式调用 explicit 构造函数创建对象并拷贝给形参
f(10);               // 错误:不能隐式调用 explicit 构造函数创建对象

编译器可以将拷贝初始化优化为直接初始化,但是拷贝构造函数必须是存在且可以访问的(即:不能没有拷贝构造函数)

// 代码中所展示的
string null_book = "9-999-99999-9"; // 理应执行拷贝构造函数

// 编译器实际调用的
string null_book("9-999-99999-9"); // 编译器略过了拷贝构造函数,执行直接初始化

13.1.2 拷贝赋值运算符

运算符左侧的运算对象绑定到隐式的 this 参数,运算符右侧的运算对象作为显式参数传递。

拷贝赋值运算符接受一个与其所在类相同类型的参数:

class Foo {
public:
    Foo &operator=(const Foo &);
};

赋值运算符通常应该返回一个指向其左侧运算对象的引用。

合成拷贝赋值运算符会将右侧运算对象的每个非 static 成员赋予左侧运算对象的对应成员。
在某些情况下,合成拷贝赋值运算符用来禁止该类型对象的赋值。

13.1.3 析构函数

  • 构造函数:初始化对象的非 static 数据成员,以及做一些其他工作
  • 析构函数:释放对象使用的资源,销毁对象的非 static 数据成员

析构函数的形式:是类的一个成员函数,名字由波浪号接类名构成,没有返回值,也不接受参数:

class Foo {
public:
    ~Foo(); // 析构函数
};
  • 构造函数首先执行初始化部分,然后执行函数体(按照成员在类中出现的顺序进行初始化)
  • 析构函数首先执行函数体,然后销毁成员(按照成员初始化顺序的逆序进行销毁)

隐式销毁一个内置指针类型的成员不会 delete 它所指向的对象。

析构函数被调用的情况:

  • 变量在离开其作用域时被销毁
  • 当一个对象被销毁时,其成员被销毁
  • 容器被销毁时,其元素被销毁
  • 对于动态分配的对象,当对指向它的指针使用 delete 运算符时被销毁
  • 对于临时对象,当创建它的完整表达式结束时被销毁

当指向一个对象的引用或指针离开作用域时,析构函数不会执行。

合成析构函数可能会被用来阻止该类型的对象被销毁。如果不是这种情况,合成析构函数的函数体为空。

13.1.4 三/五法则

如果一个类需要自定义析构函数,那么几乎可以肯定它也需要自定义拷贝赋值运算符和拷贝构造函数。

如果一个类需要一个拷贝构造函数,那么几乎可以肯定它也需要一个拷贝赋值运算符。
反之亦然
如果一个类需要一个拷贝赋值运算符,那么几乎可以肯定它也需要一个拷贝构造函数。

无论是需要拷贝构造函数,还是需要拷贝赋值运算符,都不必然意味着也需要析构函数。

13.1.5 使用=default

使用 =default 显式要求编译器生成合成的拷贝控制成员:

class Sales_data {
public:
    Sales_data() = default;
    Sales_data(const Sales_data &) = default;
    Sales_data &operator=(const Sales_data &);
    ~Sales_data() = default;
};
Sales_data &Sales_data::operator=(const Sales_data &) = default;

在类内用 =default 修饰成员的声明时,合成的函数被隐式地声明为内联函数。
在类外用 =default 时,合成的函数则不是内联函数。

13.1.6 阻止拷贝

大多数类应该定义默认构造函数、拷贝构造函数和拷贝赋值运算符,无论是隐式地还是显式地。

定义删除的函数以阻止拷贝(在函数声明后添加 =delete ):

struct NoCopy {
    NoCopy() = default;
    NoCopy(const NoCopy &) = delete; // 阻止拷贝
    NoCopy &operator=(const NoCopy &) = delete; // 阻止赋值
    ~NoCopy() = default;
};

不能将析构函数声明为删除的函数。

合成的拷贝控制成员有可能是删除的,具体的情形有:

  • 合成的默认构造函数是删除的函数:
    • 类的某个成员的析构函数是删除的或不可访问的(即 private 的)
    • 类有一个引用成员,它没有类内初始化器
    • 类有一个 const 成员,它没有类内初始化器并且其类型未显式定义默认构造函数
  • 合成的析构函数是删除的函数:
    • 类的某个成员的析构函数是删除的或不可访问的
  • 合成的拷贝构造函数是删除的函数:
    • 类的某个成员的拷贝构造函数是删除的或不可访问的
    • 类的某个成员的析构函数是删除的或不可访问的
  • 合成的拷贝赋值运算符是删除的函数:
    • 类的某个成员的拷贝赋值运算符是删除的或不可访问的
    • 类有一个 const 成员
    • 类有一个引用成员

总之:当不可能拷贝、赋值或销毁类的成员时,类的合成拷贝控制成员就被定义为删除的。

将拷贝控制成员声明为 private 的,并且不定义他们,这种做法的效果等价于删除的函数。

但是:
希望阻止拷贝的类应该使用 =delete 来定义拷贝构造函数和拷贝赋值运算符,而不是将它们声明为 private 的。

练习