这一篇解决什么问题
多继承再叠一层就会出现菱形:A、B 都继承 V,C 同时继承 A 和 B。这时 V 在 C 里有几份?怎么解决?这一篇回答:
- 普通继承的菱形为什么会带两份基类子对象,有什么问题?
- 虚继承怎么做到只留一份,代价是什么?
- vbptr 和 vbtable(虚基类偏移表)是怎么工作的?
普通菱形:两份 V 的尴尬
1 | V 普通继承下, C1 的布局: |
后果不只是浪费内存:c1.v 编译直接报歧义错误,必须写 c1.A1::v 或 c1.B1::v 显式指定——同一个对象里有两个 v,编译器不知道你要哪个。
虚继承:一份 V,用间接换唯一
A2、B2 改成 virtual public V 后,C2 里 V 只有一份。实现思路(MSVC):
- V 被挪出常规子对象序列,放到对象尾部,由最派生类(C2)负责构造——A2、B2 不再各自携带 V。
- A2、B2 子对象里改放一个 vbptr(虚基类表指针,8 字节),指向一张 vbtable:表里存的是”从我的位置到 V 的偏移量”。
- 访问
a2.v变成:取 vbptr → 查表得偏移 → 加上去 → 找到 V。多了一次间接,但任何路径找到的 V 都是同一份。
代价清晰可见:每个含虚基类的子对象多 8 字节 vbptr,每次访问虚基类成员多一次查表间接。
验证工程
1 | // 菱形继承与虚继承: 两份基类子对象 vs vbptr 定位的单份虚基类. |
预测输出
动手跑之前先推一遍:
- A1 = V 4 + a 4 = 8;C1 = A1 8 + B1 8 = 16,两份 V 各归各的子对象。
- A2 变大:vbptr 8 + a 4 + padding + V(被挪到尾部)≈ 24;C2 里 A2、B2 各自的无 V 部分 + 共享的一份 V,估计 40 上下。虚继承的精确尺寸和对齐处理关系大,算错正常,关键是下面两条定性结论。
- 定性结论 1:c1 的两条路径取到两个不同地址(两份 V 实锤)。
- 定性结论 2:c2 的两条路径取到同一个地址(一份 V 实锤,歧义消失)。
实测输出
VS2022 x64 Debug 下实际运行:
1 | plain : sizeof(A1)=8 sizeof(C1)=16 |
逐行比对
sizeof(A1)=8、sizeof(C1)=16:命中。sizeof(A2)=24、sizeof(C2)=40:命中。C2 的 40 可以拆开看:A2 部分(vbptr 8 + a 4 + padding 4 = 16)+ B2 部分(16)+ 尾部共享的一份 V(v 4 + padding 4 = 8)。V 被挪到对象尾部、每个子对象多带一个 vbptr,这就是虚继承比 C1(16 字节)大 2.5 倍的代价。- C1 两个 v 地址不同,且正好差 8:命中。A1 子对象是
[v][a]共 8 字节,B1 的 v 紧跟其后,差值 8 和sizeof(A1)=8互相印证。 - C2 两条路径地址相同:命中,一份 V 实锤,
c2.v不再有歧义。
用 /d1 看 vbtable
这个工程的重点其实在编译输出里。/d1 reportAllClassLayout 对虚继承类会打印:
vbptr的位置(通常在子对象偏移 0);vbtable的内容:形如vbtable entry 0: 到自身偏移; entry 1: 到 V 的偏移;- V 子对象被放在整个对象布局的尾部,并用
virtual base标注。
把这份报告和打印的地址对照,”V 在尾部 + vbptr 查表”这套机制就完全对上了。
小结
1 | 普通菱形: 每路一份 V, c.v 歧义, 必须 A::v / B::v 显式区分 |