C++ 虚函数、虚表与多态机制
更新时间:2026-08-25。本文是
languages/cpp/主题专家层第 5 篇。入门层讲了"多态让基类指针调用派生类实现",本文回答它是怎么实现的——虚表(vtable)、虚指针(vptr)、动态派发的底层机制,以及这个"灵活性"换来的性能代价。
本文要回答的问题
virtual函数在底层是怎么工作的?虚表里存了什么?- 为什么虚函数调用比普通函数慢?"间接跳转"慢在哪?
- 编译器能"去掉虚化"(devirtualization)吗?什么时候可以?
一、虚表的底层结构
1.1 有虚函数的类的内存布局
cpp
class Shape {
public:
virtual double area() const { return 0; } // 虚函数
virtual void draw() const {} // 虚函数
};
class Circle : public Shape {
double r_;
public:
double area() const override { return 3.14 * r_ * r_; }
};编译器为有虚函数的类自动生成一张虚表(vtable),并在对象里塞一个**虚指针(vptr)**指向它:

| 组成 | 说明 |
|---|---|
| vtable(虚表) | 每个类一张,存所有虚函数的函数指针,按声明顺序排列 |
| vptr(虚指针) | 每个对象一个,指向它所属类的虚表 |
1.2 虚表内容
虚表是函数指针数组:
cpp
// Shape 的虚表(伪代码)
void *Shape_vtable[] = {
&Shape::area, // 槽位 0
&Shape::draw, // 槽位 1
};
// Circle 的虚表(重写了 area,draw 继承)
void *Circle_vtable[] = {
&Circle::area, // 槽位 0 被重写
&Shape::draw, // 槽位 1 继承(未重写)
};二、动态派发:虚函数调用过程
cpp
Shape *s = new Circle(5.0);
s->area(); // 到底调用哪个 area?运行时才知道s->area() 的底层过程:
asm
mov (%s), %rax ; ① 读 vptr(对象首地址就是 vptr)
mov (%rax), %rdx ; ② 从虚表读第 0 个槽位(函数地址)
call *%rdx ; ③ 间接调用(跳转到实际函数)三步间接寻址:读 vptr → 读虚表 → 间接跳转。这就是虚函数比普通函数(直接 call)慢的原因。
三、虚函数 vs 普通函数 vs 模板
| 机制 | 调用方式 | 开销 | 灵活性 |
|---|---|---|---|
| 普通函数 | 直接 call(地址编译期已知) | 最低 | 低 |
| 虚函数 | 间接 call *(运行时查虚表) | 有间接跳转 | 高(运行期多态) |
| 模板 | 编译期展开 + 内联 | 零(内联后无调用) | 高(编译期多态) |
关键洞察:
- 虚函数 = 运行期多态:灵活,但有间接跳转开销。
- 模板 = 编译期多态:零开销,但代码会膨胀(每种类型一份)。
这就是 模板与泛型编程 里说的"模板 vs 虚函数"——把多态决策从运行期提前到编译期。
四、虚函数的性能代价:间接跳转
为什么间接跳转慢?原因在 CPU 分支预测:

- 直接调用:目标地址固定,分支预测器能完美预测。
- 间接调用:目标地址依赖运行时数据(虚表内容),预测器更难预测,预测失败会冲刷流水线(几十个周期的惩罚)。
详见 L4 分支预测。
五、去虚化(devirtualization)
编译器并非总是"傻乎乎"地间接调用——当它能确定对象的实际类型时,会去掉虚化:
cpp
Circle c(5.0);
c.area(); // c 是 Circle(不是指针/引用),编译期就能确定类型
// 编译器直接调用 Circle::area,跳过虚表!
Shape *s = &c;
s->area(); // 通过指针,可能间接调用
// 但如果编译器内联分析能证明 s 一定指向 Circle,也会去虚化final 关键字能帮助去虚化:
cpp
class Circle final : public Shape { // final:不能再被继承
// 编译器知道 Circle 没有派生类,s->area() 可直接绑定
};去虚化是编译器把"运行期多态"优化回"编译期绑定"的手段,配合 -O2 生效。
六、虚函数的其他开销
| 开销 | 说明 |
|---|---|
| 对象多一个 vptr | 每个对象多 8 字节(64 位) |
| 无法内联 | 虚函数一般不能被内联(除非去虚化) |
| 间接跳转 | 分支预测负担 |
| 多继承更复杂 | 多继承可能有多个 vptr、虚表偏移 |
七、与本站主线衔接
- 分支预测:间接跳转的预测失败惩罚,见 L4 分支预测。
- 模板对比:编译期多态 vs 运行期多态,见 模板与泛型编程。
- 对象布局:vptr 如何影响对象大小和对齐,见 对象内存布局。
- 剖析:虚函数调用在火焰图里显示为间接调用,用 perf 观察。
一句话总结
虚函数 = 每个类一张虚表(存函数指针)+ 每个对象一个 vptr(指向虚表)+ 调用时三步间接寻址;多态的灵活性换来的是间接跳转开销和无法内联——追求性能时,用 final 帮助去虚化,或用模板把多态移到编译期。
上一篇:并发与线程安全 下一篇:对象内存布局与编译期优化