早些时候,C++对拷贝行为的管理需要通过三个函数完成,其分别是:
- 拷贝构造函数
- 拷贝赋值函数
- 析构函数
三法则,一析两拷。
class A
{
public:
A() { }
A& operator=(const A&) {} //rule1
A(const A&) {} //rule2
~A() {}; //rule3
private:
};
这就叫做“三法则”,随着语言的发展,增加了两个函数,用于更精细的拷贝构造:
- 移动构造函数
- 移动赋值运算符
扩充之后的拷贝法则被称为“五法则”。
三五法则让编译器承担部分规则定义事实上我们不需要定义所有的五个法则,如果你没有定义的话,使用语法:规则=default
让编译器按照其内置规则补充未明显定义的法则。如,让系统生成拷贝赋值规则:
A & A(const &A)=default;
如,拷贝构造函数,按顺序复制原对象的所有数据成员。别人做的事情(编译器默认行为)当然不如自己做的,可能出现内存泄漏、值非预期等问题。
为了降低拷贝资源上出现的问题,我们有以下trick:
- 如果需要定义析构函数,那么它肯定需要拷贝构造和拷贝赋值;
- 如果需要拷贝构造函数,那么也需要赋值构造函数,不一定需要析构函数;
对于第一个trick,一个类在什么情况下需要定义析构函数?动态分配一个资源时就会需要定义析构函数,因为合成析构函数并不会对你动态分配的内存进行delete操作,那为什么一定要定义好两种拷贝行为?假设我们有一个自定义的类HasPtr,查看下面例子:
HasPtr f(HasPtr hp)
{
HasPtr ret=hp;//拷贝给定的HasPtr
return ret; //ret和hp都被销毁,删除了同一块内存
}
int main()
{
HasPtr a;
HasPtr b=a;//a b都含有同一个动态资源
}
//b先离开,删除了这个共有资源
//a再离开,再次删除同一个资源
假设在析构函数中进行了动态分配内存的delete,函数参数hp和ret都会在f返回时进行析构,也就是进行了两次delete。
对于第二个trick,如果一个类需要控制拷贝构造行为,那么意味着它必然需要一并控制拷贝赋值行为(例子,独一无二的ID号),因为没有用到动态内存,因此完全不需要析构函数。
三五法则指定禁止部分规则假如我们需要禁止拷贝构造规则,可以这么写
A (const & A )=delete;
使用规则=delete
禁止用户调用此项规则。当用户创建对象时,使用类似以下语句:
A a;
A b(a);//error,因为被删除了
同理,我们也可以对析构函数进行delete
声明。在此之前,这样的禁止是通过私有化该规则完成的。
~A()=delete;
如果不能调用析构函数,那么编译器将会禁止包括临时在内的对象生成。但是我们可以通过new
语句生成这个对象,但是又不能是使用delete
释放new
指针对应的内存,这个时候就需要额外定义内存回收函数,以防止内存泄漏。
- 一个类下的类成员析构函数是删除的,那么该类析构函数也会受影响为删除的;
- 一个类下的类成员拷贝构造函数是删除的,那么该类拷贝构造也为删除的;
- 一个类下的类成员赋值运算符是删除的,那么该类的赋值运算符也是删除的;
- 一个类下的类成员析构函数是删除的,那么该类的默认构造函数是删除的;
- 一个类下引用没有初始化器(默认初值),该类默认构造函数是删除的;
- 一个类下const对象没有初始化器(默认初值),该类默认构造函数是删除的。
简单来说,一个类有成员对应的三法则若为删除,那么合成的三法则也是删除的;如果一个类成员引用、const对象没有给定初始值,那么该类的默认构造函数是删除的。