虚函数
上一课留了一个问题:通过基类的引用或指针,调到的是基类的函数,即使对象是改写了这个函数的派生类。换成通知再看一遍:deliver 接收任何 Notification,交给它一封邮件和一条短信:
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**:
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}
copy 是一个 Notification 对象:复制进去的只有 e 的基类部分(上一课的切片),它从里到外就是一条通知。ref 引用的是 e 本身,那是一个 Email,所以虚函数调用找到的是 Email::send。
override
override 不是非写不可,但每次都写上。派生类的函数要每一样都对上才能替换基类的虚函数:名字、参数,还有 const。漏了 const 就成了另一个函数,override 会把它抓出来:
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 死的时候把它们删掉:
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,这个函数就是纯虚函数:「每个派生类都必须自己写」。有纯虚函数的类叫抽象类:它就是用来当基类的,不能用它建对象。
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,还是老样子:只有基类的。
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]。)把基类的析构函数改成虚函数:
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() {}。
练一练
写好代码,点检查。提示随便看,实在想不出来再看答案。
加一个正方形
✓写一个从 Shape 派生的类 Square:构造函数接收边长,area 返回边长乘边长,name 返回 "正方形"。main 里的循环不用改。程序要显示下面几行。
长方形:6 正方形:16 正方形:1 一共:23
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}
修好它:没对上的 override
✓这段程序编译不过。先看第一个 error:,再改 Reminder,让它真正替换 Message 的函数。程序要显示下面两行。
小林,你好 老王,账单该交了
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}
修好它:没删掉的临时文件
✓每个 TempDownload 被删除时应该删掉自己的临时文件,可那几行一直没显示。只改 Download 这个类(TempDownload 和 main 不动),让输出对上。
下载 photo.jpg 下载 song.mp3 删掉 song.mp3 的临时文件 关闭 song.mp3 删掉 photo.jpg 的临时文件 关闭 photo.jpg
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}
这一课学会了
- 没有
virtual时,通过基类引用或指针调用的是基类的版本,编译时就定死了。 - 基类里加上
virtual,调用时在程序运行时看对象,执行它那个类的版本:这叫多态。通过引用和指针才行,复制出来的不行。 - 每个替换基类的函数都写上
override:名字、参数、const有一样对不上,就得到marked 'override', but does not override,而不是一个悄悄藏着的错。 std::vector<std::unique_ptr<Shape>>能放各种形状,一个循环全管。virtual double area() const = 0;是纯虚函数;有它的类是抽象类,不能建对象,派生类必须写这个函数。- 有虚函数的类要有虚析构函数;没有的话,通过基类指针删除会漏掉派生类的析构函数。