🔷 第十一章 继承与派生
📄 一句话概括

多个父类时,"谁先构造"由 继承声明里的书写顺序决定——从左到右,和初始化列表里写的顺序完全无关。完整顺序是:各父类(按声明顺序)→ 自己的成员对象(按声明顺序)→ 自己的构造函数体;析构一律反过来。所以:写下 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 警告,但不会改变执行顺序。

四、常见误区

  1. "初始化列表里写在前面就先构造"——不是。父类按继承声明顺序、成员按声明顺序(10.2.1 的老规矩,在多重继承里照旧)。列表顺序只是给人看的。
  2. "多个父类的析构顺序和构造顺序一样"——相反。构造 A→B,析构就是 B→A。"先进后出",和栈一模一样。
  3. "给父类传参的顺序会影响构造顺序"——不会。传参是"各传各的",顺序由声明定。
  4. "忘了在列表里给某个父类传参就没救了"——不至于:只要那个父类有无参构造,编译器会自己去调。真正会报错的是"父类只有带参构造、列表里却没点名它"(11.1.4 的 no matching function)。
  5. "多重继承顺序反了无所谓,反正都能跑"——有所谓。如果两个父类之间有依赖(A 初始化时要读 B 的状态),顺序反了就是用垃圾值初始化。这类 bug 表现为"偶尔对、偶尔错",极难查。
  6. "顺序只在构造时有影响"——析构也受影响,而且析构顺序反了更危险(可能"先拆了别人要用的东西")。构造和析构的顺序必须一起想。
自测三问(点开对答案) 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)。

相关笔记