继承=站在已有的类肩膀上再造一个新类。语法就一行:class 子类 : 继承方式 父类 { };。它解决的是一件很具体的事——两个类有大量重复代码,那就把共同的部分留在父类,各自只写自己特有的。9.2.3 说的三大特点,第二个(继承)从这里正式登场。
第十章学会了"造一个类"(class、构造、析构、this、友元、命名空间);从本节起,类与类之间开始发生关系。本节要回收两笔旧账:9.2.3 的三大特点(封装/继承/多态——继承是第二个)、8.1.4 的结构体嵌套(那是"把一个结构体装进另一个",继承是"一个类是另一个类")。另外,第十章埋下的 protected(10.1.1 说"留给继承用")马上就要派上用场。
一、为什么需要继承:重复代码的痛
先看没有继承时的现场。要写一个"学生"和一个"研究生":
class Student {
private:
char name[20];
int score;
public:
void set(const char *n, int s) { /* ... */ }
void show() const { /* ... */ }
};
class GraduateStudent { // 研究生也是学生
private:
char name[20]; // ← 抄了一遍
int score; // ← 抄了一遍
char tutor[20]; // 只有这一行是新的
public:
void set(const char *n, int s) { /* ... */ } // ← 又抄一遍
void show() const { /* ... */ } // ← 又抄一遍
};
问题不在"多打了几行字",而在于"改一处要改多处":
| 麻烦 | 具体后果 |
|---|---|
| 重复 | 同样两个成员、同样两个函数,写了两遍 |
| 容易漏改 | 学生类里 score 改了名字,忘了改研究生类 → 两个类悄悄不一致 |
| 逻辑分散 | "学生到底长什么样"这件事,散落在两个类里 |
继承要做的事:把 name、score、set、show 只写在"学生"里,然后说一句"研究生是一种学生,只是多了导师"。
二、语法:一行就够了
class 子类名 : 继承方式 父类名 {
// 只写"自己特有的"成员
};
把上面的例子改写成继承:
class Student { // 父类:写"共同的"
protected: // ★ 不是 private,是为了让子类能用
char name[20];
int score;
public:
void set(const char *n, int s) { /* ... */ }
void show() const { /* ... */ }
};
class GraduateStudent : public Student { // 子类:只写"不同的"
private:
char tutor[20]; // 研究生特有的:导师
public:
void setTutor(const char *t) { /* ... */ }
};
注意 GraduateStudent 里没有 name、没有 score、没有 set()、没有 show()——但它全都有,都是从 Student 那里继承来的。
三、父子的三种叫法
| 叫法 | 常见于 |
|---|---|
| 父类 / 基类(base class) | 教材、口语 |
| 子类 / 派生类(derived class) | 教材、口语 |
| 基类 → 派生类 | 标准文档、报错信息 |
报错信息里出现 base class、derived class,说的就是这两个角色。
这层关系的本质是一个"是"字(is-a):
- 研究生 是 学生 ✅(所以可以继承)
- 汽车 是 交通工具 ✅
- 发动机 是 汽车 ❌(发动机是汽车的零件,不是"一种汽车"——那应该用"包含一个成员",也就是 8.1.4 的嵌套搞法)
判据一句话:能不能说"子类 是 父类"?说得通就继承,说不通就别硬继承。这条判据能挡住一大半滥用继承的设计。
四、这一章的路线图
第十一章要过六道关,这是全书逻辑最密的一章:
| 关 | 小节 | 一句话 |
|---|---|---|
| 1. 继承本体 | 11.1 组 | 怎么写、权限怎么变、构造顺序、怎么调父类构造、同名函数怎么办 |
| 2. 运算符重载 | 11.2 组 | 让自己造的类型也能用 + - * == << |
| 3. 多重继承 | 11.3 组 | 一个类继承多个父类;以及随之而来的"二义性" |
| 4. 多态 | 11.4 组 | virtual——"同一句话,不同反应"(三大特点的第三个) |
| 5. 抽象类 | 11.5 | 只规定"必须有什么",不许创建对象 |
| 6. 收束 | 11.6 | 小结与自测 |
五、动手 Demo:学生 → 研究生
#include <iostream>
#include <cstring>
using namespace std;
class Student {
protected: // 10.1.1 的伏笔:留给子类用
char name[20];
int score;
public:
void set(const char *n, int s) {
strcpy(name, n);
score = s;
}
void show() const {
cout << name << " 成绩 " << score;
}
};
// ★ 关键一行:GraduateStudent 继承 Student
class GraduateStudent : public Student {
private:
char tutor[20]; // 研究生特有的
public:
void setTutor(const char *t) { strcpy(tutor, t); }
void showAll() const {
show(); // ← 直接调父类的 show(),不用写 Student::
cout << ",导师 " << tutor << endl;
}
};
int main() {
Student s;
s.set("小明", 90);
s.show();
cout << endl;
GraduateStudent g;
g.set("小红", 95); // ← 用的是父类的 set()
g.setTutor("王教授");
g.showAll();
cout << "sizeof(Student) = " << sizeof(Student) << endl;
cout << "sizeof(GraduateStudent) = " << sizeof(GraduateStudent) << endl;
return 0;
}
看到什么算成功:
小明 成绩 90
小红 成绩 95,导师 王教授
sizeof(Student) = 24
sizeof(GraduateStudent) = 44
读结果:
g.set("小红", 95)能跑——说明父类的公有函数被完整继承了。showAll()里直接写show()能跑——说明子类内部调用父类函数不用加Student::(加了也行,见 11.1.5)。- 44 = 24 + 20——子类对象里先放父类那一份(24 字节),再放自己的(导师,20 字节)。这条规律很重要:"子类对象里,父类那一部分永远排在最前面"。
30 秒实验:把父类的 protected: 改成 private:,重新编译。
- 结果:strcpy(name, n); 那一行报错 'char Student::name' is private within this context。
- 这说明了 protected 存在的唯一理由:它是"给子类留的门"(11.1.2 会把三档权限在继承下的变化讲成一张表)。
六、常见误区
- "继承就是抄一份代码过来"——不是抄,是"真的拥有一份"。子类对象里确实存在父类的那部分数据(自成一个子对象),这也是为什么
sizeof会相加、为什么构造/析构要按顺序跑(11.1.3)。 - "只要两个类有相似点就该继承"——要看是不是"是"的关系。有相似点但说不成"是一种"的,应该用成员(把对方当零件装进来),而不是继承。继承是全书最强的一种耦合:改了父类,所有子类都受影响。
- "子类能访问父类的一切"——不能。父类的
private成员,子类永远碰不到(不论用哪种继承方式)。子类只能通过父类留下的public/protected接口去用它。 - "父类和子类的成员函数重名就是重载"——不是。那是隐藏(11.1.5),规则和后果跟重载完全不同,是本章最容易翻车的地方之一。
自测三问(点开对答案)
1. 继承的语法是什么?`class` 忘记写继承方式时,默认是哪种?——`class 子类名 : 继承方式 父类名 { };`。**`class` 默认是 `private` 继承,`struct` 默认是 `public` 继承**(和默认成员权限的规律一致,8.1.1、10.1.1)。 2. 怎么判断"A 该不该继承 B"?——问一句"**A 是一种 B** 吗"。说得通才继承;只是"用到"或"包含",应该写成 **B 类型的成员**。 3. 子类对象的内存里,父类那部分数据排在哪儿?——**排在最前面**,子类自己新增的成员跟在后面。所以 `sizeof(子类) ≥ sizeof(父类) + 自己新增的字节`(可能因对齐再加一点填充)。- ☐ 11.1.1 类的继承(下一节)