虚函数表(vtable)是编译器生成的只读函数指针数组,每个含虚函数的类对象含指向它的_vptr;多态通过运行时查表实现,析构函数需virtual以确保delete基类指针时正确调用派生类析构。

虚函数表(vtable)是编译器自动生成的函数指针数组
每个含虚函数的类,编译器会在其对象内存布局开头(或紧随基类部分之后)隐式插入一个指向 vtable 的指针(_vptr)。这个 vtable 本身不是类成员,而是一块只读数据段里的静态数组,存的全是该类虚函数的地址。
多态调用发生时,实际执行的是:obj->_vptr[n] 找到函数地址,再跳转——不是靠类型名匹配,而是靠运行时查表。
- 同一个类的所有对象共享同一份
vtable,不随对象数量增长 - 派生类会复制基类的
vtable条目,再覆盖(override)被重写的虚函数地址,新增的虚函数追加在末尾 - 多重继承下,子类可能有多个
_vptr(分别对应不同基类),布局更复杂,但查表逻辑不变
为什么析构函数要声明为 virtual?
因为 delete 一个基类指针时,若析构函数非 virtual,编译器只会调用基类的析构函数,派生类部分不会被清理——这不是“没调用”,而是根本没进 vtable 查找流程,直接静态绑定到基类版本。
只有声明为 virtual,析构函数才进入 vtable,delete pbase 才能正确触发派生类析构逻辑。
立即学习“C++免费学习笔记(深入)”;
- 纯虚析构函数也要提供定义(哪怕空实现),否则链接失败:
virtual ~Base() = 0 { } - 构造函数永远不能是
virtual——对象还没建好,_vptr还没初始化,没法查表 - static 成员函数、内联函数、友元函数都不进
vtable,跟虚机制无关
怎么验证 vtable 是否生效?看汇编或调试器内存
别猜,直接看生成代码。用 g++ -S 编译带虚函数的类,会看到类似 call *%rax(间接调用);而普通函数是 call _Z3foo(直接符号调用)。
在 GDB 中,打印对象地址后,用 x/4a *(void**)obj_ptr 可看到前几项就是虚函数地址(需注意 ABI 差异,如 Itanium vs MSVC)。
- 开启
-fno-rtti不影响vtable,RTTI(如dynamic_cast)只是额外用到了vtable旁的类型信息结构 - 空基类优化(EBO)可能让
_vptr和基类成员复用内存位置,但语义不变 - 模板类里定义虚函数?可以,但每个实例化版本都有自己的
vtable
虚函数调用比普通函数慢在哪?
主要慢在两次内存访问:先读对象里的 _vptr,再按偏移读 vtable 中的函数地址。现代 CPU 的分支预测和缓存通常能缓解,但高频率小函数(比如 get() 访问器)仍可能成为瓶颈。
真正伤性能的不是虚调用本身,而是它阻止了内联、妨碍了逃逸分析、限制了某些编译器优化(如 devirtualization)。
- Clang/GCC 在 LTO 模式下可能做 devirtualization(如果能证明动态类型唯一),但不可依赖
-
final关键字可显式禁止重写,帮助编译器提前决定是否内联:virtual void f() final - 避免在 tight loop 里反复通过基类指针调用虚函数;考虑批量处理或策略模式解耦
虚函数表不是黑魔法,它是编译器写死的指针数组 + 运行时一次间接跳转。真正容易被忽略的是:它的存在让对象大小增加(通常 8 字节),且一旦用了虚函数,整个类就失去 triviality(无法 memcpy 安全拷贝),这些副作用在嵌入式或高性能场景中常被低估。










