虚函数表(vtable)是编译时生成的类级别只读函数指针数组,按虚函数声明顺序排列,对象仅含指向它的vptr;sizeof增加即因vptr,多态调用通过vptr间接跳转,不含RTTI信息。

虚函数表(vtable)到底长什么样
每个含虚函数的类在编译时会生成一张静态的函数指针表,就是 vtable;它不是运行时动态构造的,也不属于对象实例,而是类级别的只读数据段。对象里只存一个指针(vptr),指向所属类的 vtable 起始地址。
常见错误现象:sizeof(带虚函数的类) 比预期多 8 字节(64 位下),就是因为加了 vptr;有人误以为虚函数开销是“每次调用都查表”,其实查的是 vptr 所指的固定内存位置,不涉及哈希或遍历。
- vtable 中函数指针顺序严格按虚函数声明顺序排列,和定义顺序无关
- 派生类若重写基类虚函数,对应槽位会被替换成派生类版本的地址;若新增虚函数,则追加到表尾
- 多重继承时,子对象可能有多个
vptr(比如class D : public A, public B),分别指向不同基类子对象的 vtable
多态调用怎么找到正确的函数
编译器看到 ptr->func() 且 func 是虚函数时,不会直接生成 call 指令,而是生成三步操作:取 vptr → 查 vtable 对应偏移 → call 该地址。这个过程叫“间接跳转”,不是运行时反射或字符串匹配。
使用场景:只有通过指针或引用调用虚函数才触发动态绑定;直接以对象变量调用(如 obj.func())仍是静态绑定,哪怕 func 是虚函数。
- 纯虚函数在 vtable 中填的是哑地址(如
__purecall),调用会 crash,不是“空实现” - 构造/析构函数里调用虚函数,实际调用的是当前正在构造/析构的那个类的版本——因为此时
vptr已被设为该类的 vtable 地址,但派生类部分尚未就绪 - 内联失败:虚函数几乎无法被编译器内联(除非 devirtualize 优化开启且条件充分),这是性能代价的根源
为什么 offsetof 不能用在虚函数类上
offsetof 要求类型是标准布局(standard-layout),而只要类含虚函数,就自动失去标准布局资格——因为编译器可能在对象开头或中间插入 vptr,位置不保证,offsetof 无法安全计算成员偏移。
错误现象:GCC/Clang 对 offsetof 用于含虚函数类会报错 invalid application of 'offsetof' to a non-standard-layout type;MSVC 可能静默返回 0,导致越界访问。
- 替代方案:用普通指针算术(如
&obj.member-&obj)可得偏移,但仅限于已知对象存在时 - 序列化、memcpy、union 别名等依赖内存布局的操作,对虚函数类必须格外小心,容易踩未定义行为
- 想保持标准布局?只能放弃虚函数,改用模板、策略类或外部调度表
虚函数表在内存里真的一张表吗
不是。vtable 是编译期生成的全局只读数据块,通常位于 .rodata 段;每个类一份,不会为每个对象复制。你用 GDB 看 *obj.vptr,得到的就是那个地址;用 readelf -x .rodata your_binary 也能搜到连续的函数指针序列。
容易被忽略的地方:vtable 本身没有 RTTI 信息(如类型名),dynamic_cast 和 typeid 依赖的是另一套独立的 RTTI 数据结构(type_info 对象和 class hierarchy 描述符),它们和 vtable 邻近但不混在一起。
- 关闭 RTTI(
-fno-rtti)不影响 vtable 存在,但会让dynamic_cast和typeid不可用 - 虚函数表大小 = 虚函数个数 × 指针宽度,不含任何元数据(没有“函数名”“参数个数”字段)
- 如果类没虚函数但继承了带虚函数的基类,它仍会继承
vptr,vtable 内容是基类 vtable 的副本(可能被覆盖)
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











