拷贝控制

参考文献

类的特殊函数

  • 初始化——构造函数
  • 拷贝——拷贝构造函数
  • 移动——移动构造函数
  • 赋值——拷贝赋值运算符、移动赋值运算符
  • 销毁——析构函数
1
2
3
4
5
6
7
8
9
默认构造(无参)   T()

拷贝构造       T(const T& )
移动构造       T(T&&)

拷贝赋值       T& operator=(T& )
移动赋值       T& operator=(T&& )

析构         ~T()

1 构造函数

与类同名的,没有返回值的函数,用来创建、拷贝、移动、销毁该类的对象。

1.1 合成构造函数

编译器自动生成的一系列构造函数。包括以下几种

  • 合成默认构造函数
    • 当用户定义了任意类型的构造函数,编译器不再自动生成合成默认构造函数
  • 合成拷贝构造函数
    • 即是用户定义了其他类型的构造函数,编译器还会自动生成合成拷贝构造函数。
    • 编译器自动生成的拷贝构造函数。从给定的对象中依次将每个非static成员拷贝到正在创建的对象当中。
  • 合成析构函数
    • 系统自动生成的析构函数。

1.2 默认构造函数和普通构造函数

  • 默认构造函数是无参构造函数
  • 普通构造函数是一系列有参数的构造函数。

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

唯一参数是当前类类型,或者当前类型的const引用。

示例

1
2
3
4
class Foo{
Foo();
Foo(const Foo&)//拷贝构造函数
}

赋值初始化(拷贝构造函数)

赋值初始化的时候会自动调用拷贝构造函数。

1
string nies = string("efji");
  • 当我门使用 赋值= 运算符时,发生赋值初始化,执行拷贝构造函数。
  • 将一个对象作为实参传递给一个非引用类型的形参
  • 从一个返回类型为费引用类型的函数返回一个对象
  • 用花括号列表初始化一个数组中的元素或一个聚合类中的成员

赋值运算符(拷贝赋值运算符)

普通赋值的时候,会调用重载的赋值运算符。

  • 编译器会自动生成合成拷贝赋值运算符
  • 需要重载赋值运算符。

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

  • 在面向对象中,有的类是可以拷贝的,例如车、房等他们的属性是可以复制的,可以调用拷贝构造函数,有点类的对象则是独一无二的,或者类的资源是独一无二的,比如 IO 、 std::unique_ptr等,他们不可以复制,但是可以把资源交出所有权给新的对象,称为可以移动的。
  • C++11最重要的一个改进之一就是引入了move语义,这样在一些对象的构造时可以获取到已有的资源(如内存)而不需要通过拷贝,申请新的内存,这样移动而非拷贝将会大幅度提升性能。例如有些右值即将消亡析构,这个时候我们用移动构造函数可以接管他们的资源。
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
61
62
63
#include <iostream>
#include <cstring>

using namespace std;

class A{
public:
//默认构造函数
A():i(new int[500]){
cout<<"class A construct!"<<endl;
}
//拷贝构造函数
A(const A &a):i(new int[500]){
memcpy(i, a.i,500*sizeof(int));
cout<<"class A copy!"<<endl;
}
//拷贝赋值运算符
A &operator =(A &rhs) noexcept{
// check self assignment
if(this != &rhs){
delete []i;
i = rhs.i;
}
cout<< "class A copy and assignment"<<std::endl;
return *this;
}
//移动构造函数
A(A &&a)noexcept:i(a.i)
{
a.i = nullptr;
cout<< "class A move"<<endl;
}
//移动赋值运算符
A &operator =(A &&rhs) noexcept{
// check self assignment
if(this != &rhs){
delete []i;
i = rhs.i;
rhs.i = nullptr;
}
cout<< "class A move and assignment"<<std::endl;
return *this;
}
//析构函数
~A(){
delete []i;
cout<<"class A destruct!"<<endl;
}

private:
int *i;
};

A get_A_value(){
return A();
}
void pass_A_by_value(A a){

}
int main(){
A a = get_A_value();
return 0;
}
  • 在移动构造函数中,我们做了什么呢,我们只是获取了被移动对象的资源(这里是内存)的所有权,同时把被移动对象的成员指针置为空(以避免移动过来的内存被析构),这个过程中没有新内存的申请和分配,在大量对象的系统中,移动构造相对与拷贝构造可以显著提高性能!这里noexcept告诉编译器这里不会抛出异常,从而让编译器省一些操作(这个也是保证了STL容器在重新分配内存的时候(知道是noexpect)而使用移动构造而不是拷贝构造函数),通常移动构造都不会抛出异常的。

注意事项:

  • 偷梁换柱直接“浅拷贝”右值引用的对象的成员;
  • 需要把原先右值引用的指针成员置为 nullptr,以避免右值在析构的时候把我们浅拷贝的资源给释放了;
  • 移动构造函数需要先检查一下是否是自赋值,然后才能先delet自己的成员内存再浅拷贝右值的成员,始终记住第2条。

1.5 委托构造函数

  • 使用已有的构造函数初始化。

2 析构函数

定义析构函数

  • 析构函数与构造函数对应,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统会自动执行析构函数。
  • 类的成员函数,由拨浪号接类名构成,没有返回值,不接受参数。不能被重载,一个类只有一个析构函数。
1
2
3
4
class Foo{
public:
~Foo();
}

原理

  • 在一个析构函数中,首先执行函数体,然后销毁成员。成员按初始化顺序逆序销毁。
  • 智能指针成员在西沟阶段会自动销毁。

何时调用

  • 变量离开作用域被销毁
  • 一个对象被销毁
  • 容器被销毁
  • 动态对象,使用delete

对象析构顺序

  1. 派生类本身的析构函数;
  2. 对象成员析构函数;
  3. 基类析构函数。

3 虚函数与构造函数和析构函数

构造函数不必是虚函数

  1. 对象通过虚函数指针访问虚函数。在执行构造函数之前,虚函数指针没有创建,所以即使声明为虚函数,也不会有多态,所以不必要是虚函数。

析构函数必须是虚函数

  1. 删除动态运行时的具体对象。
  2. 普通对象如果不被继承,析构函数可以不使用虚函数。避免生成虚函数表和虚函数指针,浪费内存空间。

4 三五法则

在较新的 C++11 标准中,为了支持移动语义,又增加了移动构造函数和移动赋值运算符,这样共有五个特殊的成员函数,所以又称为“C++五法则”;也就是说,“三法则”是针对较旧的 C++89 标准说的,“五法则”是针对较新的 C++11 标准说的;为了统一称呼,后来人们干把它叫做“C++ 三/五法则”;

  1. 需要析构函数的类也需要拷贝构造函数和拷贝赋值函数。
  2. 需要拷贝操作的类也需要赋值操作,反之亦然。
  3. 析构函数不能是删除的
  4. 如果一个类有删除的或不可访问的析构函数,那么其默认和拷贝构造函数会被定义为删除的。
  5. 如果一个类有const或引用成员,则不能使用合成的拷贝赋值操作。

5 概念区分:声明declare、定义define、初始化initialize、赋值assign

声明declare

  • 声明一个符号。如果有extern,则表示变量在外部顶底,链接其他问件事,匹配外部定义的变量。

定义define

  • 分配内存、指定变量名。

初始化initialize

  • 对象创建时获得的初始值。初始化的含义是,在创建变量的时候赋予其一个初始值。

赋值assign

  • 赋值的含义是,将当前的值擦除,而以一个新的值来代替。
  • 不能是简单的覆盖,将当前值擦除,需要调用当前对象的析构函数,对指针变量进行析构,如果只是简单的覆盖,肯定不行。