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

继承=站在已有的类肩膀上再造一个新类。语法就一行: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):

判据一句话:能不能说"子类 是 父类"?说得通就继承,说不通就别硬继承。这条判据能挡住一大半滥用继承的设计。

四、这一章的路线图

第十一章要过六道关,这是全书逻辑最密的一章:

关 小节 一句话
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

读结果:

30 秒实验:把父类的 protected: 改成 private:,重新编译。 - 结果:strcpy(name, n); 那一行报错 'char Student::name' is private within this context。 - 这说明了 protected 存在的唯一理由:它是"给子类留的门"(11.1.2 会把三档权限在继承下的变化讲成一张表)。

六、常见误区

  1. "继承就是抄一份代码过来"——不是抄,是"真的拥有一份"。子类对象里确实存在父类的那部分数据(自成一个子对象),这也是为什么 sizeof 会相加、为什么构造/析构要按顺序跑(11.1.3)。
  2. "只要两个类有相似点就该继承"——要看是不是"是"的关系。有相似点但说不成"是一种"的,应该用成员(把对方当零件装进来),而不是继承。继承是全书最强的一种耦合:改了父类,所有子类都受影响。
  3. "子类能访问父类的一切"——不能。父类的 private 成员,子类永远碰不到(不论用哪种继承方式)。子类只能通过父类留下的 public / protected 接口去用它。
  4. "父类和子类的成员函数重名就是重载"——不是。那是隐藏(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(父类) + 自己新增的字节`(可能因对齐再加一点填充)。

相关笔记