语法加一处就行:子类(参数表) : 父类名(参数表) { }——父类的构造调用,写在初始化列表里,和初始化成员变量并列。它不能写在函数体里,因为父类子对象在函数体开始执行之前就已经构造完了(11.1.3 的顺序表)。配上这一条,初始化列表的完整含义是:"先给父类子对象传参,再给自己的成员对象传参,最后才是函数体"。
上一节 11.1.3 留了一个必须回答的问题:父类的构造函数需要参数时,谁来给? 本节就是答案。同时它把 10.2.1 的初始化列表升级了:原来的列表里只有"成员变量",现在多了"父类"这一项,而且父类永远排在真实执行顺序的最前面。
一、语法:初始化列表里多一项
class 子类名 : 继承方式 父类名 {
public:
子类名(参数表) : 父类名(参数表1), 成员1(参数2), 成员2(参数3) {
// 构造函数体(最后才跑)
}
};
对照 10.2.1 的老写法看一眼:
// 没有继承时(10.2.1)
Point(int a, int b) : x(a), y(b) { }
// 有了继承之后:父类也进列表
Student(const char *n, int a, int s) : Person(n, a), score(s) { }
// ↑ 父类在这 ↑ 成员在这
一行里两种东西:没有 :: 前缀的、是一个类名 → 那是在调父类构造;是个成员名 → 那是在调成员的构造(或是给内置类型赋值)。编译器分得清,你也得看得懂。
二、为什么必须写在初始化列表里
因为顺序。回忆 11.1.3 的三步:
① 父类子对象构造 ← 函数体还没开始
② 成员对象构造
③ 子类构造函数体 ← 到这里父类早就造好了
如果你在函数体里写"调用父类构造":
Derived(int n) {
Base(n); // ❌ 这不是"调用父类构造"!
}
它做了什么? 它创建了一个临时的 Base 对象、用完立刻销毁——跟"父类子对象"毫无关系。父类子对象早就用无参构造(或默认构造)造好了。 这句话不但没用,还会让你误以为"传参成功了"。
这就是为什么"位置"在 C++ 里是硬规则:要在父类子对象出厂之前告诉它参数——只能通过初始化列表。
三、真实执行顺序(列表里写多乱都没用)
class Derived : public Base {
Member1 m1;
Member2 m2;
public:
// 列表里我把 m2 写在 m1 前面,把父类写在最后——真实顺序变了吗?没有。
Derived() : m2(2), m1(1), Base(100) { }
};
真实执行顺序永远是:
① Base ← 父类永远第一
② m1 ← 然后按"类里的声明顺序"(m1 声明在 m2 前面)
③ m2
④ 函数体
两条独立的老规矩,在这里叠在一起:
| 规矩 | 出处 |
|---|---|
| 父类子对象永远最先构造 | 11.1.3 |
| 成员按"声明顺序"构造,不按"列表顺序" | 10.2.1 |
但实践建议:列表里的书写顺序,尽量和真实顺序一致(父类 → 成员按声明顺序 → 自己)。编译器在顺序不一致时会给出警告(-Wreorder),虽不报错,但会让人误读。
四、多个父类、多个成员时的完整样子
class Derived : public A, public B { // 继承两个
Member m;
public:
// 真实顺序:A → B(按继承声明顺序)→ m → 函数体
Derived() : B(2), A(1), m(3) { }
};
多个父类时,"谁先谁后"由继承声明顺序决定,不由列表顺序决定——这条在 11.3.3 会专门做成实例。
五、动手 Demo(含实例02:子类显式调用父类的构造函数)
#include <iostream>
#include <cstring>
using namespace std;
class Person {
protected:
char name[20];
int age;
public:
// 只写了带参构造 → 编译器不再送无参构造(10.2)
Person(const char *n, int a) : age(a) {
strcpy(name, n);
cout << "[1] Person 构造:" << name << "," << age << " 岁" << endl;
}
~Person() { cout << "[4] Person 析构:" << name << endl; }
void show() const {
cout << name << "(" << age << " 岁)";
}
};
class Student : public Person {
private:
int score;
public:
// ★ 显式调用父类的带参构造:写在初始化列表里(实例02)
Student(const char *n, int a, int s) : Person(n, a), score(s) {
cout << "[2] Student 构造:成绩 " << score << endl;
}
~Student() { cout << "[3] Student 析构" << endl; }
void showAll() const {
show(); // 借父类的 show()
cout << "成绩 " << score << endl;
}
};
int main() {
Student s("小红", 20, 95);
s.showAll();
return 0;
}
看到什么算成功:
[1] Person 构造:小红,20 岁
[2] Student 构造:成绩 95
小红(20 岁)成绩 95
[3] Student 析构
[4] Person 析构:小红
读结果:
[1]早于[2]——父类的参数是在进入子类构造函数体之前就传进去的。[4]晚于[3]——析构依然是"先子后父"。
30 秒实验:
1. 把 : Person(n, a), score(s) 里的 Person(n, a), 删掉,只留 : score(s)。
2. 重新编译,你会看到:no matching function for call to 'Person::Person()'。
3. 再加回去,就好了。 这删一加,"父类没有无参构造时,子类必须显式调用"就成了条件反射。
4. 再试个反向的:把父类改成同时有两个构造——Person(const char *n, int a) 和 Person()。这时删掉 Person(n, a) 也能编过(编译器去找无参那个),但那次"无参构造"里 name 是空的、age 是垃圾值。能编过,不代表你要的结果对。
六、常见误区
- "在函数体里写
父类名(参数);就是调用父类构造"——错。那是造了一个临时对象,随即销毁。父类子对象在函数体开始前就已经造好了,你改不了。必须写进初始化列表。 - "初始化列表里把父类写在最后,父类就会最后构造"——不会。父类永远最先构造,与列表里的书写位置无关。列表的顺序只是给人看的,真实顺序由"继承声明顺序 + 成员声明顺序"决定。
- "父类没有无参构造,我在子类里补一个就行了"——不行。父类的构造函数只能在父类里加(子类不能"补充"父类)。子类能做的只有两件:在初始化列表里传参,或者干脆不要在子类里提供无参构造。
- "父类那个构造是
protected的,子类还能调吗"——能。protected构造对"子孙"是开放的,这正是"只允许被子类继承、不允许直接创建对象"这一设计惯用手法(和 11.5 的抽象类思路相通)。 - "构造函数里能调用虚函数(11.4)拿到子类版本"——不能(11.1.3 提过)。父类构造时子类的那部分还没造出来,虚函数机制还没接上。这条在工程上会引发很诡异的 bug,现在先记住结论。
自测三问(点开对答案)
1. 子类显式调用父类构造函数的语法是什么?——写在**初始化列表**里:`子类(参数) : 父类名(参数) { }`。可以和其他成员初始化并列,用逗号分隔。 2. 为什么不能把它写在函数体里?——**因为父类子对象在函数体执行之前就已经构造完成**(11.1.3)。函数体里写 `Base(n);` 只是**造了一个临时对象**,跟真正的父类子对象无关。 3. 初始化列表里 `Base(...)`、成员变量、成员对象的**真实**初始化顺序由什么决定?——**父类子对象永远第一**(按继承声明顺序);然后是**成员,按它们在类里的"声明顺序"**(不按列表顺序);最后才是**子类构造函数体**。- ☐ 11.1.5 子类隐藏父类的成员函数(下一节)
相关笔记
- 构造函数的调用顺序(含实例01) —— 上一节:三者的构造/析构顺序
- 子类隐藏父类的成员函数(含实例03) —— 下一节:同名函数引起的"隐藏"
- 构造函数概述 —— 初始化列表的原始规则
- 构造函数 —— 写了带参构造就不再送默认构造
- 析构函数 —— 析构顺序与构造相反
- 🔷 C++基础 · 索引 —— 全课路线图