这一篇解决什么问题
前面七篇的所有结论——vptr 在偏移 0、虚表按类共享、vbptr 查表——都来自 MSVC 的实测。这一篇退一步回答一个更根本的问题:这些机制是 C++ 语言规定的吗?换个编译器还成立吗?
对象模型是实现选择,不是语言规定
C++ 标准只规定行为(多态要生效、构造析构顺序要正确、虚基类只能有一份),从不规定实现。vptr、虚表、vbptr 这些概念在标准文本里根本不存在,它们是编译器为了兑现行为而发明的机制。
《深度探索 C++ 对象模型》描述的是 1990 年代 Cfront(最早的 C++ 编译器,把 C++ 翻译成 C)的实现。书里的模型和今天 MSVC 的实测有不少出入,比如:
- vptr 位置:Cfront 曾把 vptr 放在对象尾部(为了和 C 结构体布局兼容,派生类可以无缝当基类用);MSVC、GCC 今天都放在头部(偏移 0),因为虚调用取 vptr 零偏移更高效。
- 虚继承的实现:Cfront 在每个子对象里放虚基类的直接指针(指针数量随虚基类个数膨胀);MSVC 演进成 vbptr + vbtable(一张表统一管偏移)。
所以这本书的正确用法是当”机制字典”查概念(为什么需要虚表、虚继承要解决什么),具体数字一律以本机编译器实测为准——这也是这个系列每篇都做实验的原因。
MSVC 与 GCC/Clang 的对照
两大阵营的机制选择对比(定性,细节以各自 ABI 文档为准):
| 机制 | MSVC | GCC / Clang(Itanium ABI) |
|---|---|---|
| vptr 位置 | 对象偏移 0 | 对象偏移 0 |
| 虚表内容 | 纯函数指针数组 | 表头还带 RTTI 指针、offset-to-top |
| RTTI | 虚表前一个槽位(Complete Object Locator) | 虚表内(index -1) |
| 虚基类偏移 | 独立的 vbptr + vbtable | 合并进虚表(vcall/vbase offsets) |
| this 调整 | adjustor thunk 小跳板 | 虚表里直接存偏移量(non-virtual thunk 也存在) |
可以看到:要解决的问题完全一样(虚调用、this 调整、虚基类定位),解法各有各的工程审美。MSVC 喜欢”多张专表”(vftable、vbtable 分开),Itanium ABI 喜欢”一张大表全塞进去”。
ABI(应用二进制接口)这个概念也由此而来:同一份 C++ 源码,两个编译器编出的对象布局不同、虚表结构不同,所以 MSVC 编的 dll 和 GCC 编的主程序不能直接互调 C++ 接口——能互通的只有 C 接口(extern “C”),因为 C 的 ABI 是统一的。
系列回顾:实测方法论
这个系列走下来,方法论比任何单个结论都重要:
1 | 1. 写十几行最小代码, 让机制暴露出来 |
以后遇到任何”对象模型”问题——比如 noexcept、final、协程对对象的影响——同一套方法直接套用:写最小工程,让编译器和调试器告诉你答案。