C++ 编译器会自动生成六种特殊成员函数。理解它们的生成规则,是写出正确、高效类的关键。

六种特殊成员函数

  1. 默认构造函数 T()
  2. 析构函数 ~T()
  3. 拷贝构造函数 T(const T&)
  4. 拷贝赋值运算符 T& operator=(const T&)
  5. 移动构造函数 T(T&&) (C++11)
  6. 移动赋值运算符 T& operator=(T&&) (C++11)

生成规则速查表

用户声明的函数默认构造析构拷贝构造拷贝赋值移动构造移动赋值
非默认构造函数
析构函数✗¹✗¹
拷贝构造函数✗¹
拷贝赋值运算符✗¹
移动构造函数
移动赋值运算符

✓ = 自动生成,✗ = 不生成 / 定义为删除。

¹ C++11–C++17 中这些拷贝操作会被弃用生成C++20 起不再生成

逐项生成规则

默认构造函数

生成条件:类没有任何用户声明的构造函数时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct A {
// 编译器生成默认构造函数
};

struct B {
B(int x) : value(x) {}
// 没有默认构造函数
int value;
};

struct C {
C() = default;
C(int x) : value(x) {}
int value;
};

析构函数

生成条件:总是生成(除非用户声明)。

行为:依次调用非静态成员的析构函数,再调用基类析构函数。

重要结论:每个类都恰好有一个析构函数——要么由用户显式声明,要么由编译器隐式生成。即使析构函数被 = delete,它也是存在的,只是不能调用。

1
2
3
4
5
6
7
struct A {
~A(); // 用户声明,阻止自动生成
};

struct B {
std::string s; // 析构时自动调用 s.~string()
};

拷贝构造函数 / 拷贝赋值运算符

生成条件:没有用户声明的对应拷贝操作。

C++20 关键变化:声明析构函数、拷贝构造函数或拷贝赋值运算符中的任意一个,会抑制另一个拷贝操作的自动生成

1
2
3
4
5
6
7
8
9
10
11
12
13
struct A {
A(const A&) = delete; // 禁止拷贝
};

struct B {
B(B&&); // 声明移动操作
// C++11 起拷贝构造被隐式删除
};

struct C {
std::unique_ptr<int> p;
// 拷贝构造被隐式删除:unique_ptr 不可拷贝
};

移动构造函数 / 移动赋值运算符 (C++11)

生成条件(需同时满足):

  1. 没有用户声明的拷贝操作(拷贝构造、拷贝赋值)
  2. 没有用户声明的移动操作(移动构造、移动赋值)
  3. 没有用户声明的析构函数
  4. 所有非静态数据成员和基类都可移动

核心规律:拷贝操作会抑制移动操作;移动操作不会抑制拷贝操作,但会把拷贝操作标记为删除。

1
2
3
4
5
6
7
8
9
10
11
struct A {
// 编译器生成全部六个特殊成员函数
};

struct B {
~B(); // 声明析构函数 → 移动操作不再生成
};

struct C {
C(const C&); // 声明拷贝操作 → 移动操作被抑制
};

继承中的特殊成员函数

生成与调用顺序

编译器为派生类生成特殊成员函数时,会自动调用基类对应版本

  • 构造函数:基类构造 → 成员构造 → 派生类构造体
  • 析构函数:派生类析构体 → 成员析构 → 基类析构
1
2
3
4
5
6
7
8
9
struct Base {
std::string name;
};

struct Derived : Base {
std::vector<int> data;
// 编译器生成的拷贝构造会先调用 Base::Base(const Base&),
// 再调用 vector 的拷贝构造
};

基类缺少移动操作的影响

如果基类没有移动构造函数,派生类即使能生成移动构造函数,对基类部分也会退化为拷贝构造

1
2
3
4
5
6
7
8
9
10
11
struct Base {
std::vector<int> heavy;
// 没有移动构造(声明了析构、拷贝或移动操作之一)
};

struct Derived : Base {
std::vector<int> more;
};

Derived d;
Derived d2 = std::move(d); // Derived 可以移动,但 Base 部分被拷贝

基类访问说明符的影响

如果基类的构造函数、析构函数或赋值运算符是 private,派生类无法生成对应的特殊成员函数。

1
2
3
4
5
6
7
8
9
10
11
struct Base {
private:
Base(const Base&) = default; // private 拷贝构造
};

struct Derived : Base {
// 编译器无法生成 public 的拷贝构造
};

Derived d;
Derived d2 = d; // 编译错误

基类析构函数应为虚函数

若类可能被当作多态基类使用,其析构函数应声明为虚函数,否则通过基类指针删除派生类对象会导致未定义行为。

1
2
3
4
5
6
7
8
9
10
struct Base {
virtual ~Base() = default;
};

struct Derived : Base {
~Derived() override { /* 清理资源 */ }
};

std::unique_ptr<Base> p = std::make_unique<Derived>();
// 析构时正确调用 Derived::~Derived() 和 Base::~Base()

Rule of Zero / Three / Five

Rule of Zero(推荐)

用智能指针、标准容器等 RAII 类型管理资源,让编译器自动生成所有特殊成员函数。

1
2
3
4
5
6
class GoodClass {
std::string name;
std::vector<int> data;
std::unique_ptr<Impl> pImpl;
// 编译器自动生成正确的特殊成员函数
};

Rule of Three

如果类需要自定义析构函数、拷贝构造函数或拷贝赋值运算符中的任意一个,通常三者都需要自定义。这意味着类在管理某种资源。

Rule of Five (C++11)

在 Rule of Three 基础上加上移动构造函数和移动赋值运算符。当需要手动管理资源且移动语义有价值时使用。

显式控制:= default= delete

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 禁止拷贝,但可移动
struct NonCopyable {
NonCopyable() = default;
NonCopyable(const NonCopyable&) = delete;
NonCopyable& operator=(const NonCopyable&) = delete;
NonCopyable(NonCopyable&&) = default;
NonCopyable& operator=(NonCopyable&&) = default;
~NonCopyable() = default;
};

// 抽象接口:禁止所有拷贝/移动
struct Interface {
virtual ~Interface() = default;
Interface(const Interface&) = delete;
Interface& operator=(const Interface&) = delete;
virtual void doSomething() = 0;
};

注意= default 在类内声明与类外定义有区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
struct A {
A() = default; // 类内:仍是 trivial
int x;
};

struct B {
B(); // 仅声明
int x;
};
B::B() = default; // 类外定义:不再是 trivial

static_assert(std::is_trivially_default_constructible_v<A>); // OK
static_assert(std::is_trivially_default_constructible_v<B>); // 失败

常见陷阱

陷阱 1:声明析构函数阻止移动操作

1
2
3
4
5
6
7
8
9
10
class String {
public:
~String() { delete[] data; }
// 问题:移动操作不会被生成!
// 拷贝操作在 C++20 起也不会生成
private:
char* data;
};

// 修复:需要时显式声明五个操作,或改用 std::unique_ptr/vector

陷阱 2:基类析构函数非虚

1
2
3
4
5
6
7
8
9
10
11
12
class Base {
public:
virtual ~Base() = default; // 基类析构应为虚函数
};

class Derived : public Base {
public:
~Derived() { /* 清理资源 */ }
};

Base* p = new Derived;
delete p; // 若 ~Base 非虚,则 Derived 析构不会被调用

陷阱 3:成员不可移动导致移动操作被删除

1
2
3
4
5
6
7
8
9
10
11
struct NonMovable {
NonMovable(NonMovable&&) = delete;
};

struct Container {
NonMovable member;
// 移动操作被隐式删除
};

Container c;
Container c2 = std::move(c); // 编译错误

最佳实践

  1. 优先遵循 Rule of Zero:组合 RAII 类型,让编译器生成特殊成员函数。
  2. 需要自定义析构函数时,考虑 Rule of Five:同时声明/删除拷贝和移动操作。
  3. = default= delete 明确意图,不要依赖隐式规则。
  4. 基类析构函数声明为 virtual,除非确定不会被多态删除。
  5. 启用编译器警告-Wdeprecated-Wdefaulted-function-deleted

总结:一句话记忆

拷贝怕移动,移动怕拷贝;析构一出手,移动不再有;C++20 起,拷贝也没有。

核心规则

  1. 任何非默认构造函数会抑制默认构造函数的自动生成。
    想保留默认构造?显式写 T() = default

  2. 拷贝操作和移动操作互相排斥

    • 声明了拷贝构造/拷贝赋值中的任意一个,移动构造和移动赋值都不会自动生成。
    • 声明了移动构造/移动赋值中的任意一个,拷贝操作会被隐式 = delete
  3. 析构函数是“分水岭”

    • 一旦声明了析构函数,移动构造和移动赋值就不会自动生成。
    • C++20 起,拷贝构造和拷贝赋值也不会再自动生成;在 C++11–C++17 中,它们仍会被生成,但已被标记为弃用
  4. 同类操作之间也互相抑制

    • 移动构造和移动赋值(C++11 起):声明其中一个,另一个就不会自动生成。
    • 拷贝构造和拷贝赋值(C++20 起):声明其中一个,另一个也不会自动生成;在 C++11–C++17 中,它们仍会被生成,但已被标记为弃用
  5. 能自动生成的函数,编译器才会帮你写;只要任一基类或成员不支持某操作,该操作就会被隐式删除。

实践建议

  • 优先用 Rule of Zero:用智能指针、标准容器等 RAII 类型组合出类,让编译器自动生成全部特殊成员函数。
  • 必须手动管理资源时,遵循 Rule of Five:显式声明/删除所有五个操作,不要依赖隐式规则。
  • 多态基类一定要把析构函数声明为 virtual
  • = default / = delete 明确表达意图,避免代码产生意料之外的被删除函数。