虚函数表是编译器生成的虚函数地址数组,每个含虚函数的类有对应vtable,对象含指向它的vptr;调用时通过vptr查表跳转;纯虚函数填桩函数地址;gcc/clang可用-fdump-class-hierarchy查看;单继承复用父表,多重继承有多个vptr和thunk;布局非标准,不可跨编译器或abi依赖。

虚函数表是编译器自动生成的函数指针数组
它不是 C++ 语言标准里的概念,而是主流编译器(如 GCC、Clang、MSVC)为支持动态多态而生成的运行时数据结构。每个含虚函数的类都有一个对应的 vtable,里面存着该类所有虚函数的地址——不是函数体,是跳转入口。
对象实例里隐式包含一个指向其所属类 vtable 的指针(vptr),通常位于对象内存布局的最开头。调用虚函数时,实际执行的是:取 this->vptr → 查 vtable 对应下标 → 跳转到函数地址。
- 没有虚函数的类,不会有
vtable,对象也不含vptr - 派生类若重写基类虚函数,其
vtable中对应项会被替换成派生类版本的地址 - 纯虚函数在基类
vtable中填的是“纯虚函数调用”桩函数地址(如__cxa_pure_virtual),不是空指针
怎么看到 vtable 的真实内容?(GCC/Clang 下)
不能直接写代码访问 vtable 或 vptr,它们是编译器内部实现细节,但可以通过编译器导出符号或反汇编观察。
常用方法是让编译器输出类布局和虚表信息:
立即学习“C++免费学习笔记(深入)”;
- 加
-fdump-class-hierarchy编译选项(GCC/Clang),会生成.class文件,里面明确列出每个类的vtable成员顺序和函数地址 - 用
objdump -t binary | grep vtable可看到符号名,如vtable for Base - 调试时在 GDB 中打印对象地址,再用
x/4a *(void**)obj_ptr查看前几个vptr指向的函数地址(需关掉 ASLR 或用set disable-randomization off)
注意:vtable 符号名带空格和括号(如 vtable for Derived),在命令行中要加引号或转义。
继承关系改变时 vtable 怎么变?(单继承 vs 多重继承)
单继承下,子类 vtable 通常复用父类部分布局,新增虚函数追加在末尾;多重继承时,情况复杂得多——子类对象内存里可能有多个 vptr,分别指向不同基类的 vtable 片段。
- 虚继承会引入额外的调整 thunk(thunk 是一小段跳转胶水代码),用于修正
this指针偏移,这些 thunk 地址也会出现在vtable中 - 如果基类 A 和 B 都有同名虚函数
f(),而派生类 C 同时继承 A 和 B 并重写f(),C 的vtable里会出现两个f()入口,分别对应 A-subobject 和 B-subobject 的视角 - 这种布局差异直接影响
dynamic_cast和typeid的行为,也解释了为什么多重继承对象的地址转换不总是“零开销”
踩坑最多的地方:把 vtable 当成稳定 ABI 使用
vtable 布局完全由编译器决定,不同版本、不同平台、甚至不同优化等级都可能改变函数顺序或插入填充项。试图手动读写 vtable 或依赖其偏移调用函数,等于绑定到特定编译器实现。
- 导出类到动态库时,若头文件没同步更新虚函数顺序,运行时可能调用错函数(比如基类新加虚函数,导致原有虚函数下标+1,但旧二进制仍按老偏移查表)
- 跨模块传递对象指针时,确保双方用同一套 ABI(例如都用 libc++ 或都用 libstdc++),否则
vtable结构可能不兼容 - 序列化对象时,绝不能直接 memcpy 整个对象——
vptr是运行时地址,序列化后无法还原
真正需要控制底层行为时,优先考虑策略模式、类型擦除(如 std::function)或显式函数表,而不是碰 vtable。








