三元规则指出,如果一个类型曾经需要具有用户定义的副本构造函数,副本分配运算符或析构函数,则它必须具有所有三个。
该规则的原因是,需要这三个条件中的任何一个的类都可以管理某些资源(文件句柄,动态分配的内存等),并且需要这三个元素来一致地管理该资源。复制功能处理如何在对象之间复制资源,并且析构函数将根据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.name和p2.name都指向相同的字符串。
当main结束时,析构函数将被调用。Firstp2的析构函数将被调用;它将删除字符串。然后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) { // 使用复制和交换习惯用法实现分配 Person copy(other); swap(copy); // 假设swap()交换* this的内容并复制 return *this; }
复制分配运算符的实现由于需要释放现有缓冲区而变得复杂。复制和交换技术创建一个临时对象,该对象保存一个新缓冲区。交换原始缓冲区的内容*this并copy赋予其所有权copy。copy函数销毁后,销毁会释放先前由拥有的缓冲区*this。