三规则

Version <= C++ 03

Rule of Three 声明如果某个类型需要具有用户定义的复制构造函数,复制赋值运算符或析构函数,那么它必须具有所有三个

规则的原因是需要三个中的任何一个的类管理一些资源(文件句柄,动态分配的内存等),并且需要所有三个来一致地管理该资源。复制函数处理资源如何在对象之间复制,析构函数将根据 RAII 原则销毁资源。

考虑管理字符串资源的类型:

class Person
{
    char* name;
    int age;

public:
    Person(char const* new_name, int new_age)
        : name(new char[std::strlen(new_name) + 1])
        , age(new_age)
    {
       std::strcpy(name, new_name);
    }

    ~Person() {
        delete [] name;
    }
};

由于 name 是在构造函数中分配的,析构函数会释放它以避免泄漏内存。但是如果复制了这样的对象会发生什么?

int main()
{
    Person p1("foo", 11);
    Person p2 = p1;
}

首先,将建造 p1。然后 p2 将从 p1 复制。但是,C++生成的复制构造函数将按原样复制该类型的每个组件。这意味着 p1.namep2.name 都指向相同的字符串。

main 结束时,将调用析构函数。第一个 p2 的析构函数将被调用; 它会删除字符串。然后将调用 p1 的析构函数。但是,该字符串已被删除。在已删除的内存上调用 delete 会产生未定义的行为。

为避免这种情况,有必要提供合适的拷贝构造函数。一种方法是实现引用计数系统,其中不同的 Person 实例共享相同的字符串数据。每次执行复制时,共享引用计数都会递增。析构函数然后递减引用计数,仅在计数为零时释放内存。

或者我们可以实现值语义和深度复制行为

Person(Person const& other)
    : name(new char[std::strlen(other.name) + 1])
    , age(other.age)
{
    std::strcpy(name, other.name);
}

Person &operator=(Person const& other) 
{
    // Use copy and swap idiom to implement assignment
    Person copy(other);
    swap(copy);            //  assume swap() exchanges contents of *this and copy
    return *this;
}

由于需要释放现有缓冲区,因此复制赋值运算符的实现变得复杂。复制和交换技术创建一个包含新缓冲区的临时对象。交换*thiscopy 的内容将获得原始缓冲区的 copy 的所有权。当函数返回时,copy 的销毁会释放先前由*this 拥有的缓冲区。