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

动态绑定最经典的用法:用一个"父类指针数组"装下各种子类对象,然后一次遍历、各显神通。关键是 list[i]->area() 里的 area 是虚函数——编译器不管 list[i] 是圆还是矩形,先记一笔"运行时再查",于是每个对象都调到自己那一版。这带来可扩展:加新形状只写新类,遍历的代码一行不改。另有一个必须知道的陷阱:用"父类对象"(而不是指针/引用)去接子类对象,会发生"对象切片",多态直接失效。

📝 本节和前面几节的关系

11.4.1 讲了虚函数的机制和三个条件,本节把它用起来。这是 9.2.3"多态"那一栏的真正落地——那时只有一句话"同一句话,不同反应",现在你能写出完整可跑的例子。这里还会用到 7.4.1 指针、8.4 结构体数组(现在是"对象指针数组")。

一、动态绑定怎么"用":父类指针数组

思路三步:

① 定义一个"父类"(Shape),把"会变的行为"做成虚函数(area、show)
② 让各个子类(Circle、Rect)重写这些虚函数
③ 用"Shape* 数组"把它们装在一起,统一遍历调用
Shape *list[3];
list[0] = new Circle(1);
list[1] = new Rect(3, 4);
list[2] = new Circle(2);

for (int i = 0; i < 3; i++)
    cout << list[i]->area() << endl;      // ★ 同一个写法,三种结果

为什么能成立:Circle* 和 Rect* 都能隐式转成 Shape*(子类"是一种"父类,11.1)——数组里存的其实都是 Shape*,但它们各自指向的对象"身份证"不同。area() 是虚的,调用时按对象的真实身份查表(11.4.1 的机制),于是各调各的。

二、它买来的东西:可扩展

对照看:

没有虚函数(各写各的循环) 有虚函数(一个循环)
处理 3 种形状 3 个循环 / 3 个分支 1 个循环
新增第 4 种形状 改遍历的代码(加循环/加分支) 只写新类,遍历代码不动
遍历代码要不要重新编译 要 不要

第二行是本节的灵魂:"加新东西不用动老代码"——这就是面向对象"可扩展"的具体含义,也是虚函数存在的唯一理由。

三、动手 Demo(含实例06:利用虚函数实现动态绑定)

#include <iostream>
using namespace std;

// ───── 父类:Shape ─────
class Shape {
public:
    // ① "会变的行为"做成虚函数。父类先给一个"保底"实现
    virtual double area() const {
        return 0;
    }

    // ② 这个函数写一次,所有子类都受益(它内部调的就是虚函数 area)
    virtual void show() const {
        cout << "面积 = " << area() << endl;
    }

    virtual ~Shape() { }        // ★ 11.4.1 的铁律:有虚函数就要虚析构
};

// ───── 子类 1:圆 ─────
class Circle : public Shape {
private:
    double r;
public:
    Circle(double radius) : r(radius) {}
    double area() const { return 3.14159 * r * r; }      // 重写
};

// ───── 子类 2:矩形 ─────
class Rect : public Shape {
private:
    double w, h;
public:
    Rect(double width, double height) : w(width), h(height) {}
    double area() const { return w * h; }                // 重写
};

int main() {
    // ★ 一个"父类指针数组",里面装的是不同种类的形状
    Shape *shapes[3];
    shapes[0] = new Circle(1);       // 半径 1
    shapes[1] = new Rect(3, 4);      // 3×4
    shapes[2] = new Circle(2);       // 半径 2

    double total = 0;
    for (int i = 0; i < 3; i++) {
        cout << "第 " << (i + 1) << " 个形状:";
        shapes[i]->show();           // ★ 同一句话,各显神通(动态绑定)
        total += shapes[i]->area();
    }
    cout << "总面积 = " << total << endl;

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

看到什么算成功:

第 1 个形状:面积 = 3.14159
第 2 个形状:面积 = 12
第 3 个形状:面积 = 12.5664
总面积 = 27.708

读结果:

现象 为什么
同一个 shapes[i]->show() 打出三种不同面积 show() 里调的是虚函数 area(),运行时按对象身份查到各自的 area
show() 只在父类写了一次 "不变的框架"写在父类,"会变的部分"交给子类——这是继承 + 多态的标准分工
total = 27.708 = 3.14159 + 12 + 12.5664 遍历里 area() 每次都调对了版本

30 秒实验(这一步能让你彻底分清"虚"与"不虚"): 1. 把 Shape 里的 virtual double area() const 去掉 virtual,重新编译运行。 2. 结果:三次都打印"面积 = 0"——因为现在 area 不是虚的,shapes[i]->area() 按 Shape* 的类型调到了父类那个"保底返回 0"的版本。 3. 加回 virtual,立刻恢复正确。 4. 试着只给最末的 } 前加 virtual?不行——虚函数必须从"父类这一层"开始声明(11.4 的实验试过)。

四、必须知道的陷阱:对象切片

下面这段代码"能编过",但结果一定不是你要的:

int main() {
    Circle c(1);

    Shape s = c;              // ★ 注意:不是指针、不是引用,而是"父类对象"
    cout << s.area() << endl; // 输出 0,不是 3.14159!

    Shape &r = c;             // ← 引用(可以)
    cout << r.area() << endl; // 输出 3.14159 ✅
}

为什么:Shape s = c; 这行的意思是"造一个 Shape 对象,用 c 去初始化它"——Shape 只有 Shape 那点内存,装不下 Circle 多出来的 r。于是 Circle 的部分被切掉了(只留下 Shape 那一部分)。这个对象是"货真价实的 Shape",不是"缩水的 Circle" → 调 area() 自然是 Shape::area()(返回 0)。

一句话记住:

要享受多态,就必须"用指针或引用"。一旦用"对象"去装,多态立刻消失,而且数据被切掉。

Shape s = c; 和 Shape &r = c; 的差别,就是"复印一张身份证"和"指着一个真人"的差别。

五、常见误区

  1. "用父类对象接子类对象也能多态"——不能,而且会切片(上一节的 Shape s = c;)。多态必须走指针或引用(11.4.1 的条件①)。
  2. "父类那个虚函数不需要写实现"——本节里写了一个"保底实现"(返回 0),是为了让父类能单独使用。如果你希望父类"根本不能被创建对象",就该把它写成纯虚函数——那是 11.5 抽象类的事。
  3. "数组里存的指针可以直接按子类类型用"——不行。shapes[i] 的静态类型是 Shape*,你只能调用 Shape 里有的成员。子类新增的函数(比如 Rect::setW)得先转成 Rect* 才能用。多态只解决"同名虚函数调谁",不解决"能看见哪些函数"。
  4. "遍历里 delete 不 delete 都行"——必须 delete(否则内存泄漏),而且基类析构必须是 virtual(11.4.1),否则子类析构不执行。本节 Demo 结尾的 delete shapes[i]; 不是装饰,是必需。
  5. "每种形状都在父类里写一个 if 分支判断类型更好懂"——那是把多态退化成 switch。每次加类型都要改那个分支——又把"可扩展"还回去了。"用虚函数还是用 if 判断类型"是判断一个设计好不好最灵的一条线索。
自测三问(点开对答案) 1. 怎么用虚函数实现"同一句话,不同反应"?——把**会变的行为**做成虚函数放在父类;子类各自重写;然后**用父类指针(或引用)数组**装各种子类对象,统一遍历调用。 2. 为什么必须用"指针或引用"而不能用"对象"?——因为**用父类对象去接子类对象会发生"对象切片"**:只复制父类那部分数据,多出来的子类成员被切掉,**这个对象就真的是父类对象了**,多态也随之消失。 3. 新增一种形状时,需要改动什么?——**只写新类**(继承 `Shape`、重写 `area()`、`show()` 自动跟着对)。**遍历的代码、父类、已有子类一行都不用改**——这就是虚函数买来的"可扩展"。

相关笔记