拷贝控制指的是一个类如何管理其对象的

  • 拷贝
  • 移动
  • 赋值
  • 销毁

拷贝控制由以下五个特殊的成员函数组成:

  • 拷贝构造函数
  • 拷贝赋值运算符
  • 移动构造函数
  • 移动赋值运算符
  • 析构函数

拷贝控制的核心问题是资源管理,如果一个类不管理任何资源,那么编译器生成的默认版本通常就足够了。

当一个类包含指针成员并管理动态分配的内存时,默认的逐成员拷贝和赋值就会出问题,导致浅拷贝。

class Foo {
public:
    Foo() : p(new int[10]()) { }

    ~Foo() { delete[] p; }

    Foo(const Foo& other) {
        p = new int[10];
        std::copy(other.p, other.p + 10, p);
    }

    Foo& operator=(const Foo& other) {
        if (this != &other) {
            int* newp = new int[10];                // 先分配新内存
            std::copy(other.p, other.p + 10, newp); // 拷贝数据
            delete[] p;                             // 再释放旧内存
            p = newp;
        }
        return *this;
    }

    Foo(Foo&& other) noexcept {
        p = other.p;
        other.p = nullptr;
    }

    Foo& operator=(Foo&& other) noexcept {
        if(this != &other) {
            delete[] p; // 先要释放现有资源
            p = other.p;
            other.p = nullptr;
        }
        return *this;
    }

private:
    int *p;
};

自动生成拷贝控制函数的条件

需要注意的点:

  1. 拷贝构造和拷贝赋值是彼此独立的,如果一个显式定义,另外一个还是会自动生成默认的版本
  2. 移动构造和移动赋值都涉及资源的转移,如果显式定义其中一个,另外一个则不会自动生成。

拷贝构造函数

只有当没有用户声明以下任何一个时,编译器才自动生成拷贝构造函数:

  • 拷贝构造函数
  • 移动构造函数
  • 移动赋值运算符
  • 析构函数

拷贝赋值运算符

只有当没有用户声明以下任何一个时,编译器才自动生成拷贝赋值运算符:

  • 拷贝赋值运算符
  • 移动构造函数
  • 移动赋值运算符
  • 析构函数

移动构造函数

只有当没有用户声明以下任何一个时,编译器才自动生成移动构造函数:

  • 拷贝构造函数
  • 拷贝赋值运算符
  • 移动构造函数
  • 移动赋值运算符
  • 析构函数

移动赋值运算符

只有当没有用户声明以下任何一个时,编译器才自动生成移动赋值运算符:

  • 拷贝构造函数
  • 拷贝赋值运算符
  • 移动构造函数
  • 移动赋值运算符
  • 析构函数

析构函数

只要没有用户声明析构函数,编译器就会自动生成默认析构函数

三五零法则

三法则(c++11之前)

如果一个类需要显式定义以下三个特殊成员函数之一,那么通常也需要显式定义所有三个。

  • 析构函数
  • 拷贝构造函数
  • 拷贝赋值运算符

五法则

如果一个类需要显式定义以下五个特殊成员函数中的任何一个,那么通常也需要显式定义所有五个。

  • 析构函数
  • 拷贝构造函数
  • 拷贝赋值运算符
  • 移动构造函数
  • 移动赋值运算符

不提供移动构造函数和移动赋值运算符通常不是错误,但会导致失去优化机会。

零法则

尽可能不显式定义任何特殊成员函数,而是利用现代C++特性(如智能指针、容器)来管理资源。