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

第十一章讲的是"让类与类之间建立关系,并让同一个调用在不同对象上跑出不同结果"。六块内容:继承本体(11.1 组,语法、权限、构造顺序、隐藏同名函数)、运算符重载(11.2 组,让自定义类型也能用 + == <<)、多重继承与二义性(11.3 组)、多态(11.4 组,virtual 与动态绑定)、抽象类(11.5,纯虚函数定义接口)、收束(本节)。这一章把 9.2.3 的三大特点全部讲完了——从此你写的类不只是"各管一摊的零件",而是一批能替换、能扩展、能被统一调度的零件。

📝 本节和前面几节的关系

收束整章。往回看:第十章教会你"造一个类";这一章教会你"让类互相接替"。9.2.3 那句"封装/继承/多态",到这里三个都落地了。往前看:整章结尾留了三条"必须记住的工程习惯"(虚析构、override、少用多重继承)——它们比任何语法细节都更影响你写的代码能不能维护。

一、本章知识地图

块 小节 一句话要点
继承本体 11.1 / 11.1.1 / 11.1.2 / 11.1.3 / 11.1.4 / 11.1.5 class 子 : 继承方式 父 { };;class 默认 private 继承(必须显式写 public);父类 private 成员子类永远碰不到(但它在对象里);构造先父后子、析构先子后父;父类无参构造缺失就必须显式调父类构造;子类同名函数会"隐藏"父类的所有同名重载
运算符重载 11.2 / 11.2.1 / 11.2.2 / 11.2.3 / 11.2.4 运算符就是函数(a+b ≡ operator+(a,b));改不了优先级、不能发明新符号;= [] () -> 必须成员,<< >> 必须非成员;改自己的返回 T&、产生新值的返回 T;转换运算符 operator double()
多重继承 11.3 / 11.3.1 / 11.3.2 / 11.3.3 一列父类、每个都要写 public;同名成员 = 二义性(要 obj.A::x 指名);菱形继承 = 祖先有两份;构造顺序按继承声明顺序
多态 11.4 / 11.4.1 / 11.4.2 virtual → 运行时按对象真实身份选函数;三条件:指针/引用 + 虚函数 + 签名一致;基类析构必须虚;只能靠指针/引用,用对象接子类会"切片"
虚继承 11.4.3 class B : virtual public A → 祖先只留一份(解菱形);代价是对象变大、访问绕一层、虚基类最先构造且由最终派生类初始化
抽象类 11.5 virtual 返回类型 f() = 0; = 纯虚;含纯虚的类不能创建对象;子类实现全部纯虚才能实例化;抽象类 = 接口/契约

二、必须记住的八个结论

  1. class 子 : 父(不写继承方式)就是 private 继承——父类的 public 成员在子类里全被降级成 private,外界访问不到,而代码看起来"明明写了继承"。每次写继承,第一件事就是把 public 补上(11.1.1、11.1.2)。
  2. 构造"先父后子",析构"先子后父";完整顺序:父类 → 成员(按声明顺序)→ 自己的函数体;决定顺序的是"声明顺序",不是初始化列表的书写顺序(11.1.3、11.3.3)。
  3. 初始化列表是"给父类和成员传参"的唯一场所——因为它们在函数体开始之前就构造完了。函数体里写 Base(n); 只是造了个临时对象(11.1.4)。
  4. 子类同名函数"隐藏"父类的全部同名版本(跟参数是否相同无关)。解药:using Base::f;(一次引入全部)或 Base::f(参数)(指名调用)。隐藏是编译期的事,和 virtual 无关(11.1.5)。
  5. 运算符重载买的是"可读性":a * b + c 而不是 add(mul(a,b),c)。规则三条:不能发明新符号、不能改优先级/结合性、至少一个操作数是自定义类型。返回值:改自己 → T&;产生新值 → T(11.2 组)。
  6. virtual + 指针/引用 + 签名一致 = 动态绑定(多态)。差一个 const 就退化成隐藏 → 写 override 让编译器帮你抓错;基类析构必须 virtual,否则父类指针 delete 时子类析构不执行(内存泄漏)(11.4.1)。
  7. 多态必须走"指针或引用"。用父类对象去接子类对象会发生对象切片——多出来的成员被切掉,多态也随之消失(11.4.2)。
  8. 菱形继承(A→B/C→D)会产生两份祖先数据,且"只改一份"会造成静默的错误。正规解药是虚继承,但最好的解药是重新设计——能不改结构就不改,多重继承要慎用(11.3.2、11.4.3)。

三、动手 Demo:两关自测

第一关(写代码):一个能扩展的"形状库"

要求:用本章的东西搭一套形状体系。

项 要求
抽象基类 Shape,含纯虚 area() 和 name()(11.5)
虚析构 Shape 的析构必须是 virtual(11.4.1)
子类 Circle(半径)、Rect(宽高)、Square(边长,继承 Rect)
多态遍历 Shape *list[] 装三种形状,遍历打印"名字 + 面积"并累加总面积(11.4.2)
释放 用 delete 逐个释放,验证析构正确执行(11.4.1)

参考实现(自己先写,写不出来再看):

#include <iostream>
using namespace std;

// ───────── 抽象基类:只定契约 ─────────
class Shape {
public:
    virtual double      area() const = 0;      // 纯虚:子类必须实现
    virtual const char *name() const = 0;      // 纯虚
    virtual void        show() const {         // 普通虚函数:框架写在父类
        cout << name() << " 的面积 = " << area() << endl;
    }
    virtual ~Shape() { }                       // ★ 铁律:有虚函数就要虚析构
};

// ───────── 圆 ─────────
class Circle : public Shape {
private:
    double r;
public:
    Circle(double radius) : r(radius) { }
    double      area() const { return 3.14159 * r * r; }
    const char *name() const { return "圆"; }
};

// ───────── 矩形(正方形会继承它) ─────────
class Rect : public Shape {
protected:
    double w, h;                               // 留给子类用(11.1.2)
public:
    Rect(double width, double height) : w(width), h(height) { }
    double      area() const { return w * h; }
    const char *name() const { return "矩形"; }
};

// ───────── 正方形:借 Rect 的实现,只改名字 ─────────
class Square : public Rect {
public:
    Square(double s) : Rect(s, s) { }          // ★ 显式调用父类构造(11.1.4)
    const char *name() const { return "正方形"; }   // ★ 只需要重写 name
};

int main() {
    Shape *list[3];                            // 父类指针数组(11.4.2)
    list[0] = new Circle(1);
    list[1] = new Rect(3, 4);
    list[2] = new Square(2);

    double total = 0;
    for (int i = 0; i < 3; i++) {
        list[i]->show();                       // 同一句话,各显神通
        total += list[i]->area();
    }
    cout << "总面积 = " << total << endl;

    for (int i = 0; i < 3; i++) delete list[i];   // 虚析构保证正确释放
    return 0;
}

看到什么算成功:

圆 的面积 = 3.14159
矩形 的面积 = 12
正方形 的面积 = 4
总面积 = 19.1416

读结果:

如果这一关你能独立写出来:说明本章五大块(继承语法、权限、构造顺序、虚函数、抽象类)你已经能连起来用了。

加分实验(两个小实验,各验证一条规则):

实验一:抽象类连"切片"都不让你做

Shape sliced = *(list[1]);     // ★ 试图用父类对象去接

→ 编译错:cannot declare variable 'sliced' to be of abstract type 'Shape'。

有意思的地方在这里:11.4.2 里那个 Shape(area 有"保底实现"、返回 0)能编译出切片,只是结果不对;改成纯虚之后,编译器直接把这条路堵死了。"抽象类不能创建对象"这条规则,顺手帮你挡掉了一类错误。

实验二:加一种新形状,看要改多少代码

照着 Circle 的样子,自己加一个 Triangle(三条边 a、b、c,面积用海伦公式 √(s(s-a)(s-b)(s-c)),需要 #include <cmath>)。

加完之后回头数一数:你在 main 里改了几行?

第二关(找错):下面这段代码有八处问题

class Base {
public:
    int  id;                                                  // ①
    Base(int i) : id(i) { }
    void show() { cout << "Base " << id << endl; }            // ②
    ~Base() { }                                               // ③
};

class Derived : private Base {                                // ④
public:
    int id;                                                   // ⑤
    Derived() { }                                             // ⑥
    void show() { cout << "Derived " << id << endl; }         // ⑦
};

int main() {
    Derived d;
    Base *p = &d;                                             // ⑧
    delete p;
    return 0;
}

答案:

# 问题 为什么错 怎么改
① id 是 public 数据裸露在外,规则守不住(10.1.1) 挪到 private / protected(子类要用就 protected)
② show() 不是 virtual 想让"父类指针调用时各显神通",父类这一层就必须是虚的(11.4.1) 改成 virtual void show()
③ ~Base() 不是 virtual delete p 时子类的析构函数不会执行 → 子类申请的资源永久泄漏(11.4.1) 改成 virtual ~Base()
④ private 继承 Derived* 在外界不能转成 Base*(⑧ 的报错直接来自这里);Base 的 public 成员在 Derived 里也全变私有(11.1.2) 改成 class Derived : public Base
⑤ Derived 里又声明了一个 id 它把 Base::id 隐藏了——这是两个独立变量、各占内存(11.1.5),很容易改错一个 起个不同的名字(如 myId),或干脆别加
⑥ Derived() 没给父类传参 Base 只有带参构造,没有无参构造 → no matching function for call to 'Base::Base()'(11.1.4) 写成 Derived() : Base(0) { }
⑦ Derived::show 只是"隐藏"父类的,不是"重写" 父子类之间同名函数=隐藏(11.1.5),调用时按类型选,和多态无关 父类加 virtual,子类写 override
⑧ Base *p = &d; 编译不过 因为 Base 是 Derived 的 private 基类,main(外界)无权做这个向上转型(11.1.2) 继承改成 public(见 ④)

30 秒实验:把这八处逐条改对,改一处、编一次。特别留意:

四、常见误区(本章汇总)

  1. "继承就是把父类的代码抄一份"——不是抄,是"真的拥有一份":子类对象里存在父类的子对象、sizeof 相加、构造/析构按顺序跑、private 成员也在里面(只是碰不到)(11.1、11.1.1)。
  2. "不写继承方式就是 public"——class 默认 private、struct 默认 public。这是"代码看起来对、却报 inaccessible"的头号原因(11.1.1、11.1.2)。
  3. "子类写同名函数就是重载/重写"——父子之间叫"隐藏"(编译期按类型找名字)。参数不同也照藏。加了 virtual 且签名一致,才是"重写"(运行期按对象身份)(11.1.5、11.4.1)。
  4. "运算符重载越多越好"——只重载"符号含义天然贴切"的那些。Person + Person 这种"说不清语义"的,写注释解释的时候就已经错了(11.2.1)。
  5. "多重继承很强大,能多用就多用"——恰恰相反:每一次多重继承都要问"会不会形成菱形"。能改成"组合(把对象作为成员)"就别继承;必须用时,优先保证多个父类毫无关系(11.3)。
  6. "virtual 只要写在子类上就行"——必须从父类这一层开始虚。父类不虚、子类写 virtual,多态不成立(11.4 的实验)。反过来(父类虚、子类不写)是没问题的——虚性会遗传。
  7. "用父类对象接子类对象也能多态"——会切片。多态必须走指针或引用(11.4.2)。
  8. "抽象类只是'不能创建对象'的类,没什么用"——它的用处正是"不能创建对象":它保证"凡是这个类型的对象,都一定会 area()"——这是用父类指针统一处理一批对象的底气(11.5)。

五、写完这一章,你会什么了

这一章之后,你的"类"不再是孤立的零件,而是一族可以互相替换的零件。 具体来说,你已经能:

同时拿到三条能一直用的工程习惯:

习惯 出处 一句话
基类析构写 virtual 11.4.1 只要类可能被"父类指针 delete",就写
重写虚函数写 override 11.4.1 让编译器帮你抓"签名差一个 const"
继承前先问"是不是 is-a" 11.1、11.3 说不成就改用"组合";多重继承尤其要问"会不会菱形"
自测八问(点开对答案) 1. `class D : B { };` 是什么继承?会有什么后果?——**`private` 继承**(`class` 的默认)。后果:父类的 `public` / `protected` 成员在 `D` 里**全被降级成 `private`**,外界访问不到,**`D*` 也无法转成 `B*`**。 2. 构造与析构的完整顺序(含成员对象、多父类)?——**构造**:各父类(按**继承声明顺序**)→ 成员对象(按**声明顺序**)→ 自己的函数体。**析构**:完全倒序。 3. 父类没有无参构造时,子类必须做什么?——**在初始化列表里显式调用父类构造**(`Derived() : Base(参数) { }`),否则报 `no matching function for call to 'Base::Base()'`。**函数体里写不算**——那只是造了个临时对象。 4. "隐藏""重载""重写"分别发生在哪儿、什么时候定?——**重载**:同一作用域、参数不同、**编译期**;**隐藏**:父子类之间同名(**不论参数**)、**编译期**;**重写**:父子类之间同名同参数**且父类 `virtual`**、**运行期**。 5. 动态绑定的三个必要条件?——**① 通过指针或引用调用;② 函数是虚函数;③ 子类签名与父类完全一致**(含 `const`)。 6. 为什么基类析构函数必须写成 `virtual`?——**因为基类会被"父类指针 `delete`"**;`~Base` 不虚时,`delete p;` **只调 `~Base`**,子类析构不执行 → 子类申请的资源永久泄漏。 7. 菱形继承的问题是什么?正规解药是什么?——问题:**两条路径通向同一个祖先,祖先的数据在最终对象里有"两份"**(冗余 + 二义性,且"只改一份"会造成静默错误)。解药:**虚继承**(通向该祖先的每条路径都写 `virtual`);**虚基类最先构造,且由最终派生类负责初始化**。 8. 纯虚函数怎么写?抽象类的硬规则是什么?——写法:**`virtual 返回类型 函数名(参数) = 0;`**。硬规则:**含纯虚函数的类不能创建对象**;**子类必须实现全部纯虚函数才能创建对象**;**但可以定义指针和引用**(多态要用)。

相关笔记