什么是 Big Five ?
Big Five 是指 C++ 中一个类的五个核心函数,在 C++ 11 之前还是 Big Three, C++ 11 引入了移动语义,三大核心函数变为了五大核心函数。
这五个核心函数分别是:

  1. 构造函数
  2. 拷贝构造函数
  3. 拷贝赋值函数
  4. 移动构造函数
  5. 移动赋值函数

What is the Big Five?
Big Five refers to the five core functions of a class in C++. Before C++ 11, it was the Big Three; C++ 11 introduced move semantics, turning the three core functions into five.
The five core functions are:

  1. Constructor
  2. Copy constructor
  3. Copy assignment operator
  4. Move constructor
  5. Move assignment operator

浅拷贝与深拷贝

这是一个只写了默认构造函数的 Buffer 类,这个类包括三个私有成员,分别是用于存储数据的字符指针,容量以及长度。

在主函数中,首先创建一个 buffer 对象,然后通过等号的方式创建第二个对象 buffer2。

此时调用的是编译器提供的拷贝构造函数,这个默认的拷贝构造函数的行为就是在内存中进行按位的拷贝(浅拷贝),也就是说 buffer2 中的 buf 与 buffer 中的 buf 指向的是相同的内存空间。为了验证这一情况,在主函数中向buffer中写入了一个字符42,再打印输出两个buffer中的内容,可以看到输出的结构完全相同。

Shallow Copy and Deep Copy

This is a Buffer class that only implements the default constructor. The class has three private members: a character pointer used to store data, the capacity, and the length.

In the main function, we first create a buffer object, then create a second object buffer2 with the assignment operator.

At this point, the compiler-provided copy constructor is called. The default copy constructor performs a bitwise copy in memory (shallow copy), meaning that buf in buffer2 and buf in buffer point to the same memory space. To verify this, we write the character 42 into buffer in the main function, then print the contents of both buffers — the output shows they are exactly the same.

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
#include <iostream>
#include <bits/stdc++.h>
using namespace std;

class Buffer {
public:
explicit Buffer(int capacity)
: capacity(capacity), length(0), buf(new unsigned char[capacity]{0}) {}
int get_length() { return length; }
int get_capacity() { return capacity; }

bool write(unsigned char value)
{
if(length == capacity) return false;
buf[length++] = value;
return true;
}

~Buffer() { delete[] buf; }

friend ostream &operator<<(ostream &os, Buffer &buffer);

private:
unsigned char *buf;
int capacity;
int length;
};

ostream &operator<<(ostream &os, Buffer &buffer) {
os << "Buffer(" << buffer.length << "/" << buffer.capacity << ")[";
for (size_t i = 0; i < buffer.capacity; i++) {
os << (int)buffer.buf[i] << ",";
}
os << "]";
return os;
}

int main() {
auto buffer = Buffer(10);
auto buffer2 = buffer;
buffer.write(42);
cout << buffer << endl;
cout << buffer2 << endl;
}

函数的执行结果如下:

The result of running the program is as follows:

1
2
3
4
Buffer(1/10)[42,0,0,0,0,0,0,0,0,0,]
Buffer(0/10)[42,0,0,0,0,0,0,0,0,0,]
free(): double free detected in tcache 2
[1] 10536 IOT instruction (core dumped) ./build/src/main

在输出的结果中,除了可以发现两个Buffer对象的内容被同步修改之外,还可以发现另外两个点:

  1. buffer2 中虽然有内容,但它的长度还是显示的 0
  2. 程序并没有正确退出,而是产生了 core dumped

产生这两个现象的原因也很简单,由于buffer2并没有调用write函数,因此buffer2的length成员变量是没有被修改的。由于两个对象的buf指针指向的是同一块内存空间,因此在两个对象离开作用域被析构的过程中发生了内存的重复释放问题。这也就是浅拷贝的问题所在。

如果我们希望对象在复制的过程中,能够重新开辟一块内存空间,并使新的内存空间的内容与原来对象的内容保持一致,这种拷贝行为就叫做深拷贝,可以通过自己实现拷贝构造函数的方式来实现深拷贝行为。

In the output, besides noticing that the contents of the two Buffer objects were modified in sync, we can also spot two other things:

  1. buffer2 has content, but its length is still shown as 0
  2. The program did not exit normally; instead, it produced a core dump

The reasons for these two phenomena are also simple: since buffer2 never called the write function, its length member was never modified. Since the buf pointers of the two objects point to the same memory space, a double free problem occurs when the two objects leave scope and are destructed. This is where the problem with shallow copy lies.

If we want the copy process to allocate a brand-new memory space and keep its content consistent with the original object, this copy behavior is called a deep copy, which can be implemented by writing our own copy constructor.

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
64
65
66
67
68
69
70
#include <iostream>
#include <bits/stdc++.h>
using namespace std;

class Buffer {
public:
explicit Buffer(int capacity)
: capacity(capacity), length(0), buf(new unsigned char[capacity]{0}) {}

// 拷贝构造函数
Buffer(const Buffer& buffer)
{
this->capacity = buffer.capacity;
this->length = buffer.length;
this->buf = new unsigned char[buffer.capacity];
std::copy(buffer.buf,buffer.buf + buffer.capacity,this->buf);
}
// 拷贝赋值函数
Buffer& operator=(const Buffer& buffer)
{
// 判断传进来的对象是不是自身,如果是自身不需要操作,直接返回即可。
if(this != &buffer)
{
this->capacity = buffer.capacity;
this->length = buffer.length;
// 先释放掉自身的空间,再开辟新的和被拷贝对象大小相同的空间。
delete[] this->buf;
this->buf = new unsigned char[buffer.capacity];
std::copy(buffer.buf,buffer.buf + buffer.capacity,this->buf);
}
return *this;
}

int get_length() { return length; }
int get_capacity() { return capacity; }

bool write(unsigned char value)
{
if(length == capacity) return false;
buf[length++] = value;
return true;
}

~Buffer() { delete[] buf; }

friend ostream &operator<<(ostream &os, Buffer &buffer);

private:
unsigned char *buf;
int capacity;
int length;
};

ostream &operator<<(ostream &os, Buffer &buffer) {
os << "Buffer(" << buffer.length << "/" << buffer.capacity << ")[";
for (size_t i = 0; i < buffer.capacity; i++) {
os << (int)buffer.buf[i] << ",";
}
os << "]";
return os;
}

int main() {
auto buffer = Buffer(10);
buffer.write(42);
auto buffer2 = buffer;
buffer2.write(97);
cout << buffer << endl;
cout << buffer2 << endl;
}

在上面的代码中,我们实现了拷贝构造函数与拷贝赋值函数,在两个函数内部都开辟了新的空间,然后将原buffer中的内容复制到新的buffer中,两个buffer对象分别管理两段不同的内存空间。这样就实现了深拷贝。

程序的输出如下:

In the code above, we implemented the copy constructor and the copy assignment operator. Both functions allocate new space and copy the content of the original buffer into the new buffer, so the two buffer objects manage two different memory regions. This achieves a deep copy.

The output of the program is as follows:

1
2
Buffer(1/10)[42,0,0,0,0,0,0,0,0,0,]
Buffer(2/10)[42,97,0,0,0,0,0,0,0,0,]

可以看到两个buffer中的内容不同,并且长度不同,程序也能正常退出,不崩溃。

As you can see, the two buffers have different contents and different lengths, and the program exits normally without crashing.

移动构造函数

观察这样一段代码:

Move Constructor

Take a look at the following code:

1
2
3
4
5
int main() {
vector<Buffer> buffers;
buffers.push_back(Buffer(10));
return 0;
}

这段代码中会执行几次构造函数几次析构函数呢?
通过在每个函数中打印输出内容,观察上述代码的执行过程。

How many times are the constructor and the destructor called in this code?
By printing output in each function, observe the execution process of the code above.

1
2
3
4
构造函数
拷贝构造函数
析构函数
析构函数

我们的本意只是创建一个对象,放在 vector 中,但是却执行了两次析构函数,也就是说上述代码中产生了两个 Buffer 对象。

buffers.push_back(Buffer(10))
这句代码的执行过程是这样的,首先调用构造函数创建一个 Buffer 的临时对象,在 push_back 的过程中,调用Buffer类中的拷贝构造函数创建一个新的对象,然后将临时对象释放掉。最后程序结束释放掉vector中的Buffer对象。

分析上述执行过程可以发现存在一个缺陷,也就是说我们本想要一个对象就够了,但是他却创建了两个释放了一个,而每次析构都需要释放堆区内存。如果一个程序中频繁出现复杂对象的复制,会导致程序整体的性能下降。

解决这个问题的思路也比较简单,那就是创建一个对象直接放在vector中就可以了,没有必要再执行一次拷贝构造函数。

为了解决这个为题,C++11中提出了移动语义的相关概念。

首先,像是 Buffer(10) 这样的对象被成为右值,也叫做将亡值,它的生命周期往往只是当前行,过了这一行,它就被析构掉了。移动构造函数的意义就是接管这些右值所申请的空间,而不必重新申请一块空间再进行拷贝操作。

移动构造函数和移动赋值函数的代码如下:

Our intention was just to create one object and put it into the vector, but the destructor was called twice, meaning that two Buffer objects were created in the code above.

buffers.push_back(Buffer(10))
This statement works like this: first, the constructor is called to create a temporary Buffer object; during push_back, the copy constructor of the Buffer class creates a new object, and then the temporary object is released. Finally, when the program ends, the Buffer object in the vector is released.

Analyzing the process above reveals a flaw: we only wanted one object, but it created two and released one, and every destruction has to release heap memory. If a program frequently copies complex objects, the overall performance will degrade.

The idea to solve this problem is also simple: just put the object directly into the vector — there is no need to run the copy constructor again.

To solve this problem, C++11 introduced the concept of move semantics.

First, an object like Buffer(10) is called an rvalue, also known as an expiring value. Its lifetime usually spans only the current line — after that line, it is destructed. The meaning of the move constructor is to take over the space allocated by these rvalues, instead of allocating a new space and copying again.

The code for the move constructor and the move assignment operator is as follows:

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
#include <iostream>
#include <bits/stdc++.h>
using namespace std;

class Buffer {
public:
explicit Buffer(int capacity)
: capacity(capacity), length(0), buf(new unsigned char[capacity]{0}) {
cout << "构造函数" << endl;
}

// 拷贝构造函数
Buffer(const Buffer& buffer)
{
cout << "拷贝构造函数" << endl;
this->capacity = buffer.capacity;
this->length = buffer.length;
this->buf = new unsigned char[buffer.capacity];
std::copy(buffer.buf,buffer.buf + buffer.capacity,this->buf);
}
// 拷贝赋值函数
Buffer& operator=(const Buffer& buffer)
{
cout << "拷贝赋值函数" << endl;
// 判断传进来的对象是不是自身,如果是自身不需要操作,直接返回即可。
if(this != &buffer)
{
this->capacity = buffer.capacity;
this->length = buffer.length;
// 先释放掉自身的空间,再开辟新的和被拷贝对象大小相同的空间。
delete[] this->buf;
this->buf = new unsigned char[buffer.capacity];
std::copy(buffer.buf,buffer.buf + buffer.capacity,this->buf);
}
return *this;
}

// 移动构造函数
Buffer(Buffer&& buffer) noexcept {
cout << "移动构造函数" << endl;
this->capacity = buffer.capacity;
this->length = buffer.length;
// 接管 buffer 的堆区内存
this->buf = buffer.buf;
// 将 buffer 的指针置空
buffer.capacity = 0;
buffer.length = 0;
buffer.buf = nullptr;
}
// 移动赋值函数
Buffer& operator=(Buffer&& buffer) noexcept {
cout << "移动赋值函数" << endl;
if(this != &buffer)
{
this->capacity = buffer.capacity;
this->length = buffer.length;
delete[] this->buf;
this->buf = buffer.buf;

buffer.capacity = 0;
buffer.length = 0;
buffer.buf = nullptr;
}
return *this;
}
int get_length() { return length; }
int get_capacity() { return capacity; }

bool write(unsigned char value)
{
if(length == capacity) return false;
buf[length++] = value;
return true;
}

~Buffer() {
cout << "析构函数" << endl;
delete[] buf;
}

friend ostream &operator<<(ostream &os, Buffer &buffer);

private:
unsigned char *buf;
int capacity;
int length;
};

ostream &operator<<(ostream &os, Buffer &buffer) {
os << "Buffer(" << buffer.length << "/" << buffer.capacity << ")[";
for (size_t i = 0; i < buffer.capacity; i++) {
os << (int)buffer.buf[i] << ",";
}
os << "]";
return os;
}

int main() {
vector<Buffer> buffers;
buffers.push_back(Buffer(10));
buffers[0] = Buffer(20);
return 0;
}

上述代码的执行结果变为:

The execution result of the code above becomes:

1
2
3
4
5
6
7
构造函数
移动构造函数
析构函数 // 由于我们吧临时对象的 buf 指针置空了,所以此处析构函数会释放空指针,对性能没什么影响,下面同理。
构造函数
移动赋值函数
析构函数
析构函数

委托构造函数

观察上面实现的 Big Five ,可以发现上面的实现存在大量的重复代码,尤其是拷贝构造函数和拷贝赋值函数,移动构造函数和移动赋值函数内部的代码。

实际上,拷贝构造函数和拷贝赋值函数是存在逻辑上的相关性的,可以通过在拷贝构造函数中调用拷贝赋值函数来实现代码的简化。
比如:

Delegating Constructors

Looking at the Big Five implemented above, we can see that the implementation contains a lot of duplicated code, especially inside the copy constructor and the copy assignment operator, as well as the move constructor and the move assignment operator.

In fact, the copy constructor and the copy assignment operator are logically related — we can simplify the code by calling the copy assignment operator inside the copy constructor.
For example:

1
2
3
4
5
6
// 拷贝构造函数
Buffer::Buffer(const Buffer& buffer)
{
// 调用拷贝赋值函数初始化自身。
*this = buffer;
}

但上述代码还存在一个问题,在拷贝赋值函数中创建新的内存空间之前先释放了自身所指的空间,因此,在执行 delete[] 前,要先将buf指针置空。可以通过类初始化列表来先初始化这几个成员变量。

But the code above still has a problem: the copy assignment operator releases the space that this points to before creating new memory space. Therefore, before executing delete[], the buf pointer must be set to null first. This can be done by initializing these member variables in the member initializer list.

1
2
3
4
5
6
// 拷贝构造函数
Buffer::Buffer(const Buffer& buffer)
: capacity(0), length(0), buf(nullptr)
{
*this = buffer;
}

同样的移动构造函数也可以用类似的方式。

The move constructor can be written in a similar way.

1
2
3
4
5
Buffer::Buffer(Buffer&& buffer) noexcept
: capacity(0), length(0), buf(nullptr)
{
*this = buffer;
}

但是,上述代码也存在一个问题,就是 *this=buffer 不会调用移动赋值函数,而是会调用拷贝赋值函数。这是为什么呢?buffer明明传进来的是右值引用啊?这还要用右值的定义出发,匿名对象,将亡值被叫做右值,在这个函数内部,buffer 是有名的,所以不是右值。

要想实现调用移动赋值函数,需要使用C++11的库函数std::move();

However, the code above also has a problem: *this=buffer will not call the move assignment operator; instead, it will call the copy assignment operator. Why is that? Isn’t buffer passed in as an rvalue reference? This goes back to the definition of an rvalue: anonymous objects and expiring values are called rvalues, but inside this function, buffer has a name, so it is not an rvalue.

To make it call the move assignment operator, we need to use the C++11 library function std::move();

1
2
3
4
5
6
Buffer::Buffer(Buffer&& buffer) noexcept
: capacity(0), length(0), buf(nullptr)
{
*this = std::move(buffer);
// 观察move的实现,可以发现其实就是进行了一个类型转换,写成 static_cast<Buffer&&>(buffer) 也是可以的。
}

经过上面的修改,代码已经简洁了很多,但是,构造函数初始化列表中的内容,还是被重复写了很多次。还是不够优雅。

C++ 这么强大的语言,肯定可以写的更优雅。
这里通过其他构造函数调用基本的构造函数来实现成员的初始化。这种方式就是委托构造

有了委托构造,Big Five 的最终版本也就形成了。

After the changes above, the code is already much simpler, but the content of the constructor’s member initializer list is still repeated many times. It is still not elegant enough.

A language as powerful as C++ can certainly be written more elegantly.
Here, the members are initialized by having other constructors call the basic constructor. This approach is called delegating construction.

With delegating constructors, the final version of the Big Five is complete.

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#include <iostream>
#include <bits/stdc++.h>
using namespace std;

class Buffer {
public:
explicit Buffer(int capacity) : capacity(capacity), length(0) {
cout << "构造函数" << endl;
// 容量不是 0 ,创建新的空间。
buf = capacity == 0 ? nullptr : new unsigned char[capacity]{};
}

// 拷贝构造函数
Buffer(const Buffer &buffer) : Buffer(0) { *this = buffer; }
// 拷贝赋值函数
Buffer &operator=(const Buffer &buffer) {
cout << "拷贝赋值函数" << endl;
// 判断传进来的对象是不是自身,如果是自身不需要操作,直接返回即可。
if (this != &buffer) {
this->capacity = buffer.capacity;
this->length = buffer.length;
// 先释放掉自身的空间,再开辟新的和被拷贝对象大小相同的空间。
delete[] this->buf;
this->buf = new unsigned char[buffer.capacity];
std::copy(buffer.buf, buffer.buf + buffer.capacity, this->buf);
}
return *this;
}

// 移动构造函数
Buffer(Buffer &&buffer) noexcept : Buffer(0) { *this = std::move(buffer); }
// 移动赋值函数
Buffer &operator=(Buffer &&buffer) noexcept {
cout << "移动赋值函数" << endl;
if (this != &buffer) {
this->capacity = buffer.capacity;
this->length = buffer.length;
this->buf = buffer.buf;

buffer.capacity = 0;
buffer.length = 0;
buffer.buf = nullptr;
}
return *this;
}
int get_length() { return length; }
int get_capacity() { return capacity; }

bool write(unsigned char value) {
if (length == capacity) return false;
buf[length++] = value;
return true;
}

~Buffer() {
cout << "析构函数" << endl;
delete[] buf;
}

friend ostream &operator<<(ostream &os, Buffer &buffer);

private:
unsigned char *buf;
int capacity;
int length;
};

ostream &operator<<(ostream &os, Buffer &buffer) {
os << "Buffer(" << buffer.length << "/" << buffer.capacity << ")[";
for (size_t i = 0; i < buffer.capacity; i++) {
os << (int)buffer.buf[i] << ",";
}
os << "]";
return os;
}

int main() {
vector<Buffer> buffers;
buffers.push_back(Buffer(10));
buffers[0] = Buffer(20);
return 0;
}