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++ 的对象模型往往不是我们想象中的那么简单,编译器暗地里会做很多额外的工作。所以我们在管理类对象的资源的时候需要格外小心。
推荐阅读
- 热闹中的孤独
- JS中的各种宽高度定义及其应用
- 我眼中的佛系经纪人
- 《魔法科高中的劣等生》第26卷(Invasion篇)发售
- Android中的AES加密-下
- 放下心中的偶像包袱吧
- C语言字符函数中的isalnum()和iscntrl()你都知道吗
- opencv|opencv C++模板匹配的简单实现
- C语言浮点函数中的modf和fmod详解
- C语言中的时间函数clock()和time()你都了解吗