C++特种成员函数生成机制及相关原则
注:默认C++标准是C++11及以后的标准,因为C++11之前的标准定义的默认成员函数不包含移动构造函数和移动赋值运算符
1. C++默认成员函数
默认成员函数的定义:
类中没有显示声明,在需要时由编译器自动生成的函数,包括默认构造函数、默认析构函数、默认复制构造函数、默认复制赋值运算符、默认移动构造函数和默认移动赋值运算符共计6种。
基础类定义:
class BaseClass {};
1.1 构造函数
对于任何一个class而言构造函数都是必须,因为每一个类对象的构造都会调用构造函数。
BaseClass();
1.1.1 生成条件
当且仅当,类中没有显式声明任何构造函数(包括任何形式的有参构造)时,编译器会为该类自动生成默认构造函数。
1.1.2 属性
属性 | 结果 |
---|---|
访问权限 | public |
inline | yes |
virtual | no |
另外,C++允许构造函数重载,当类的实现已经有了对于构造函数的声明时,可以通过BaseClass() = default
的方式保留编译器自动生成默认构造函数的机会,在需要的时候编译器仍然可以自动生成。
1.2 析构函数
析构函数在类对象生命周期结束时被调用,负责对象资源的销毁,所以析构函数对于任何一个class而言都是必须的。
~BaseClass();
1.2.1 生成条件
当且仅当,类中没有显式声明析构函数时,编译器会为该类自动生成默认析构函数。
1.2.2 属性
属性 | 结果 |
---|---|
访问权限 | public |
inline | yes |
virtual | 当基类的析构函数为虚时才有virtual属性 |
noexcept | yes |
关于virtual属性的代码示例:
如下情况时,编译器为Base
和Deriver
类自动生成的析构函数都为非虚析构函数。
class Base{};
class Derive: public Base{};
如下情况时,编译器为Deriver
类自动生成的析构函数为虚析构函数。
class Base
{
public:
virtal ~Base(){};
};
class Derive: public Base{};
如下情况时,编译器为DeriverA
和DeriverB
类自动生成的析构函数都为虚析构函数。
class Base
{
public:
virtal ~Base(){};
};
class DeriveA: public Base{};
class DeriveB: public DeriveA{};
另外,C++不允许析构函数重载,所以显式声明析构函数和~BaseClass() = default
只能存其一。
1.3 复制操作
复制是指同一个类的不同对象之间的赋值行为,都是按成员进行非静态成员的复制构造。在C++中有默认赋值构造函数和默认复制赋值运算符,两者的自动生成机制相互独立,声明一个并不会组阻止编译器自动生成另一个。
1.3.1 默认复制构造函数
复制构造函数的基本形式如下,表示用一个对象构造另外一个同类对象。
BaseClass(BaseClass&);
如下代码中,b2
和b3
对象的构造都会调用复制构造函数。
BaseClass b1{};
BaseClass b2{b1};
BaseClass b3(b1);
1.3.1.1 生成条件
-
当且仅当,类中未显式声明复制构造函数并且代码中出现了调用复制构造函数的场景时编译器才会为该类自动生成默认复制构造函数。
-
如果该类中声明了移动操作,则默认复制构造函数将被删除。
-
如何该类中声明了复制赋值运算符或析构函数,仍然生成默认复制构造函数被认为是废弃行为。
1.3.1.2 属性
属性 | 结果 |
---|---|
访问权限 | public |
inline | yes |
virtual | no |
explicit | no |
另外,当显式声明复制构造函数时,该函数的参数列表必须是BaseClass&
的形式。必须是引用的原因是:如果没有引用那么实参向形参的赋值过程中为值传递,就会调用复制构造函数,而复制构造函数的参数本来就是值传递又会调用复制构造函数,这样会导致无限递归下去,而引用的传递则不会调用复制构造函数。
1.3.2 默认复制赋值运算符
复制赋值运算符基本声明如下,表示同类不同对象之间的数据拷贝。
BaseClass& operator=(BaseClass&);
如下代码中,b2 = b1
过程会调用复制赋值运算符。
BaseClass b1{};
BaseClass b2{};
b2 = b1;
1.3.2.1 生成条件
-
当且仅当,类中未显式声明复制赋值运算符并且代码中出现了调用复制赋值运算符的场景时编译器才会为该类自动生成默认复制赋值运算符。
-
如果该类中声明了移动操作,则默认复制赋值运算符将被删除。
-
如果该类中声明了复制构造函数或析构函数,仍然生成默认复制赋值运算符被认为是废弃行为。
1.3.2.2 属性
属性 | 结果 |
---|---|
访问权限 | public |
inline | yes |
virtual | no |
另外,当显式声明复制赋值运算符时,该函数的参数列表必须是BaseClass&
的形式,原因同复制构造函数。
1.4 移动操作
移动操作用于资源控制权的移交,具体过程则是按成员进行非静态成员的移动构造,以达到减少数据的复制的目的,从而提高程序运行效率。在C++中有默认移动构造函数和默认移动赋值运算符,两种移动操作不相互独立,声明其中一个就会阻止编译器生成另一个。
1.4.1 移动构造函数
移动构造函数的基本形式如下,表示用一个对象移动构造另外一个同类对象。
BaseClass(BaseClass&&);
如下代码中,b2
和b3
对象的构造都会调用移动构造函数。
BaseClass b1{};
BaseClass b2{std::move(b1)};
BaseClass b3(std::move(b2));
1.4.1.1 生成条件
当且仅当,类中没有声明复制操作(复制构造函数和复制赋值运算符)、移动操作(移动赋值运算符)和析构函数并且出现会调用移动构造函数场景时,编译器会为该类自动生成默认移动构造函数。
1.4.1.2 属性
属性 | 结果 |
---|---|
访问权限 | public |
inline | yes |
virtual | no |
explicit | no |
1.4.2 移动赋值运算符
移动赋值运算符的基本形式如下,表示将实参所拥有的资源转移给当前对象。
BaseClass& operator=(BaseClass&&);
每当重载决议选择移动赋值运算符时,该函数会被调用。例如=
左侧为可赋值的左值,而右侧则为同类型或可隐式转换为同类型的可移动的右值,示例如下:
BaseClass b1{};
BaseClass b2{};
b1 = std::move(b2);
1.4.2.1 生成条件
当且仅当,类中没有显式声明复制操作(复制构造函数和复制赋值运算符)、移动操作(移动构造函数)和析构函数并且出现会调用移动构造函数场景时,编译器会为该类自动生成默认移动构造函数。
1.4.2.2 属性
属性 | 结果 |
---|---|
访问权限 | public |
inline | yes |
virtual | no |
2. 相关的编码原则
2.1 三之原则
如果一个类需要用户自定义析构函数、复制构造函数或复制赋值运算符,那么该类几乎需要全部三者。
2.2 五之原则
用户自定义的析构函数、复制构造函数和复制赋值运算符会阻止默认移动构造函数和移动赋值运算符的自动生成,所以如果一个类需要用户定义移动操作,那么该类就需要用户自定义全部五个特殊成员函数。
2.3 零之原则
有自定义析构函数、复制操作函数和移动操作函数的类应该专门处理所有权,其他类都不应该拥有自定义的析构函数、复制操作函数和移动操作函数。
参考内容
- 《Effectivate Modern C++》
- https://zh.cppreference.com/w/cpp/language/rule_of_three