0%

C++ 对象模型实战(八):对象模型不是语言规定——Cfront、MSVC 与 ABI 对照

这一篇解决什么问题

前面七篇的所有结论——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
2
3
4
5
1. 写十几行最小代码, 让机制暴露出来
2. sizeof / offsetof / 打印地址, 量化布局
3. /d1 reportAllClassLayout 让编译器自证
4. 断点 + 内存窗口 + 反汇编(Ctrl+K,G), 运行时验证
5. 预测 → 实测 → 比对, 不一致的地方就是最好的学习点

以后遇到任何”对象模型”问题——比如 noexcept、final、协程对对象的影响——同一套方法直接套用:写最小工程,让编译器和调试器告诉你答案。