Kodewick
第 24 课,共 30 课目录

虚函数

第 24 课,共 30 课 · 第 6 部分 · 继承和模板 · 约 20 分钟

上一课留了一个问题:通过基类的引用或指针,调到的是基类的函数,即使对象是改写了这个函数的派生类。换成通知再看一遍:deliver 接收任何 Notification,交给它一封邮件和一条短信:

main.cpp
1#include <iostream>2 3class Notification {4public:5    void send() const {6        std::cout << "发送一条通知\n";7    }8};9 10class Email : public Notification {11public:12    void send() const {13        std::cout << "发送一封邮件\n";14    }15};16 17class TextMessage : public Notification {18public:19    void send() const {20        std::cout << "发送一条短信\n";21    }22};23 24void deliver(const Notification& n) {25    n.send();26}27 28int main() {29    Email e;30    TextMessage t;31    deliver(e);32    deliver(t);33    return 0;34}
输出
发送一条通知
发送一条通知

两行都是「通知」。编译器把 n.send() 变成代码的时候,只知道 n 是 Notification&,于是一次定死:调用 Notification::send。n 后面到底是什么对象,要等程序跑起来才知道。

virtual

在基类的函数前面加上 **virtual,挑哪个版本就推迟到程序运行的时候:每次调用都看看对象本身,执行它那个类的版本。派生类那边,在括号后面写上 override**:

main.cpp
1#include <iostream>2 3class Notification {4public:5    virtual void send() const {6        std::cout << "发送一条通知\n";7    }8};9 10class Email : public Notification {11public:12    void send() const override {13        std::cout << "发送一封邮件\n";14    }15};16 17class TextMessage : public Notification {18public:19    void send() const override {20        std::cout << "发送一条短信\n";21    }22};23 24void deliver(const Notification& n) {25    n.send();26}27 28int main() {29    Email e;30    TextMessage t;31    deliver(e);32    deliver(t);33    return 0;34}
输出
发送一封邮件
发送一条短信

只改了一个 virtual 和两个 override,deliver 对每种通知都做对了。点单步:从 deliver 里的 n.send() 出发,第一次进了 Email::send,第二次进了 TextMessage::send。同一行代码,看对象是什么,每次执行不同的函数:这叫多态。

基类里是 virtual 的函数,到了每个派生类里也都是虚函数,写不写这个词都一样。为了做到这一点,有虚函数的类的每个对象都带着一个隐藏的指针,指向它那个类的版本表;单步画面里不显示它。

只有通过引用或指针调用才看对象。复制出来的就是一个基类对象:

先猜一猜

这段程序显示什么?

1#include <iostream>2 3class Notification {4public:5    virtual void send() const {6        std::cout << "通知\n";7    }8};9 10class Email : public Notification {11public:12    void send() const override {13        std::cout << "邮件\n";14    }15};16 17int main() {18    Email e;19    Notification copy = e;20    Notification& ref = e;21    copy.send();22    ref.send();23    return 0;24}

override

override 不是非写不可,但每次都写上。派生类的函数要每一样都对上才能替换基类的虚函数:名字、参数,还有 const。漏了 const 就成了另一个函数,override 会把它抓出来:

main.cpp
1#include <iostream>2 3class Notification {4public:5    virtual void send() const {6        std::cout << "发送一条通知\n";7    }8};9 10class Email : public Notification {11public:12    void send() override {13        std::cout << "发送一封邮件\n";14    }15};16 17int main() {18    Email e;19    const Notification& n = e;20    n.send();21    return 0;22}
输出
main.cpp:12:10: error: 'void Email::send()' marked 'override', but does not override
   12 |     void send() override {
      |          ^~~~

'void Email::send()' marked 'override', but does not override:基类里没有不带 const 的 send() 可以替换。先看第一个 error:,意思是「它和基类里的哪个都对不上」。加上 const 就编译通过了。

不写 override,同样的错能悄悄编译通过:Email::send 成了一个新函数,只是把基类的隐藏了,通过 n 调用还是走到 Notification::send。这种错很难找;override 把它变成编译错误。

一个 vector 里放各种形状

对象多了、类又各不相同的时候,多态最有用。把它们当基类指针放进同一个 vector,一个循环就全管了。std::unique_ptr(第 21 课)在 vector 死的时候把它们删掉:

main.cpp
1#include <iostream>2#include <memory>3#include <string>4#include <vector>5 6class Shape {7public:8    virtual ~Shape() {}9 10    virtual double area() const {11        return 0;12    }13 14    virtual std::string name() const {15        return "形状";16    }17};18 19class Rectangle : public Shape {20public:21    Rectangle(double w, double h) : w_(w), h_(h) {}22 23    double area() const override {24        return w_ * h_;25    }26 27    std::string name() const override {28        return "长方形";29    }30 31private:32    double w_;33    double h_;34};35 36class Circle : public Shape {37public:38    Circle(double r) : r_(r) {}39 40    double area() const override {41        return 3.14 * r_ * r_;42    }43 44    std::string name() const override {45        return "圆";46    }47 48private:49    double r_;50};51 52int main() {53    std::vector<std::unique_ptr<Shape>> shapes;54    shapes.push_back(std::make_unique<Rectangle>(3, 4));55    shapes.push_back(std::make_unique<Circle>(1));56    shapes.push_back(std::make_unique<Rectangle>(2, 5));57    double total = 0;58    for (const auto& s : shapes) {59        std::cout << s->name() << ":" << s->area() << '\n';60        total += s->area();61    }62    std::cout << "一共:" << total << '\n';63    return 0;64}
输出
长方形:12
圆:3.14
长方形:10
一共:25.14

std::make_unique<Rectangle>(3, 4) 建一个 Rectangle,放进 vector 时成了 unique_ptr<Shape>,就像 Rectangle* 能变成 Shape* 一样。循环只认识 Shape;明天加一个 Triangle 类,循环一个字都不用改。

为什么放指针,不用 std::vector<Shape>?Shape 元素只放得下一个 Shape,长方形放进去会被切成一个普通的形状。类里第一行的 virtual ~Shape() {} 这一课最后再讲。

纯虚函数和抽象类

一个笼统的「形状」面积是多少?没有像样的答案,上面的 return 0; 是凑的。不写函数体,写成 = 0,这个函数就是纯虚函数:「每个派生类都必须自己写」。有纯虚函数的类叫抽象类:它就是用来当基类的,不能用它建对象。

main.cpp
1#include <iostream>2 3class Shape {4public:5    virtual ~Shape() {}6    virtual double area() const = 0;7};8 9class Square : public Shape {10public:11    Square(double side) : side_(side) {}12 13    double area() const override {14        return side_ * side_;15    }16 17private:18    double side_;19};20 21int main() {22    Square sq{3};23    std::cout << sq.area() << '\n';24    Shape s;25    return 0;26}
输出
main.cpp: In function 'int main()':
main.cpp:24:11: error: cannot declare variable 's' to be of abstract type 'Shape'
   24 |     Shape s;
      |           ^
main.cpp:3:7: note:   because the following virtual functions are pure within 'Shape':
    3 | class Shape {
      |       ^~~~~
main.cpp:6:20: note:     'virtual double Shape::area() const'
    6 |     virtual double area() const = 0;
      |                    ^~~~

cannot declare variable 's' to be of abstract type 'Shape'。上一行的 Square sq{3}; 没问题:它写了 area。派生类忘了写,它自己也还是抽象类,建它的对象会报同样的错,只是换成它的名字。这样,新加一种形状要写哪些函数,编译器一个个提醒你。抽象类的指针和引用没问题,上面的 vector 就是这样放形状的。

虚析构函数

通过基类指针删除一个派生类对象,执行的是谁的析构函数?没有 virtual,还是老样子:只有基类的。

main.cpp
1#include <iostream>2 3class Notification {4public:5    ~Notification() {6        std::cout << "通知清理完毕\n";7    }8 9    virtual void send() const = 0;10};11 12class Email : public Notification {13public:14    ~Email() {15        std::cout << "邮件草稿已删除\n";16    }17 18    void send() const override {19        std::cout << "发送一封邮件\n";20    }21};22 23int main() {24    Notification* n = new Email;25    n->send();26    delete n;27    return 0;28}
输出
发送一封邮件
通知清理完毕

「邮件草稿已删除」一直没出来:delete n 只知道 n 是 Notification*。要是 Email 有内存或文件要还,就落下了。(按 C++ 的规定这是未定义行为,g++ 的做法就是你看到的这样。用 g++ -Wall 编译,它会在 delete n; 这一行警告:[-Wdelete-non-virtual-dtor]。)把基类的析构函数改成虚函数:

main.cpp
1#include <iostream>2 3class Notification {4public:5    virtual ~Notification() {6        std::cout << "通知清理完毕\n";7    }8 9    virtual void send() const = 0;10};11 12class Email : public Notification {13public:14    ~Email() {15        std::cout << "邮件草稿已删除\n";16    }17 18    void send() const override {19        std::cout << "发送一封邮件\n";20    }21};22 23int main() {24    Notification* n = new Email;25    n->send();26    delete n;27    return 0;28}
输出
发送一封邮件
邮件草稿已删除
通知清理完毕

这回 delete n 找到了对象自己的析构函数 ~Email,它之后照常执行 ~Notification:先子后父,和上一课一样。两段程序都点单步,比一比 delete n; 之后程序去了哪里。

std::unique_ptr<Notification> 和那个放形状的 vector 也一样:它们也是通过基类指针删除的。规矩很短:有虚函数的类,析构函数也写成虚的,哪怕是空的,像 virtual ~Shape() {}。

练一练

写好代码,点检查。提示随便看,实在想不出来再看答案。

练习 1

加一个正方形

写一个从 Shape 派生的类 Square:构造函数接收边长,area 返回边长乘边长,name 返回 "正方形"。main 里的循环不用改。程序要显示下面几行。

要显示
长方形:6
正方形:16
正方形:1
一共:23
exercise1.cpp
1#include <iostream>2#include <memory>3#include <string>4#include <vector>5 6class Shape {7public:8    virtual ~Shape() {}9    virtual int area() const = 0;10    virtual std::string name() const = 0;11};12 13class Rectangle : public Shape {14public:15    Rectangle(int w, int h) : w_(w), h_(h) {}16 17    int area() const override {18        return w_ * h_;19    }20 21    std::string name() const override {22        return "长方形";23    }24 25private:26    int w_;27    int h_;28};29 30int main() {31    std::vector<std::unique_ptr<Shape>> shapes;32    shapes.push_back(std::make_unique<Rectangle>(2, 3));33    shapes.push_back(std::make_unique<Square>(4));34    shapes.push_back(std::make_unique<Square>(1));35    int total = 0;36    for (const auto& s : shapes) {37        std::cout << s->name() << ":" << s->area() << '\n';38        total += s->area();39    }40    std::cout << "一共:" << total << '\n';41    return 0;42}
输出
点「运行」或按 Ctrl+Enter
练习 2

修好它:没对上的 override

这段程序编译不过。先看第一个 error:,再改 Reminder,让它真正替换 Message 的函数。程序要显示下面两行。

要显示
小林,你好
老王,账单该交了
exercise2.cpp
1#include <iostream>2#include <string>3 4class Message {5public:6    virtual ~Message() {}7 8    virtual std::string text(const std::string& name) const {9        return name + ",你好";10    }11};12 13class Reminder : public Message {14public:15    std::string text(std::string name) override {16        return name + ",账单该交了";17    }18};19 20void show(const Message& m, const std::string& name) {21    std::cout << m.text(name) << '\n';22}23 24int main() {25    Message hello;26    Reminder bill;27    show(hello, "小林");28    show(bill, "老王");29    return 0;30}
输出
点「运行」或按 Ctrl+Enter
练习 3

修好它:没删掉的临时文件

每个 TempDownload 被删除时应该删掉自己的临时文件,可那几行一直没显示。只改 Download 这个类(TempDownload 和 main 不动),让输出对上。

要显示
下载 photo.jpg
下载 song.mp3
删掉 song.mp3 的临时文件
关闭 song.mp3
删掉 photo.jpg 的临时文件
关闭 photo.jpg
exercise3.cpp
1#include <iostream>2#include <memory>3#include <string>4 5class Download {6public:7    Download(const std::string& file) : file_(file) {}8 9    ~Download() {10        std::cout << "关闭 " << file_ << '\n';11    }12 13    virtual void start() const = 0;14 15protected:16    std::string file_;17};18 19class TempDownload : public Download {20public:21    TempDownload(const std::string& file) : Download(file) {}22 23    ~TempDownload() {24        std::cout << "删掉 " << file_ << " 的临时文件\n";25    }26 27    void start() const override {28        std::cout << "下载 " << file_ << '\n';29    }30};31 32int main() {33    std::unique_ptr<Download> a = std::make_unique<TempDownload>("photo.jpg");34    std::unique_ptr<Download> b = std::make_unique<TempDownload>("song.mp3");35    a->start();36    b->start();37    return 0;38}
输出
点「运行」或按 Ctrl+Enter

这一课学会了

  • 没有 virtual 时,通过基类引用或指针调用的是基类的版本,编译时就定死了。
  • 基类里加上 virtual,调用时在程序运行时看对象,执行它那个类的版本:这叫多态。通过引用和指针才行,复制出来的不行。
  • 每个替换基类的函数都写上 override:名字、参数、const 有一样对不上,就得到 marked 'override', but does not override,而不是一个悄悄藏着的错。
  • std::vector<std::unique_ptr<Shape>> 能放各种形状,一个循环全管。
  • virtual double area() const = 0; 是纯虚函数;有它的类是抽象类,不能建对象,派生类必须写这个函数。
  • 有虚函数的类要有虚析构函数;没有的话,通过基类指针删除会漏掉派生类的析构函数。