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

继承之后多了一条铁律:构造"先父后子",析构"先子后父"——完全对称、完全相反。理由是常识:子类的那部分可能用到父类那部分的数据,所以父类必须先就位;销毁时反过来,子类先把自己多出来的东西收拾干净,再拆父类。另外要记住完整顺序:父类构造 → 成员对象构造(按声明顺序) → 子类自己的构造函数体。

📝 本节和前面几节的关系

10.2 讲了"构造函数的本职"(对象出生时自动跑)、10.3 讲了"析构函数的时机"(离开作用域就析构,且顺序与构造相反)。本节把这两条规律扩展到继承关系上:现在"构造"不再是"一个函数",而是一条链。11.1.4 会紧接着回答一个必然出现的问题——这条链上的父类构造,如果它需要参数,我该怎么办。

一、铁律:构造先父后子,析构先子后父

class Base {
public:
    Base()  { cout << "父类构造" << endl; }
    ~Base() { cout << "父类析构" << endl; }
};

class Derived : public Base {
public:
    Derived()  { cout << "子类构造" << endl; }
    ~Derived() { cout << "子类析构" << endl; }
};

创建 Derived d; 时,打印顺序是:

父类构造         ← ① 先把父类那部分造好
子类构造         ← ② 再补上子类自己的

子类析构         ← ③ 先拆自己多出来的部分
父类析构         ← ④ 再拆父类那部分

为什么是"先父后子"?

因为子类的那部分,建立在父类那部分之上。 举个具体例子:

class Base {
protected:
    int size;
public:
    Base() : size(10) { }
};

class Derived : public Base {
    int *arr;
public:
    Derived() {
        arr = new int[size];        // ★ 如果 Base 构造还没跑,size 就是垃圾值!
    }
};

如果子类先构造,size 还是个没初始化的垃圾值——new int[size] 就会申请一块乱七八糟大小的内存。"先父后子"不是约定,是必需的。

为什么析构反过来? 同样的道理倒着说:arr 是子类申请的资源,必须在父类那部分还在的时候释放(万一释放逻辑依赖父类的数据)。"后进先出"——和栈一样(10.3 讲局部对象析构顺序时就是这个规律)。

二、完整顺序:父类 → 成员 → 自己的函数体

很多人以为"构造函数"就是"函数体",其实函数体是最后才跑的。完整顺序:

步骤 做什么 顺序由谁决定
① 父类(基类)子对象的构造 父类的构造函数(11.1.4 讲怎么给它传参)
② 子类自己的成员对象(对象成员)的构造 成员在类里的声明顺序(不是初始化列表里写的顺序!10.2.1 的老规则)
③ 子类自己的构造函数体 你自己写的那些语句

一句话记住:"先造爹,再造零件,最后自己动手"。

三、如果父类没有无参构造会怎样

这是本节最实用的一个考点。

规则:子类的初始化列表里没有写父类的构造时,编译器会自动去调父类的"无参构造"(默认构造)。

于是分两种情况:

父类的情况 结果
父类有无参构造(或者编译器生成的那个) ✅ 编译通过,父类子对象被"空手"构造出来
父类没有无参构造(比如只写了带参构造,10.2 的老坑) ❌ 编译错:no matching function for call to 'Base::Base()'

第二种情况的解药:在子类的初始化列表里显式调用——这就是下一节 11.1.4。

先看一眼那个错脸:

class Base {
public:
    Base(int n) { }            // 只写了带参构造 → 编译器不再送无参构造(10.2)
};

class Derived : public Base {
public:
    Derived() { }              // ← 这里会报错
};
error: no matching function for call to 'Base::Base()'
note: candidate: Base::Base(int)
note:   candidate expects 1 argument, 0 provided

报错信息里 candidate expects 1 argument, 0 provided 这行特别有用——它直接告诉你"父类只有带参构造,你得给它一个参数"。

四、动手 Demo(含实例01:构造函数的调用顺序)

实例01 的现场:教材用这个例子展示"父类、成员对象、子类"三者的构造/析构先后。这里把它做成"编号打印",一眼看清顺序。

#include <iostream>
using namespace std;

class Base {
public:
    Base()  { cout << "[1] Base 构造" << endl; }
    ~Base() { cout << "[6] Base 析构" << endl; }
};

class Member {                        // 用来当子类的"成员对象"
public:
    Member()  { cout << "[2] Member 构造" << endl; }
    ~Member() { cout << "[5] Member 析构" << endl; }
};

class Derived : public Base {
private:
    Member m;                         // ← 成员对象:在父类之后、函数体之前构造

public:
    Derived()  { cout << "[3] Derived 构造函数体" << endl; }
    ~Derived() { cout << "[4] Derived 析构函数体" << endl; }
};

int main() {
    cout << "── 进入作用域,创建 d ──" << endl;
    {
        Derived d;
        cout << "── d 还在世 ──" << endl;
    }
    cout << "── d 已销毁 ──" << endl;
    return 0;
}

看到什么算成功:

── 进入作用域,创建 d ──
[1] Base 构造
[2] Member 构造
[3] Derived 构造函数体
── d 还在世 ──
[4] Derived 析构函数体
[5] Member 析构
[6] Base 析构
── d 已销毁 ──

读结果(把 6 行读成两句话):

方向 顺序
构造(1→2→3) 父类 → 成员 → 自己的函数体
析构(4→5→6) 自己的函数体 → 成员 → 父类(完全倒过来)

注意 [4] 的位置:析构是从子类自己的析构函数体开始的——因为"自己那部分"是最外层的活儿,先干。这就是"先子后父"在代码里的样子。

30 秒实验: 1. 把 Base() 改成 Base(int)(删掉无参版本),只改这一处。 2. 重新编译:Derived() { ... } 立刻报 no matching function for call to 'Base::Base()'。 3. 这一条报错就是"父类构造必须被显式调用"的入场券——翻开下一节(11.1.4),你会看到它怎么被治好。

五、常见误区

  1. "子类构造函数的函数体先跑,跑的时候顺便把父类也初始化了"——恰好相反。父类子对象、成员对象,都在函数体之前就构造完了;函数体是最后执行的。所以在函数体里"初始化"父类/成员是徒劳的(11.1.4 会说明这一点为什么重要)。
  2. "析构顺序和构造顺序一样"——相反。构造:父→成员→自己;析构:自己→成员→父。一辈子都遵守"栈的后进先出"。
  3. "父类没有无参构造也不影响我想要一个子类的无参构造"——影响很大。要么在初始化列表里给父类传参(11.1.4),要么父类自己补一个无参构造。没有第三条路。
  4. "成员对象在初始化列表里写在前面的先构造"——不是。按成员在类里的声明顺序构造(10.2.1 的铁律,在继承下依然成立)。初始化列表里的顺序只是"写法顺序",不决定"执行顺序"。
  5. "父类构造函数里调用虚函数(11.4)能拿到子类的版本"——不能。构造父类时子类那部分还不存在,虚函数机制还没生效。这是个深坑,本章末尾会以"知道有这回事"的方式提一下。
自测三问(点开对答案) 1. 创建一个子类对象时,构造与析构的完整顺序是什么?——**构造**:父类子对象 → 子类的成员对象(按**声明顺序**)→ 子类自己的构造函数体。**析构**:子类析构函数体 → 成员对象 → 父类子对象(**完全倒序**)。 2. 为什么必须"先构造父类"?——因为**子类那部分可能要用父类的数据**(`new int[size]` 里 `size` 来自父类)。如果父类后构造,子类拿到的就是**垃圾值**。 3. 父类只写了带参构造时,子类写 `Derived() { }` 会报什么错?——`no matching function for call to 'Base::Base()'`,并提示 `candidate expects 1 argument, 0 provided`。**解药是在初始化列表里写 `: Base(参数)`**(11.1.4)。

相关笔记