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

多态="同一句话,不同反应"——同一个函数调用,作用在不同对象上,跑出不同的结果。9.2.3 说的三大特点,第三个(多态)从这里正式登场。C++ 的多态有两条路:编译期多态(重载、模板——编译时就定好了调哪个)和运行期多态(虚函数——跑起来才知道调哪个)。本章讲的是第二条,它是"可扩展"这件事的技术核心。

📝 本节和前面几节的关系

这是这一章的高潮。往回看:11.1.5 讲了"子类同名函数会隐藏父类的"——那是编译期按类型找名字;本节开始讲 virtual,让"同名函数"能按对象的真实身份在运行期选——同样是同名函数,加一个词,行为天翻地覆。往前看:11.4.1 讲虚函数机制,11.4.2 用实例06演示动态绑定,11.4.3 用实例07解决 11.3.2 的菱形问题,最后 11.5 的抽象类完全建立在虚函数之上。

一、先看一个"奇怪"的现象

这段代码,先猜输出什么:

#include <iostream>
using namespace std;

class Base {
public:
    void hello() { cout << "我是 Base" << endl; }     // 注意:没有 virtual
};

class Derived : public Base {
public:
    void hello() { cout << "我是 Derived" << endl; }
};

int main() {
    Derived d;
    Base *p = &d;            // 父类指针指向子类对象(合法:子类"是一种"父类,11.1)

    p->hello();              // 猜猜打印哪个?
    d.hello();
    return 0;
}

实际输出:

我是 Base
我是 Derived

同一个对象 d,用两种方式叫它,行为不一样:

这算"多态"吗?不算——这是"没有多态"的样子。 因为编译器在编译时就把 p->hello() 定死成 Base::hello 了(它只看 p 的类型,不看 p 实际指向谁)。这叫静态绑定(早绑定)。

要让"同一句话,不同反应"成立,需要在 Base::hello 前面加一个词:

class Base {
public:
    virtual void hello() { cout << "我是 Base" << endl; }
    //  ↑ 就加这一个词
};

加了之后,p->hello() 输出变成 我是 Derived——编译器改口了:"我不知道 p 到底指谁,等运行时再看"。这就是虚函数,也是动态绑定(晚绑定)。

这个"一个词的差别"就是本节到 11.5 的全部内容。

二、两种多态,一张表分清

编译期多态(静态多态) 运行期多态(动态多态)
靠什么实现 函数重载(6.5)、模板 虚函数(virtual)
什么时候定 编译时(编译器看着代码就能定) 运行时(要看对象真实身份)
调用形态 直接按实参/类型选版本 通过指针或引用调用虚函数
开销 零开销(编译期就定好了) 有一点点(一次间接跳转、每个对象多一个隐藏指针)
本章/本书位置 6.5 已经学过 本节的第 11.4 组
经典例子 add(int,int) 和 add(double,double) Shape* 指向 Circle / Rect,调 area()

一句话记住:"编译期多态——编译器替你选;运行期多态——等到跑起来才选。"

三、为什么"运行期多态"这么重要

因为它买来的是"可扩展"。看一个具体的场景:

没有虚函数时:写一个"算所有图形总面积"的函数——

double totalArea(Circle *cs, int n, Rect *rs, int m) {   // 痛苦开始
    double t = 0;
    for (int i = 0; i < n; i++) t += cs[i].area();
    for (int i = 0; i < m; i++) t += rs[i].area();       // 加一种形状,就加一个循环
    return t;
}

每加一种新形状(三角形、椭圆……),这个函数都要改、要重编。而"函数的代码本来是对的"——它只是不认识新类型。

有了虚函数之后:

double totalArea(Shape *list[], int n) {                 // 一个循环,够了
    double t = 0;
    for (int i = 0; i < n; i++) t += list[i]->area();
    return t;
}

加一百种形状,这个函数一行都不用改。因为 list[i]->area() 会在运行时按每个对象的真实类型去调各自的 area()。

这就是"开放-封闭"的感觉:对扩展开放(加新形状不用动老代码),对修改封闭(老代码不需要改)。多态的价值就在这一句。

四、本章路线图

节 讲什么
11.4.1 虚函数概述:virtual 怎么起作用、三个必要条件、虚析构函数铁律
11.4.2 动态绑定:用 Shape* 数组装各种形状,一次遍历各显神通;含实例06
11.4.3 虚继承:virtual 的另一个用法——解决 11.3.2 的菱形问题;含实例07

注意 virtual 的两个"同名不同用":在函数前 → 虚函数(动态绑定);在继承方式前(class B : virtual public A)→ 虚继承(共享基类)。同一个关键字,干两件毫不相干的事,看到 virtual 先看它在哪个位置。

五、动手 Demo:一个词带来的差别

#include <iostream>
using namespace std;

class Base1 {                            // 没有 virtual
public:
    void hello() { cout << "Base1::hello" << endl; }
};

class Derived1 : public Base1 {
public:
    void hello() { cout << "Derived1::hello" << endl; }
};

class Base2 {                            // 有 virtual
public:
    virtual void hello() { cout << "Base2::hello" << endl; }
};

class Derived2 : public Base2 {
public:
    void hello() { cout << "Derived2::hello" << endl; }
};

int main() {
    cout << "── 非虚 ──" << endl;
    Derived1 d1;
    Base1 *p1 = &d1;
    p1->hello();                         // 按 p1 的类型(Base1)定 → Base1::hello
    d1.hello();                          // 直接调对象 → Derived1::hello

    cout << "── 虚函数 ──" << endl;
    Derived2 d2;
    Base2 *p2 = &d2;
    p2->hello();                         // 按对象的真实身份 → Derived2::hello
    d2.hello();
    return 0;
}

看到什么算成功:

── 非虚 ──
Base1::hello
Derived1::hello
── 虚函数 ──
Derived2::hello
Derived2::hello

读结果:

非虚组 虚函数组
p->hello() Base1::hello(被"类型"定死) Derived2::hello(按"对象身份"选)
d.hello() Derived1::hello Derived2::hello

唯一改动就是 virtual 一个词——这就是多态的开机键。

30 秒实验:把 Derived2::hello 的 virtual 去掉(只去子类里的),重新编译运行 → 输出不变。 - 因为"虚"这个性质会往下遗传:父类某函数是虚的,子类同名同参数的那个自动也是虚的(不管写不写 virtual)。 - 反过来试:父类不写 virtual、子类写 virtual → 父类不是虚的,多态不成立(p->hello() 还是父类版本)。"虚"必须从父类开始,才行。

六、常见误区

  1. "父类指针指向子类对象,调用同名函数就会调子类的"——只有函数是 virtual 才会。非虚函数按"指针的类型"定,虚函数按"对象的真实类型"定(本节 Demo 的对照)。
  2. "多态=函数重载"——那是编译期多态。重载在编译时就定死了(按实参类型选版本);本书讲的多态是运行期多态(虚函数,跑起来才知道调谁)。
  3. "多态要求我用对象本身调用"——恰好相反。动态绑定只在"通过指针或引用调用"时生效(11.4.1 会讲清三个必要条件)。用对象(值)调用,编译器早就知道它是谁了,不需要"动态"。
  4. "子类里要写 virtual 才是虚函数"——不用。父类声明了虚,子类的同名同参数函数自动是虚的(遗传)。但建议还是写上 virtual(或现代写法的 override),让人一眼看出"这是重写"。
  5. "虚函数有性能代价,能不用就不用"——这点代价在绝大多数场合可以直接忽略(一次间接跳转)。真正的代价是"必须用指针/引用"和"对象多一个隐藏指针"。用错地方的代价(无法扩展)远大于"那一点点开销"。
自测三问(点开对答案) 1. 编译期多态和运行期多态分别靠什么实现?——**编译期多态**靠**函数重载(6.5)和模板**(编译时就定好调哪个);**运行期多态**靠**虚函数 `virtual`**(运行时按对象真实类型选)。 2. 为什么说"运行期多态买来的是可扩展"?——因为**调用方的代码不需要知道具体有多少种类型**:`list[i]->area()` 一个循环就能处理任意多种形状。**加新类型只写新类,老代码一行不改。** 3. `virtual` 在继承处还能干别的事吗?——**能,但那是另一回事**:写在**继承方式前**(`class B : virtual public A`)表示**虚继承**,用来解决菱形继承的"两份祖先数据"问题(11.4.3)。**同一个词,两个用途。**

相关笔记