C++ 中的 Rule-of-Three

Three 为何物? 所谓的 Three 其实就是 copy constrcutor (拷贝构造函数)copy assignment operator (拷贝赋值操作符)destructor (析构函数) 。 在介绍 Rule-of-Three 之前,我们回顾一下 C++ Class 的声明与定义。

class Empty { private: int _val { 0 }; };

Empty 十分简单,我没有为它赋予任何 data members (数据成员) 或显式地声明/定义 member functions (成员函数) 。但事实上,编译器会在必要时为类 Empty 合成必要的成员函数。此话何解?我们不妨来看一下示例。
int main() { Empty e1 {}; // 创建一个Empty对象 Empty e2 { e1 }; // 通过一个已有的Empty对象给另外一个Empty对象初始化 Empty e3; e3 = e2; // 通过一个已有的Empty对象给另外一个Empty对象初始化 }

【C++ 中的 Rule-of-Three】由于我没有为类 Empty 显式地编写构造函数函数,编译器自然会为我补充如下的一个 default constructor (默认构造函数)
// C++ 概念代码 Empty::Empty() { // 啥也不干 }

当我们需要通过 e1 去构造更多的相同类型对象的时候,编译器又帮我们做了以下的事情。编译器会为类 Empty 添加拷贝构造函数 Empty::Empty(const Empty&) 和拷贝赋值操作符 Empty& operator=(const Empty& other)
事实上就类 Empty 的结构而言,编译器根本不需要生成拷贝构造函数和拷贝赋值操作符。因为类 Empty 里根本没有其他得 class object (类对象) 数据成员。编译器只需要通过 bitwise copy (位逐次拷贝) 把内存逐一拷贝便能完成任务。不过我们可以假设编译器会自动生成所需要的函数。
// C++ 概念代码 Empty::Empty(const Empty& other) { _val = other._val; }Empty& Empty::operator=(const Empty& other) { _val = other._val; return *this; }Empty::~Empty() { // 啥也不干 }

为什么要给 Three 定规则? 我们已经对 Three 有了初步的了解,编译器可能会在背后为我们做了很多事情。不过助手毕竟也不过我们不能完全依赖编译器的行为。
由于类 Empty 添加了新的数据成员,所以我显式地声明并定义了构造函数 Empty::Empty(int, char) 。同时为了避免内存泄漏,我也添加了析构函数 Empty::~Empty()
class Empty { public: Empty(int val, char c) : _val(val), _cptr(new char(c)) {} ~Empty() { delete _cptr; }private: int _val { 0 }; char* _cptr { nullptr }; };

然后我尝试对类 Empty 的一些对象进行拷贝操作。此时编译器再次帮我添加两个成员函数。
Empty(const Empty& other) { _val = other._val; _cptr = other._cptr; }Empty& operator=(const Empty& other) { _val = other._val; _cptr = other._cptr; return *this; }

编译器真是帮了我一个大忙,自动帮我把 Empty e1 的成员逐个拷贝给 Empty e2 。不过遗憾的是,程序结束之前会崩溃。崩溃原因是 Empty::_cptr 被重复释放。因为它拷贝的是 Empty::_cptr 这个指针,而非 Empty::_cptr 这个指针指向的地址存放的值。
int main() { Empty e1(10, 'h'); auto e2 = e1; } // creash !!!

所以当 Three 同时存在的时候,为了让它们都 "安分守己",我们必须给它们顶下规矩,这就是所谓的 Rule of Three
Rules Three 的问题在于,如果一个类里面有指针类型 (或者需要手动释放的资源类型) 的数据成员,编译器的位逐次拷贝会让程序变得不可靠。所以我们必须为让程序变得安全。基于上面的问题,我们有两个解决方案。要么这个类的对象是不允许被拷贝;要么这个类的对象允许被拷贝,但我们必须亲自重写这个类的 Three
方案一:
class Empty { public: // other user-definfed ctors ~Empty() { delete _cptr; }Empty(const Empty& other) = delete; Empty& operator=(const Empty& other) = delete; private: // data members };

方案二:
class Empty { public: // other user-definfed ctors ~Empty() { delete _cptr; }Empty(const Empty& other) { _val = other._val; if (_cptr != nullptr) // 检查 _cptr 的合法性很重要 delete _cptr; _cptr = new char(*other._cptr); }Empty& operator=(const Empty& other) { _val = other._val; if (_cptr != nullptr) // 同上 delete _cptr; _cptr = new char(*other._cptr); }private: // data members };

启发 C++ 的对象模型往往不是我们想象中的那么简单,编译器暗地里会做很多额外的工作。所以我们在管理类对象的资源的时候需要格外小心。

    推荐阅读