注意:这篇文章上次更新于1400天前,文章内容可能已经过时。
什么是 Big Five ?
Big Five 是指 C++ 中一个类的五个核心函数,在 C++ 11 之前还是 Big Three, C++ 11 引入了移动语义,三大核心函数变为了五大核心函数。
这五个核心函数分别是:
- 构造函数
- 拷贝构造函数
- 拷贝赋值函数
- 移动构造函数
- 移动赋值函数
浅拷贝与深拷贝
这是一个只写了默认构造函数的 Buffer 类,这个类包括三个私有成员,分别是用于存储数据的字符指针,容量以及长度。
在主函数中,首先创建一个 buffer 对象,然后通过等号的方式创建第二个对象 buffer2。
此时调用的是编译器提供的拷贝构造函数,这个默认的拷贝构造函数的行为就是在内存中进行按位的拷贝(浅拷贝),也就是说 buffer2 中的 buf 与 buffer 中的 buf 指向的是相同的内存空间。为了验证这一情况,在主函数中向buffer中写入了一个字符42,再打印输出两个buffer中的内容,可以看到输出的结构完全相同。
1 |
|
函数的执行结果如下:
1 | Buffer(1/10)[42,0,0,0,0,0,0,0,0,0,] |
在输出的结果中,除了可以发现两个Buffer对象的内容被同步修改之外,还可以发现另外两个点:
- buffer2 中虽然有内容,但它的长度还是显示的 0
- 程序并没有正确退出,而是产生了 core dumped
产生这两个现象的原因也很简单,由于buffer2并没有调用write函数,因此buffer2的length成员变量是没有被修改的。由于两个对象的buf指针指向的是同一块内存空间,因此在两个对象离开作用域被析构的过程中发生了内存的重复释放问题。这也就是浅拷贝的问题所在。
如果我们希望对象在复制的过程中,能够重新开辟一块内存空间,并使新的内存空间的内容与原来对象的内容保持一致,这种拷贝行为就叫做深拷贝,可以通过自己实现拷贝构造函数的方式来实现深拷贝行为。
1 |
|
在上面的代码中,我们实现了拷贝构造函数与拷贝赋值函数,在两个函数内部都开辟了新的空间,然后将原buffer中的内容复制到新的buffer中,两个buffer对象分别管理两段不同的内存空间。这样就实现了深拷贝。
程序的输出如下:
1 | Buffer(1/10)[42,0,0,0,0,0,0,0,0,0,] |
可以看到两个buffer中的内容不同,并且长度不同,程序也能正常退出,不崩溃。
移动构造函数
观察这样一段代码:
1 | int main() { |
这段代码中会执行几次构造函数几次析构函数呢?
通过在每个函数中打印输出内容,观察上述代码的执行过程。
1 | 构造函数 |
我们的本意只是创建一个对象,放在 vector 中,但是却执行了两次析构函数,也就是说上述代码中产生了两个 Buffer 对象。
buffers.push_back(Buffer(10))
这句代码的执行过程是这样的,首先调用构造函数创建一个 Buffer 的临时对象,在 push_back 的过程中,调用Buffer类中的拷贝构造函数创建一个新的对象,然后将临时对象释放掉。最后程序结束释放掉vector中的Buffer对象。
分析上述执行过程可以发现存在一个缺陷,也就是说我们本想要一个对象就够了,但是他却创建了两个释放了一个,而每次析构都需要释放堆区内存。如果一个程序中频繁出现复杂对象的复制,会导致程序整体的性能下降。
解决这个问题的思路也比较简单,那就是创建一个对象直接放在vector中就可以了,没有必要再执行一次拷贝构造函数。
为了解决这个为题,C++11中提出了移动语义的相关概念。
首先,像是 Buffer(10) 这样的对象被成为右值,也叫做将亡值,它的生命周期往往只是当前行,过了这一行,它就被析构掉了。移动构造函数的意义就是接管这些右值所申请的空间,而不必重新申请一块空间再进行拷贝操作。
移动构造函数和移动赋值函数的代码如下:
1 |
|
上述代码的执行结果变为:
1 | 构造函数 |
委托构造函数
观察上面实现的 Big Five ,可以发现上面的实现存在大量的重复代码,尤其是拷贝构造函数和拷贝赋值函数,移动构造函数和移动赋值函数内部的代码。
实际上,拷贝构造函数和拷贝赋值函数是存在逻辑上的相关性的,可以通过在拷贝构造函数中调用拷贝赋值函数来实现代码的简化。
比如:
1 | // 拷贝构造函数 |
但上述代码还存在一个问题,在拷贝赋值函数中创建新的内存空间之前先释放了自身所指的空间,因此,在执行 delete[] 前,要先将buf指针置空。可以通过类初始化列表来先初始化这几个成员变量。
1 | // 拷贝构造函数 |
同样的移动构造函数也可以用类似的方式。
1 | Buffer::Buffer(Buffer&& buffer) noexcept |
但是,上述代码也存在一个问题,就是 *this=buffer 不会调用移动赋值函数,而是会调用拷贝赋值函数。这是为什么呢?buffer明明传进来的是右值引用啊?这还要用右值的定义出发,匿名对象,将亡值被叫做右值,在这个函数内部,buffer 是有名的,所以不是右值。
要想实现调用移动赋值函数,需要使用C++11的库函数std::move();
1 | Buffer::Buffer(Buffer&& buffer) noexcept |
经过上面的修改,代码已经简洁了很多,但是,构造函数初始化列表中的内容,还是被重复写了很多次。还是不够优雅。
C++ 这么强大的语言,肯定可以写的更优雅。
这里通过其他构造函数调用基本的构造函数来实现成员的初始化。这种方式就是委托构造。
有了委托构造,Big Five 的最终版本也就形成了。
1 |
|


