继承之后多了一条铁律:构造"先父后子",析构"先子后父"——完全对称、完全相反。理由是常识:子类的那部分可能用到父类那部分的数据,所以父类必须先就位;销毁时反过来,子类先把自己多出来的东西收拾干净,再拆父类。另外要记住完整顺序:父类构造 → 成员对象构造(按声明顺序) → 子类自己的构造函数体。
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),你会看到它怎么被治好。
五、常见误区
- "子类构造函数的函数体先跑,跑的时候顺便把父类也初始化了"——恰好相反。父类子对象、成员对象,都在函数体之前就构造完了;函数体是最后执行的。所以在函数体里"初始化"父类/成员是徒劳的(11.1.4 会说明这一点为什么重要)。
- "析构顺序和构造顺序一样"——相反。构造:父→成员→自己;析构:自己→成员→父。一辈子都遵守"栈的后进先出"。
- "父类没有无参构造也不影响我想要一个子类的无参构造"——影响很大。要么在初始化列表里给父类传参(11.1.4),要么父类自己补一个无参构造。没有第三条路。
- "成员对象在初始化列表里写在前面的先构造"——不是。按成员在类里的声明顺序构造(10.2.1 的铁律,在继承下依然成立)。初始化列表里的顺序只是"写法顺序",不决定"执行顺序"。
- "父类构造函数里调用虚函数(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)。- ☐ 11.1.4 子类显式调用父类的构造函数(下一节)
相关笔记
- 继承后的可访问性 —— 上一节:三档权限在继承下的变化
- 子类显式调用父类的构造函数(含实例02) —— 下一节:给父类构造传参
- 构造函数 —— "写了带参构造就不再送默认构造"的老坑
- 构造函数概述 —— 初始化列表与"按声明顺序初始化"
- 析构函数 —— 析构顺序与构造相反
- 🔷 C++基础 · 索引 —— 全课路线图