DevTool-C++虚函数与对象布局
C++ 的运行时多态让调用者通过基类接口使用不同实现。语言规定的是调用应该落到哪个覆盖函数,而不是要求所有编译器都使用同一张表、同一个偏移。
虚表和虚指针是理解常见实现的工具。讨论对象内存时,应始终区分标准语义与具体 ABI(应用程序二进制接口),尤其不要把普通继承、虚函数和虚继承混在一起。
1. 先确认调用结果
下面是一个可独立编译的 C++17 示例:
1 | |
foo 被覆盖,bar 沿用基类实现。baz 是普通成员函数,不能通过 Base 接口直接访问,也不因为 Derived 里有其他虚函数就自动变成虚函数。
2. 常见实现如何找到函数
可以把简单单继承对象概念化为“基类子对象中的虚指针→该动态类型的虚表→目标函数入口”。派生类继承 bar 时,其对应入口仍指向 Base 的实现。
但虚表还可能包含运行时类型信息、偏移或不同析构入口,实际布局依 ABI 而异。笔记中的“三行函数地址表”适合说明覆盖关系,不适合据此硬编码槽位或解引用对象内存。
编译器若能证明动态类型,还可能去虚拟化并内联调用;语言层面虚函数调用不保证一定产生一次实际间接跳转。
3. 为什么基类析构函数常需要 virtual
通过基类指针删除派生对象时,基类通常应提供虚析构函数,使销毁遵循正确的动态类型。智能指针也不会自动修复所有错误接口设计,特别是以 Base 类型默认删除器持有对象的情形。
若类明确禁止通过基类删除,也可以使用相应的受保护析构设计。应让所有权规则清楚,而不是看到继承就无条件堆功能。
4. 虚继承解决的是另一种问题
菱形继承中,两个中间类都继承同一基类。普通继承产生两个基类子对象;把这两条相关路径改为虚继承,才会共享一个虚基类子对象。
1 | |
这与 Root 是否有虚函数无关。原笔记前面的 Derived : Base 没有 virtual 继承,不能在后续解释里声称它因此拥有 vbptr。
MSVC 的某些布局使用虚基类表指针,其他 ABI 可采用不同方式。完整 Leaf 对象还包含基类子对象的实现数据,不能简单说“最派生对象没有 vbptr”就把内部结构忽略。
图中只表达子对象的共享关系,不指定内存偏移和指针布局。具体 ABI 对虚基类的访问方式应使用编译器布局报告或调试器验证。
5. 多继承还可能调整 this
Base 指针可能指向完整对象中的一个子对象,而不是完整对象开头。跨基类转换、虚调用和协变返回等情况,可能需要 this 调整或跳板。
这也是不应把“vptr 总在整个对象第一个字节”“一个对象只有一个 vptr”当作通用规则的原因。对象大小还受成员、对齐、空基类优化等影响。
6. 怎样验证自己的编译目标
先用实际调用确认语义,再用调试器、反汇编或编译器布局报告观察实现。记录编译器版本、目标架构、优化选项和 ABI;同一源码在 Debug 与 Release 中也可能生成不同调用形式。
性能判断应关注调用频率、类型分布和优化机会。频繁虚调用可能影响内联与局部性,但“一个间接调用一定很慢”也不是有效分析。
总结
虚函数决定动态派发,虚继承决定基类子对象共享,虚表与虚基类表属于实现机制。先根据语言语义判断结果,再使用具体编译器证据解释布局,才不会把示意图误当成标准。