C++虚函数表(vtable)是编译期生成的类级静态函数指针数组,存于只读段;每个含虚函数的类对象隐含一个vptr指针(x64下占8字节),指向其所属类的vtable,sizeof增大即因vptr存在,多态调用通过vptr→vtable→函数地址三级跳转实现。

多态不是编译器“猜”出来的,而是靠 _vfptr 和 vtable 这两个东西硬生生在内存里搭出来的运行时调度机制。没有虚函数表,就没有运行时多态。
为什么加了 virtual,sizeof 就变大了?
因为对象头部(或尾部,取决于编译器和平台)悄悄塞进了一个指针:_vfptr。它不参与成员变量对齐计算,但占真实空间。
- x86 下通常是 4 字节,x64 下是 8 字节 —— 和指针大小一致
- 哪怕类里只有 1 个虚函数,也得加这 1 个指针;10 个虚函数,还是只加 1 个指针
- 没有虚函数的类,
_vfptr完全不存在,sizeof就严格按成员变量+对齐算
vtable 是什么?它存哪儿?
vtable 是一张函数指针数组,每个元素是某个虚函数的地址(或跳转桩地址)。它不在对象里,而是在只读数据段(.rodata 或 .rdata)中,由编译器生成、全局共享。
- 基类有自己的
vtable,派生类重写虚函数后,会生成**新的vtable**,其中对应位置填入派生类函数地址 - 如果派生类没重写某个虚函数,它的
vtable对应项仍指向基类函数地址 - VS2022 x86 下,
vtable中存的未必是函数入口地址,可能是 jmp 指令地址(用于支持 DLL 导出等场景)
调用虚函数时,编译器实际干了什么?
不是直接 call 地址,而是走三步查表:取 _vfptr → 查 vtable 数组索引 → 取函数地址 → call。这个过程无法在编译期确定,必须运行时做。
- 非虚函数调用:汇编里就是一条
call XXX,地址固定 - 虚函数调用(满足多态):至少 3–4 条指令,包括
mov eax, [ecx](取 _vfptr)、mov eax, [eax](取 vtable[0])、call eax - 传参用对象值(而非指针/引用)会切片,
_vfptr被截断,vtable 查找失效 → 多态立刻消失
为什么必须用基类指针或引用?
因为只有它们能“看到”派生类对象完整的内存布局,包括那个关键的 _vfptr。用对象值传递,就只剩基类部分,_vfptr 不见了,vtable 也就没用了。
- 派生类对象内存 = [基类部分(含 _vfptr)] + [派生类新增成员]
- 基类引用/指针指向的是派生类对象起始地址,
_vfptr有效 → 查的是派生类vtable - 基类对象赋值(
Base b = derived;)会复制基类子对象,_vfptr复制的是基类的,永远调不到派生函数
真正容易被忽略的点:vtable 是编译期静态生成的,但 _vfptr 的初始化发生在构造函数里——基类构造时设为基类 vtable 地址,派生类构造函数体执行前会把它改成派生类 vtable 地址。这意味着,在基类构造函数中调用虚函数,哪怕派生类重写了,也只会调基类版本。这个细节,调试时经常踩坑。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











