虚函数与 VTable
本课属于 Coddy C++ 学习旅程的“面向对象编程”单元。第 52 课,共 104 课。
virtual 关键字解决了我们在上一课中遇到的问题。当你将一个方法声明为 virtual 时,C++ 会在运行时根据对象的实际类型,而不是指针类型,来确定要调用哪个版本。
class Animal {
public:
virtual void speak() {
std::cout << "Some sound" << std::endl;
}
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "Woof!" << std::endl;
}
};
Dog d;
Animal* ptr = &d;
ptr->speak(); // 输出:Woof!现在调用了正确的方法!override 关键字是可选的,但建议使用。它会告诉编译器你打算重写虚函数,如果函数签名不匹配,就能捕获错误。
这是如何工作的?当一个类有虚函数时,编译器会创建一个虚函数表(vtable)。这是一个隐藏的查找表,其中包含指向该类虚函数的指针。每个有虚函数的对象都会存储一个指向其类的 vtable 的隐藏指针(vptr)。
通过指针调用虚函数时,程序会在运行时从 vtable 中查找正确的函数地址。这称为动态分派。与普通函数调用相比,它会增加少量开销,但能实现强大的多态行为。
一条重要规则:如果一个类有虚函数,并且会被用作基类,那么它的析构函数也应该是虚函数。这样,通过基类指针删除派生对象时,才能正确清理:
class Animal {
public:
virtual ~Animal() {} // 虚析构函数
virtual void speak() {}
};挑战
简单让我们构建一个媒体播放器系统,展示虚函数和运行时多态的强大之处。你将创建一个媒体类型层次结构,让每种媒体都以不同的方式播放,并了解虚函数如何确保通过基类指针访问时也能执行正确的行为。
你将把代码分成三个文件:
Media.h:定义一个表示任何可播放媒体的基类Media:- 一个受保护的
std::string title成员 - 一个接收标题并保存它的构造函数
- 一个虚
play()方法,输出:Playing media: <title> - 一个虚析构函数,输出:
Media [<title>] destroyed
- 一个受保护的
AudioTrack.h:定义一个公开继承自Media的AudioTrack类:- 一个私有的
std::string artist成员 - 一个接收标题和艺术家姓名、将标题传给基类并保存艺术家姓名的构造函数
- 使用
override关键字重写play(),输出:Playing audio: <title> by <artist> - 一个输出
AudioTrack [<title>] destroyed的析构函数
- 一个私有的
main.cpp:读取三个输入(每个输入单独占一行):- 音轨标题(字符串)
- 艺术家姓名(字符串)
- 视频标题(字符串)
在 main.cpp 中直接定义一个公开继承自
Media的VideoClip类:- 一个私有的
int duration成员(单位为秒) - 一个接收标题和时长的构造函数(默认时长为 120)
- 重写
play(),输出:Playing video: <title> (<duration>s) - 一个输出
VideoClip [<title>] destroyed的析构函数
在一个代码块作用域内创建一个包含三个
Media*指针的数组。动态分配以下对象:- 一个标题为 "Generic Media" 的基类
Media对象 - 一个使用输入的标题和艺术家姓名的
AudioTrack对象 - 一个使用输入的视频标题的
VideoClip对象
遍历数组,并对每个指针调用
play()。然后按相反顺序删除所有对象。离开代码块后,输出:Playback complete!
例如,输入 Bohemian Rhapsody、Queen 和 Nature Documentary 时:
Playing media: Generic Media
Playing audio: Bohemian Rhapsody by Queen
Playing video: Nature Documentary (120s)
VideoClip [Nature Documentary] destroyed
Media [Nature Documentary] destroyed
AudioTrack [Bohemian Rhapsody] destroyed
Media [Bohemian Rhapsody] destroyed
Media [Generic Media] destroyed
Playback complete!注意,通过 Media* 指针调用 play() 时,虚函数会调用正确的派生类方法。也请留意,虚析构函数可以确保正确清理对象:通过基类指针删除对象时,派生类和基类的析构函数都会运行。
亲手试试
#include <iostream>
#include <string>
#include "Media.h"
#include "AudioTrack.h"
using namespace std;
// TODO:在这里定义公开继承 Media 的 VideoClip 类
// - 私有 int 类型的 duration 成员(单位:秒)
// - 构造函数接收 title 和 duration(duration 默认值为 120)
// - 重写 play(),输出:Playing video: <title> (<duration>s)
// - 析构函数输出:VideoClip [<title>] destroyed
class VideoClip : public Media {
private:
int duration;
public:
// TODO:实现构造函数
VideoClip(const std::string& t, int d = 120) : Media(t) {
// TODO:保存 duration
}
// TODO:重写 play() 方法
void play() override {
// TODO:实现这个方法
}
// TODO:实现析构函数
~VideoClip() {
// TODO:实现这个析构函数
}
};
int main() {
// 读取输入
string audioTitle;
string artist;
string videoTitle;
getline(cin, audioTitle);
getline(cin, artist);
getline(cin, videoTitle);
// TODO: 用花括号创建一个块作用域
// 在代码块中:
// 1. 创建一个包含三个 Media* 指针的数组
// 2. 动态分配:
// 一个标题为 "Generic Media" 的基类 Media 对象
// 一个使用输入的标题和艺术家信息创建的 AudioTrack 对象
// 一个使用输入的视频标题创建的 VideoClip 对象
// 3. 遍历数组,对每个指针调用 play()
// 4. 按相反顺序删除所有对象
// TODO: 在代码块之后输出:Playback complete!
return 0;
}
这节课包含一个小测验。开始学习即可作答,并记录你的进度。
面向对象编程中的所有课
自己练习: C++ 在线编译器