多个父类时,"谁先构造"由 继承声明里的书写顺序决定——从左到右,和初始化列表里写的顺序完全无关。完整顺序是:各父类(按声明顺序)→ 自己的成员对象(按声明顺序)→ 自己的构造函数体;析构一律反过来。所以:写下 class D : public A, public B 的那一刻,"先 A 后 B"就已经定死了。
11.1.3 讲了单继承的"先父后子",本节把父类从 1 个变成 N 个——规律一模一样,只是"父类之间"的顺序成了新问题。这里会再次强调一条被反复讲过的规矩(10.2.1 起就有):初始化列表里的书写顺序 ≠ 真实执行顺序。在多重继承里,这条规矩终于会让人真的翻车。
一、铁律:按"继承声明顺序",不按初始化列表顺序
class D : public B, public A { // ← 声明顺序:B 在前
public:
D() : A(), B() { } // ← 列表顺序:A 在前(但没用!)
};
真实构造顺序:B → A → D(跟声明顺序走)。
为什么这么定:因为声明顺序决定了对象里的内存布局(11.3.1)——B 的子对象排在前面、A 的排在后面。构造顺序跟着布局走,逻辑上最自然(先造排在前面那块的)。
通用顺序表(N 个父类时):
| 步骤 | 做什么 | 顺序由谁决定 |
|---|---|---|
| ① | 各个父类子对象依次构造 | 继承声明顺序(左 → 右) |
| ② | 自己的成员对象依次构造 | 成员在类里的声明顺序(10.2.1) |
| ③ | 自己的构造函数体 | 你写的那几行 |
析构:完全倒序——③ → ② → ①,各个父类之间的顺序也要倒过来(后声明的父类,先析构)。
二、初始化列表要写哪些父类
多个父类时,初始化列表里可以逐个点名:
class D : public A, public B {
public:
D(int x, int y) : A(x), B(y) { } // 给两个父类分别传参
};
三条实践建议:
| 建议 | 原因 |
|---|---|
| 列表里的书写顺序,尽量和"声明顺序"保持一致 | 便于阅读;顺序不一致时有些编译器会给警告(-Wreorder) |
| 不需要传参的父类可以省略不写 | 编译器会调它的无参构造(11.1.3) |
| 任意一个父类没有无参构造,就必须在列表里点名它 | 否则 no matching function for call to 'X::X()'(11.1.4) |
三、动手 Demo(含实例05:多重继承的构造顺序)
实例05 的现场:教材用这个例子验证"多个父类按声明顺序构造"。这里用编号打印把顺序钉死,并顺手验证"析构倒序"。
#include <iostream>
using namespace std;
class A {
public:
A() { cout << "[1] A 构造" << endl; }
~A() { cout << "[6] A 析构" << endl; }
};
class B {
public:
B() { cout << "[2] B 构造" << endl; }
~B() { cout << "[5] B 析构" << endl; }
};
class M { // 子类自己的成员对象
public:
M() { cout << "[3] M 构造" << endl; }
~M() { cout << "[4] M 析构" << endl; }
};
class D : public A, public B { // ★ 声明顺序:A、B
private:
M m;
public:
D() { cout << " D 构造函数体" << endl; }
~D() { cout << " D 析构函数体" << endl; }
};
int main() {
cout << "── 开始,创建 d ──" << endl;
{
D d;
cout << "── d 还在世 ──" << endl;
}
cout << "── d 已销毁 ──" << endl;
return 0;
}
看到什么算成功:
── 开始,创建 d ──
[1] A 构造
[2] B 构造
[3] M 构造
D 构造函数体
── d 还在世 ──
D 析构函数体
[4] M 析构
[5] B 析构
[6] A 析构
── d 已销毁 ──
读结果(两条规律一次看清):
| 方向 | 顺序 | 由谁决定 |
|---|---|---|
| 构造 | A → B → M → D 函数体 | 父类按继承声明顺序,成员按声明顺序 |
| 析构 | D 函数体 → M → B → A | 完全倒序(连父类之间的先后都翻过来) |
注意 [1] A 与 [2] B 的关系:它俩的顺序不是"谁在列表里先写",而是"谁在冒号后面先写"。
30 秒实验(本节最关键的一步):
1. 把 class D : public A, public B 改成 class D : public B, public A——只改这一处,别动其它任何东西。
2. 重新编译运行 → 输出里 A 和 B 的构造顺序整个对调(变成 [1] B 构造、[2] A 构造)。
3. 析构那边也跟着对调(变成先 A 后 B)。
4. 结论:顺序由"继承声明"决定——初始化列表、代码书写位置都改不了它。
5. 再做一个对照:在 D 的构造函数后面加上 : B(), A()(把列表顺序写成 B 在前,故意和声明顺序 A, B 相反),输出顺序依然不变。编译器可能给个 -Wreorder 警告,但不会改变执行顺序。
四、常见误区
- "初始化列表里写在前面就先构造"——不是。父类按继承声明顺序、成员按声明顺序(10.2.1 的老规矩,在多重继承里照旧)。列表顺序只是给人看的。
- "多个父类的析构顺序和构造顺序一样"——相反。构造 A→B,析构就是 B→A。"先进后出",和栈一模一样。
- "给父类传参的顺序会影响构造顺序"——不会。传参是"各传各的",顺序由声明定。
- "忘了在列表里给某个父类传参就没救了"——不至于:只要那个父类有无参构造,编译器会自己去调。真正会报错的是"父类只有带参构造、列表里却没点名它"(11.1.4 的
no matching function)。 - "多重继承顺序反了无所谓,反正都能跑"——有所谓。如果两个父类之间有依赖(A 初始化时要读 B 的状态),顺序反了就是用垃圾值初始化。这类 bug 表现为"偶尔对、偶尔错",极难查。
- "顺序只在构造时有影响"——析构也受影响,而且析构顺序反了更危险(可能"先拆了别人要用的东西")。构造和析构的顺序必须一起想。
自测三问(点开对答案)
1. `class D : public A, public B` 且有成员 `M m;`,构造和析构的完整顺序是什么?——**构造**:`A` → `B` → `m` → `D` 构造函数体。**析构**:`D` 析构函数体 → `m` → `B` → `A`(**完全倒序**)。 2. 决定多个父类构造先后的是什么?初始化列表的顺序会影响吗?——**继承声明顺序**(冒号后面从左到右)。**初始化列表的顺序完全不影响**(有些编译器会警告,但执行顺序不变)。 3. 什么时候"某个父类不在初始化列表里"会报错?——当**这个父类没有"无参构造"**时(比如它只写了带参构造,10.2)。报错是 **`no matching function for call to 'Base::Base()'`**;解药是在列表里**点名并传参**(11.1.4)。- ☐ 11.4 多态(下一节)
相关笔记
- 二义性 —— 上一节:同名成员与菱形继承
- 多态 —— 下一节:三大特点的第三个登场
- 构造函数的调用顺序(含实例01) —— 单继承版本的同一规律
- 子类显式调用父类的构造函数(含实例02) —— 列表里给父类传参
- 构造函数概述 —— "按声明顺序初始化"的原始规则
- 🔷 C++基础 · 索引 —— 全课路线图