虚函数表(vtable)是编译期生成的类级静态数据,存于只读段;每个对象含一个vptr指向所属类vtable,sizeof增加指针大小即为直接证据。

虚函数表(vtable)不是运行时构造的数据结构,而是编译器在编译阶段为每个含虚函数的类生成的一张静态函数指针数组,存放在程序的只读数据段(.rodata)里;每个对象内部隐含一个指针(vptr),指向它所属类的这张表——这是动态多态能工作的唯一前提。
怎么确认对象里真有 vptr?看 sizeof 就行
只要类声明了虚函数(哪怕只有一个),它的 sizeof 就会比成员变量总和多出一个指针大小(64 位下是 8 字节)。这不是巧合,是 vptr 占用的空间:
-
class A { int x; virtual void f(); };→sizeof(A)是 16(int占 4,vptr占 8,再加 4 字节对齐填充) -
class B { virtual void f(); };→sizeof(B)是 8(纯vptr,无其他成员) - 去掉
virtual,sizeof立刻回落到成员变量对齐后的大小
这个差异是最直接、无需调试器就能验证的证据。别信“可能编译器优化掉了”,只要虚函数存在,vptr 就一定在对象起始位置。
vtable 里到底存了什么?不只是函数地址
在 GCC/Clang(Itanium ABI)下,vtable 开头通常不是函数指针,而是元数据:
立即学习“C++免费学习笔记(深入)”;
- 第 0 项:可能是
typeinfo指针(用于dynamic_cast和异常匹配) - 第 1 项:可能是 “offset-to-top”(多重继承时用于调整
this指针) - 后续项才是虚函数指针,按类中虚函数声明顺序排列(不是定义顺序,也不是重写顺序)
所以不能简单认为 vtable[0] 就是第一个虚函数。用 GDB 查看时,要结合 info vtbl 或手动偏移跳过前导元数据。MSVC 的布局略有不同,但同样不全是函数指针。
为什么派生类对象调用虚函数能跳到子类实现?
关键不在“查找”,而在“覆盖”。编译器为派生类生成独立的 vtable,其中对应基类虚函数的位置,被替换成派生类版本的地址:
-
Base::f在Base的vtable[0] -
Derived::f在Derived的vtable[0](覆盖) -
Derived新增的虚函数g()则追加在vtable末尾(如vtable[2])
当 Base* p = new Derived,p 指向的对象首地址处的 vptr 指向的是 Derived 的 vtable,所以 p->f() 查 vtable[0] 拿到的就是 Derived::f 的地址。没有哈希、没有遍历、没有运行时解析——只是固定偏移取指针。
多重继承下 vptr 可能不止一个
如果一个类从多个带虚函数的基类继承(比如 class D : public A, public B,且 A、B 都有虚函数),那么 D 对象内存里会包含多个 vptr:
- 第一个
vptr放在对象开头,指向A相关的vtable(或合并后的主表) - 第二个
vptr出现在A成员之后、B成员之前,指向B的vtable - 每次通过不同基类指针访问,取的是不同位置的
vptr,查的是不同表
这也是为什么 static_cast 在多重继承中可能改变指针值——它得把指针调整到对应基类子对象的起始地址,否则 vptr 偏移就错了。
真正容易被忽略的是:虚函数调用开销不在“查表”,而在于 CPU 无法在编译期确定目标地址,导致分支预测失败、间接跳转延迟。在 hot path 上,哪怕一次 vptr + vtable 二级寻址,也可能比内联普通函数慢 2–3 个周期。这不是理论,是 cache line 和 BTB(Branch Target Buffer)的真实限制。


















