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

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

浅拷贝与深拷贝

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

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

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

#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;
}

函数的执行结果如下:

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指针指向的是同一块内存空间,因此在两个对象离开作用域被析构的过程中发生了内存的重复释放问题。这也就是浅拷贝的问题所在。

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

#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对象分别管理两段不同的内存空间。这样就实现了深拷贝。

程序的输出如下:

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中的内容不同,并且长度不同,程序也能正常退出,不崩溃。

移动构造函数

观察这样一段代码:

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

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

构造函数
拷贝构造函数
析构函数
析构函数

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

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

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

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

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

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

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

#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;
}

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

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

委托构造函数

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

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

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

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

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

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

Buffer::Buffer(Buffer&& buffer) noexcept
    : capacity(0), length(0), buf(nullptr)
    {
        *this = buffer;
    }

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

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

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

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

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

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

#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;
}