C++ 特殊成员函数生成规则
C++ 编译器会自动生成六种特殊成员函数。理解它们的生成规则,是写出正确、高效类的关键。
六种特殊成员函数
- 默认构造函数
T() - 析构函数
~T() - 拷贝构造函数
T(const T&) - 拷贝赋值运算符
T& operator=(const T&) - 移动构造函数
T(T&&)(C++11) - 移动赋值运算符
T& operator=(T&&)(C++11)
生成规则速查表
| 用户声明的函数 | 默认构造 | 析构 | 拷贝构造 | 拷贝赋值 | 移动构造 | 移动赋值 |
|---|---|---|---|---|---|---|
| 无 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| 非默认构造函数 | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ |
| 析构函数 | ✓ | ✓ | ✗¹ | ✗¹ | ✗ | ✗ |
| 拷贝构造函数 | ✗ | ✓ | ✓ | ✗¹ | ✗ | ✗ |
| 拷贝赋值运算符 | ✓ | ✓ | ✗¹ | ✓ | ✗ | ✗ |
| 移动构造函数 | ✗ | ✓ | ✗ | ✗ | ✓ | ✗ |
| 移动赋值运算符 | ✓ | ✓ | ✗ | ✗ | ✗ | ✓ |
✓ = 自动生成,✗ = 不生成 / 定义为删除。
¹ C++11–C++17 中这些拷贝操作会被弃用生成;C++20 起不再生成。
逐项生成规则
默认构造函数
生成条件:类没有任何用户声明的构造函数时。
1 | |
析构函数
生成条件:总是生成(除非用户声明)。
行为:依次调用非静态成员的析构函数,再调用基类析构函数。
重要结论:每个类都恰好有一个析构函数——要么由用户显式声明,要么由编译器隐式生成。即使析构函数被 = delete,它也是存在的,只是不能调用。
1 | |
拷贝构造函数 / 拷贝赋值运算符
生成条件:没有用户声明的对应拷贝操作。
C++20 关键变化:声明析构函数、拷贝构造函数或拷贝赋值运算符中的任意一个,会抑制另一个拷贝操作的自动生成。
1 | |
移动构造函数 / 移动赋值运算符 (C++11)
生成条件(需同时满足):
- 没有用户声明的拷贝操作(拷贝构造、拷贝赋值)
- 没有用户声明的移动操作(移动构造、移动赋值)
- 没有用户声明的析构函数
- 所有非静态数据成员和基类都可移动
核心规律:拷贝操作会抑制移动操作;移动操作不会抑制拷贝操作,但会把拷贝操作标记为删除。
1 | |
继承中的特殊成员函数
生成与调用顺序
编译器为派生类生成特殊成员函数时,会自动调用基类对应版本:
- 构造函数:基类构造 → 成员构造 → 派生类构造体
- 析构函数:派生类析构体 → 成员析构 → 基类析构
1 | |
基类缺少移动操作的影响
如果基类没有移动构造函数,派生类即使能生成移动构造函数,对基类部分也会退化为拷贝构造:
1 | |
基类访问说明符的影响
如果基类的构造函数、析构函数或赋值运算符是 private,派生类无法生成对应的特殊成员函数。
1 | |
基类析构函数应为虚函数
若类可能被当作多态基类使用,其析构函数应声明为虚函数,否则通过基类指针删除派生类对象会导致未定义行为。
1 | |
Rule of Zero / Three / Five
Rule of Zero(推荐)
用智能指针、标准容器等 RAII 类型管理资源,让编译器自动生成所有特殊成员函数。
1 | |
Rule of Three
如果类需要自定义析构函数、拷贝构造函数或拷贝赋值运算符中的任意一个,通常三者都需要自定义。这意味着类在管理某种资源。
Rule of Five (C++11)
在 Rule of Three 基础上加上移动构造函数和移动赋值运算符。当需要手动管理资源且移动语义有价值时使用。
显式控制:= default 与 = delete
1 | |
注意:= default 在类内声明与类外定义有区别。
1 | |
常见陷阱
陷阱 1:声明析构函数阻止移动操作
1 | |
陷阱 2:基类析构函数非虚
1 | |
陷阱 3:成员不可移动导致移动操作被删除
1 | |
最佳实践
- 优先遵循 Rule of Zero:组合 RAII 类型,让编译器生成特殊成员函数。
- 需要自定义析构函数时,考虑 Rule of Five:同时声明/删除拷贝和移动操作。
- 用
= default和= delete明确意图,不要依赖隐式规则。 - 基类析构函数声明为
virtual,除非确定不会被多态删除。 - 启用编译器警告:
-Wdeprecated、-Wdefaulted-function-deleted。
总结:一句话记忆
拷贝怕移动,移动怕拷贝;析构一出手,移动不再有;C++20 起,拷贝也没有。
核心规则
任何非默认构造函数会抑制默认构造函数的自动生成。
想保留默认构造?显式写T() = default。拷贝操作和移动操作互相排斥:
- 声明了拷贝构造/拷贝赋值中的任意一个,移动构造和移动赋值都不会自动生成。
- 声明了移动构造/移动赋值中的任意一个,拷贝操作会被隐式
= delete。
析构函数是“分水岭”:
- 一旦声明了析构函数,移动构造和移动赋值就不会自动生成。
- 从 C++20 起,拷贝构造和拷贝赋值也不会再自动生成;在 C++11–C++17 中,它们仍会被生成,但已被标记为弃用。
同类操作之间也互相抑制:
- 移动构造和移动赋值(C++11 起):声明其中一个,另一个就不会自动生成。
- 拷贝构造和拷贝赋值(C++20 起):声明其中一个,另一个也不会自动生成;在 C++11–C++17 中,它们仍会被生成,但已被标记为弃用。
能自动生成的函数,编译器才会帮你写;只要任一基类或成员不支持某操作,该操作就会被隐式删除。
实践建议
- 优先用 Rule of Zero:用智能指针、标准容器等 RAII 类型组合出类,让编译器自动生成全部特殊成员函数。
- 必须手动管理资源时,遵循 Rule of Five:显式声明/删除所有五个操作,不要依赖隐式规则。
- 多态基类一定要把析构函数声明为
virtual。 - 用
= default/= delete明确表达意图,避免代码产生意料之外的被删除函数。


