C++ 类和对象(二):默认成员函数详解
在 C++ 中,当你定义一个类时,即使你什么都不写,编译器也会为你自动生成一些特殊的成员函数,这些函数被称为默认成员函数(也叫合成成员函数或隐式生成的特殊成员函数)。
C++11 之后,这些默认生成的函数行为更加清晰和可控,理解它们是掌握 C++ 类语义、资源管理、移动语义等现代 C++ 特性的关键。
一、C++ 类会自动生成的 6 个默认特殊成员函数
| 序号 | 函数名称 | 作用简述 | 是否默认生成 | 是否 noexcept(默认) | 是否 trivial(平凡)可能 | 是否可以被 =default / =delete |
|---|---|---|---|---|---|---|
| 1 | 默认构造函数 | X() | 是 | 是 | 是(条件) | 可以 |
| 2 | 拷贝构造函数 | X(const X&) | 是 | 是 | 是(条件) | 可以 |
| 3 | 拷贝赋值运算符 | X& operator=(const X&) | 是 | 是 | 是(条件) | 可以 |
| 4 | 移动构造函数 | X(X&&) | 是(条件) | 是 | 是(条件) | 可以 |
| 5 | 移动赋值运算符 | X& operator=(X&&) | 是(条件) | 是 | 是(条件) | 可以 |
| 6 | 析构函数 | ~X() | 是 | 是(条件) | 是(条件) | 可以 |
二、每个默认成员函数的生成规则(C++11/14/17/20)
1. 默认构造函数 X::X()
什么时候生成?
- 用户没有声明任何构造函数时,编译器会生成一个无参默认构造函数。
- 如果用户声明了任何带参数的构造函数,则不再生成默认构造函数。
默认行为:
- 对类类型的成员:调用成员的默认构造函数
- 对基本类型成员:不做任何初始化(值不确定)
class A {
int x; // 未初始化
string s; // 调用 string()
vector<int> v; // 调用 vector()
};
A a; // x 是未定义值,s 和 v 是空对象
现代建议:
- 显式写
= default或= delete - 优先使用成员初始化列表,避免未初始化问题
2. 拷贝构造函数 X::X(const X&)
生成条件:
- 用户没有声明任何拷贝构造函数时,编译器自动生成。
- 生成的版本是成员逐个拷贝(member-wise copy)。
默认行为:
- 基本类型成员:值拷贝
- 类类型成员:调用该成员的拷贝构造函数
class Point {
int x, y;
public:
Point(const Point& other) = default; // 编译器会生成逐成员拷贝
};
3. 拷贝赋值运算符 X& X::operator=(const X&)
生成条件:
- 用户没有声明任何拷贝赋值运算符时生成。
- 默认行为:成员逐个赋值,并返回
*this
注意:
- 如果类有 const 或引用成员,默认拷贝赋值会被抑制(无法生成)。
4. 移动构造函数 & 移动赋值运算符(C++11 引入)
生成条件(非常重要):
编译器只在以下三种情况都满足时才会生成移动操作:
- 用户没有声明任何拷贝构造函数、拷贝赋值、移动构造函数、移动赋值、析构函数。
- 用户没有声明任何纯虚函数(即不是抽象类)。
- 移动操作的隐式声明没有被显式删除(=delete)。
最常见导致不生成移动语义的情况:
- 声明了析构函数
- 声明了拷贝构造 / 拷贝赋值
- 声明了其中一个移动函数(另一个通常也会被抑制)
class MyString {
char* str;
public:
~MyString() { delete[] str; } // 声明了析构 → 移动被抑制
// 此时编译器不会生成移动构造和移动赋值
};
现代推荐写法(Rule of Five / Rule of Zero):
// 最佳实践:要么全定义,要么全默认(Rule of Zero)
class Widget {
public:
Widget() = default;
Widget(const Widget&) = default;
Widget& operator=(const Widget&) = default;
Widget(Widget&&) = default;
Widget& operator=(Widget&&) = default;
~Widget() = default;
};
5. 析构函数 ~X()
生成条件:
- 用户没有声明析构函数时,编译器生成一个默认析构函数。
- 默认行为:对每个成员调用其析构函数。
特殊情况:
- 如果类有虚函数,默认析构函数是虚析构。
- 如果用户声明了析构函数(无论是否 =default),都会抑制移动操作的生成。
三、六大默认成员函数的生成规则总结表(非常重要)
| 你声明了这些成员中的任何一个 | 默认构造函数 | 拷贝构造 | 拷贝赋值 | 移动构造 | 移动赋值 | 析构函数 |
|---|---|---|---|---|---|---|
| 什么都没声明 | 生成 | 生成 | 生成 | 生成 | 生成 | 生成 |
| 声明了任何构造函数 | 不生成 | 生成 | 生成 | 生成 | 生成 | 生成 |
| 声明了拷贝构造 | 生成 | — | 生成 | 不生成 | 不生成 | 生成 |
| 声明了拷贝赋值 | 生成 | 生成 | — | 不生成 | 不生成 | 生成 |
| 声明了移动构造 | 不生成 | 生成 | 生成 | — | 不生成 | 生成 |
| 声明了移动赋值 | 不生成 | 生成 | 生成 | 不生成 | — | 生成 |
| 声明了析构函数 | 生成 | 生成 | 生成 | 不生成 | 不生成 | — |
四、现代 C++ 推荐的写法模式(2024-2025 主流)
- Rule of Zero(最推荐)
不写任何特殊成员函数,让编译器自动生成(适合资源管理完全交给成员的类) - Rule of Five
如果你需要自定义其中一个,就把五个都显式声明(通常配合 =default 或实现) - Rule of Four + Move(折中)
自定义析构 + 拷贝构造 + 拷贝赋值 + 移动构造 + 移动赋值(较老风格)
五、经典面试问题
- 声明了析构函数后,移动构造还会生成吗?为什么?
- 什么时候编译器不会生成默认构造函数?
- 什么是 trivial 的默认构造函数?有什么用?
- 如果一个类有引用成员或 const 成员,会影响哪些默认函数的生成?
- 如何让一个类只能移动不能拷贝?(最常见写法是什么?)
如果你想看具体代码示例、某个规则的详细实验、或者“如何设计一个支持完美转发的类”,可以告诉我,我可以继续深入展开讲解。