Three 为何物?
所谓的 Three 其实就是 copy constrcutor (拷贝构造函数) 、copy assignment operator (拷贝赋值操作符) 和 destructor (析构函数) 。 在介绍 Rule-of-Three 之前,我们回顾一下 C++ Class 的声明与定义。
class Empty
{
};
类 Empty
十分简单,我没有为它赋予任何 data members (数据成员) 或显式地声明/定义 member functions (成员函数) 。但事实上,编译器会在必要时为类 Empty
合成必要的成员函数。
int main()
{
Empty e1 {}; // 创建一个Empty对象
}
由于我没有为类 Empty
声明构造函数函数,编译器自然会为我补充一个 default constructor (默认构造函数) 。
// C++ 概念代码
Empty::Empty() {}
当我们需要通过 e1
去构造更多的相同类型对象的时候,编译器又帮我们做了以下的事情。编译器会为类 Empty
添加拷贝构造函数 Empty::Empty(const Empty&)
和拷贝赋值操作符 Empty& operator=(const Empty& other)
。
事实上就类 Empty
的结构而言,编译器根本不需要生这两个函数。因为类 Empty
里根本没有其他的 class object (类对象) 数据成员。编译器只需要通过 bitwise copy (位逐次拷贝) 把内存逐一拷贝便能完成任务。不过我们可以假设编译器会自动生成所需要的函数。
// C++ 概念代码
class Empty
{
public:
Empty(int val) : _val(val) {}
private:
int _val { 0 };
};
Empty::Empty(const Empty& other)
{
_val = other._val;
}
Empty& Empty::operator=(const Empty& other)
{
_val = other._val;
return *this;
}
Empty::~Empty()
{
}
// 执行代码
int main()
{
Empty e1 {};
Empty e2 { e1 };
Empty e3;
e3 = e2;
}
为什么要给 Three 定规则?
我们已经对 Three 有了初步的了解,同时编译器可能会在背后做了很多小动作。所以我们不能完全依赖编译器的行为。
由于类 Empty
添加了新的数据成员,所以我定义了新的构造函数。同时为了避免内存泄漏,我也补上了析构函数 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;
}
// 执行代码
int main()
{
Empty e1 {"1", "empty"};
Empty e2 { e1 };
Empty e3;
e3 = e2;
}
编译器把 e1
的成员逐个拷贝给 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;
_cptr = new char(*other._cptr);
}
Empty& operator=(const Empty& other)
{
if (*this == other)
return *this;
_val = other._val;
if (_cptr != nullptr)
delete _cptr;
_cptr = new char(*other._cptr);
}
private:
// data members
};
启发
C++ 的对象模型往往不是我们想象中的那么简单,编译器暗地里会做很多额外的工作。所以我们在管理类对象的资源的时候需要格外小心。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。