Lesson 0303 · 多态与虚函数

③ 面向对象程序设计 — 第 3 课

三句话速览
  1. 多态 = 同一个调用,不同对象做不同的事。
  2. 基类 virtual,子类 override,要指针或引用。
  3. 纯虚 = 0 是抽象类;基类析构加 virtual。
上节课(0302):继承 = is-a,子类继承父类成员,同名方法会覆盖父类版本。
这节课:OOP 第三个特性多态——同一个函数名,不同对象做出不同的事。这是 OOP 最核心、也最容易绕晕的一课。

1. 什么是多态

同样是 speak(),狗叫「汪汪」,猫叫「喵喵」。但我们希望调用它的代码写得一模一样:

p->speak();   // p 指向狗就是汪汪,指向猫就是喵喵

同一行代码,结果由 p 实际指向谁来决定。多态 = 同一个接口,不同行为。

它解决的是「扩展」问题:以后再加一只鸟,只要它也会 speak(),调用它的那个循环一个字都不用改。

2. virtual —— 让函数跟着实际对象走

class Animal {
protected:
    string name;
public:
    Animal(string n) : name(n) {}
    virtual void speak() {           // 基类加 virtual
        cout << name << " 叫了一声" << endl;
    }
};

class Dog : public Animal {
public:
    Dog(string n) : Animal(n) {}
    void speak() override {          // 子类加 override
        cout << name << " 汪汪叫" << endl;
    }
};

class Cat : public Animal {
public:
    Cat(string n) : Animal(n) {}
    void speak() override {
        cout << name << " 喵喵叫" << endl;
    }
};
两个关键字:基类写 virtual,子类写 override(表示「我在重写爸爸的函数」)。
override 不是必须的,但强烈建议写:它会替你检查「是不是真的重写上了」。

3. 必须用指针或引用

Dog d("旺财");
Cat c("咪咪");

Animal* p = &d;
p->speak();        // 旺财 汪汪叫

p = &c;
p->speak();        // 咪咪 喵喵叫

只有通过基类指针/引用调用虚函数,才会在运行时判断真实类型。原因在于:p 本身只知道「我指着一个 Animal」,它需要多存一份信息才能知道「其实这是 Dog」。

不要这样写:Animal a = d; —— 这叫「对象切片」,Dog 多出来的部分被切掉,就只剩 Animal,多态失效。

切片之后,a 是一个货真价实的 Animal 对象,它身上已经没有 Dog 的那部分数据了。所以多态的第一道门槛是:用指针或引用,不要按值传递。

4. 纯虚函数和抽象类

class Shape {
protected:
    string name;
public:
    Shape(string n) : name(n) {}
    virtual double area() const = 0;   // 纯虚函数:只声明,不实现
};

= 0 表示「每个子类必须自己实现 area()」。有纯虚函数的类叫抽象类,不能直接创建对象:Shape s("xxx"); ❌

抽象类的价值是「定规矩」:它规定所有图形都必须能算面积,但自己不提供算法。写 area() 的时候如果你忘了在子类实现,编译直接报错,而不是运行到一半才发现。

5. 完整示例 —— 用基类指针统一处理

#include <iostream>
#include <string>
#include <vector>
using namespace std;

class Shape {
protected:
    string name;
public:
    Shape(string n) : name(n) {}
    virtual double area() const = 0;              // 每个图形自己算面积

    virtual void show() const {
        cout << name << " 面积 " << area() << endl;
    }
};

class Circle : public Shape {
    double r;
public:
    Circle(string n, double radius) : Shape(n), r(radius) {}
    double area() const override { return 3.14159 * r * r; }
};

class Rectangle : public Shape {
    double w, h;
public:
    Rectangle(string n, double a, double b) : Shape(n), w(a), h(b) {}
    double area() const override { return w * h; }
};

int main() {
    Circle c("圆", 3);
    Rectangle r("矩形", 6, 6);

    vector<Shape*> shapes = { &c, &r };   // 统一放基类指针
    for (Shape* s : shapes) {
        s->show();                            // 自动调用对应的 area()
    }
    return 0;
}

关键在那句 s->show():同一个调用点,s 指向圆就调用 Circle::area,指向矩形就调用 Rectangle::area。以后加一个三角形,只要它实现了 area(),这个循环不用改一个字。

顺带一个细节:show() 也是 virtual,但它的子类没有重写它;这没问题,不重写就沿用父类的版本,里面调用的 area() 仍然会按实际类型走。

6. 虚析构 —— 先记住这个坑

虚析构函数预告:以后如果用 new 创建对象、通过基类指针 delete,基类析构函数要加 virtual。本课先知道有这个坑,后面用到再讲。

一句话解释为什么:delete p 时,如果析构不是虚函数,编译器只看 p 的静态类型(Animal),只调 ~Animal(),子类那部分的清理就漏掉了。

7. 坑点清单

8. 小测

Q1. 实现多态需要在基类函数前加什么?
Q2. 纯虚函数怎么写?
Q3. Animal* p = &dog; 后 p->speak() 调用谁?
Q4. 包含纯虚函数的类有什么特点?

9. 动手练习

题目:动物合唱团

- 基类 Animal:virtual void speak()
- 派生类 Dog / Cat:各自 override speak()
- main 里创建狗和猫,用 vector<Animal*> 统一存起来
- 循环调用 speak(),要求输出狗的「汪汪」和猫的「喵喵」

进阶:再加一个 Bird(「叽叽」),不修改循环代码也能输出新动物的叫声。
卡住时的自查顺序
  1. Animal::speak 前面有没有 virtual?没有的话,循环里全是一个声音。
  2. 子类写的是 override 还是把参数写歪了?
  3. vector 里存的是 Animal* 还是 Animal?存对象就会切片。
  4. 狗和猫对象是不是活到了循环结束(没在循环里被销毁)?
  5. 输出是「汪汪/喵喵」,还是变成了基类那句「叫了一声」?后者说明没重写上。
  6. 进阶:加了 Bird 之后,循环体有没有被动过?没动才算真多态。

本课小结

下一课预告
0304 运算符重载与友元 —— 让 a + b、a == b、cout << a 认识你自己的类。为什么 operator<< 不能写成成员函数、必须请 friend 出马,下一课一次讲清。
本课第 5 节那个 vector<Shape*> 循环,下一课会换成更顺手的写法:重载 << 之后,直接 cout << *s 就能打印。

← 上一课:继承与组合 · 课程对照表

视频 黑马第 80-105 集对应段 · 书籍 Primer Plus 第 13 章

在线 CodeFather 编程导航 — C++ 教程(补充视频参考)