虚拟继承确保菱形继承中虚基类子对象唯一存在,由最派生类统一构造并置于对象末尾,通过vbptr和vbase table在运行时动态定位。

虚拟继承解决的是菱形继承中基类重复实例化的问题,其核心在于让最派生类“独占”一份虚基类子对象,并通过额外的指针(virtual base pointer, vbptr)和偏移信息(virtual base offset)在运行时动态定位虚基类成员。
虚基类子对象只存在一份
在非虚拟继承的菱形结构中(如 B 和 C 都继承 A,D 继承 B 和 C),D 对象内存里会包含两份 A 的副本。虚拟继承后,B 和 C 中对 A 的继承声明为 virtual,此时编译器确保 D 中仅存在**唯一一份 A 的子对象**,且它被放置在 D 对象布局的末尾(或靠近末尾)区域,而非嵌入在 B 或 C 子对象内部。
这意味着:
- B 和 C 子对象内部不再包含 A 的数据成员,只保留一个 vbptr(指向虚基类表 vbase table)
- D 的构造函数负责构造唯一的 A 实例,并调整所有中间类(B/C)中的 vbptr,使其能正确定位到该 A 实例
- 访问 A 的成员时,需通过 vbptr + 偏移量间接计算地址,无法在编译期直接确定
vtable 中新增虚基类表指针(vbptr)和虚基类表(vbase table)
每个含虚基类的类(如 B、C)的对象头部(或特定位置)会多出一个指针:vbptr。它指向一张只读的虚基类表(vbase table),该表按虚基类声明顺序存放若干 偏移量(offset to virtual base)。
立即学习“C++免费学习笔记(深入)”;
例如:
- 假设 class B : virtual A,则 B 的 vbase table 第一项是 “从 B 对象地址到 A 子对象的字节偏移”
- class D : B, C,D 的对象中,B 子对象和 C 子对象各自带自己的 vbptr;两者指向的 vbase table 内容可能不同,但最终都导向同一份 A 实例
- 这个偏移值在 D 构造时由编译器生成并填入——因为只有最派生类知道 A 究竟被放在哪
构造顺序与偏移修正由最派生类控制
虚基类 A 的构造由最派生类 D 的构造函数**直接调用**(跳过 B 和 C 的构造函数中对 A 的调用),这是关键语义规则。同时,D 的构造函数体执行前,会先完成:
- 为 A 分配空间(在 D 对象内某处)
- 计算 A 相对于 B 子对象起始地址的偏移,并写入 B 的 vbptr 所指的 vbase table 对应项
- 同理处理 C 的 vbptr
因此,B::func() 中若访问 A::x,实际执行的是:读 B->vbptr → 查 vbase table 得 offset → (char*)this + offset → 访问 x。整个过程比普通继承多一次查表+地址计算,有轻微开销。
典型 vtable / 对象布局示意(简化)
以 GCC/Itanium ABI 为例,D 对象内存大致如下(低地址→高地址):
- [B 子对象]:vptr_B(指向 B 的 vtable)、B 自有成员、vbptr_B(指向 B 的 vbase table)
- [C 子对象]:vptr_C、C 自有成员、vbptr_C
- [A 子对象]:A 的数据成员(唯一一份)
B 的 vtable 除普通虚函数外,还隐含一个“虚基类偏移字段”,但更常见的是:vtable 末尾或单独存 vbase table,其中条目为 相对于当前子对象起始地址的偏移。比如 vbptr_B 指向的表中存着 +24,表示 A 子对象起始地址 = (char*)&b_obj + 24。
基本上就这些。虚拟继承不是语法糖,它改变了对象布局模型,引入了运行时间接寻址,但也换来语义上的唯一性和可预测性。理解它,关键是抓住“最派生类统一安置 + vbptr 动态导航”这一机制。











