Skip to content

继承深化:虚继承(菱形/钻石)、多重继承、二义性、this(C++ 深度)

vtable.md 讲对象布局,concepts.md 讲重载重写/切片;本篇专门攻坚多重/虚继承这道进阶题:菱形、vp 布局、二义性解决、this 偏移与 thunk。属于 medium-hard,讲清即拉开差距。

一、菱形继承(diamond)与问题

cpp
struct A { int a; void fa(){} };
struct B : A {};
struct C : A {};
struct D : B, C {};     // A 出现两次:D 有 B::a 与 C::a 两份 A 子对象
D d; d.a;                // ❌ 二义性:走 B 的 A 还是 C 的 A?
d.B::a;                  // 需带基类限定
  • is-a 的两份基类子对象:D 里有两份 A 数据 → 冗余 + 二义。
  • 虚继承让 A 只在最后派生类 D 里共存唯一一份
cpp
struct B : virtual A {};
struct C : virtual A {};
struct D : B, C {};   // D 中只有一份 A 子对象

二、虚继承的布局实现(关键独到点)

虚基类子对象不放在继承者的固定偏移开头,而是通过**虚基类表指针(vbptr/vbase offset)**间接定位:

D 对象(虚继承 B,C → 共享唯一 A)
┌───────────────────────────┐
 B 部分  [vbptr→ B 的 vbtable(含 A 偏移)] [B自己字段]
 C 部分  [vbptr→ C 的 vbtable(含 A 偏移)] [C自己字段]
 D 自己字段
 A 部分  [a]                 ← 放对象尾部,仅一份
└───────────────────────────┘
  • 每个含虚基类的对象要额外存 vbptr(运行时查偏移找到真正 A),也因此虚继承对象略大、访问虚基成员有一次间接
  • 构造顺序:最派生类先构造虚基类 A,再构造直接基类 B/C(这样唯一 A 只初始化一次,避免两次)——与普通“先基类后自己”不同,是虚继承构造顺序常见考点。

三、二义性解决三板斧

  1. 限定d.B::fa() / d.C::fa()
  2. 虚继承消除重复:菱形用 virtual 使共享唯一 A,去掉歧义来源之一。
  3. 在 D 里 override/using 显式指定using B::fa;d.fa() 可用。 (判断“是不是二义”:同名成员的查找逐层进行,若两条路径在同一层级各自找到 → 二义,编译器见歧义即报错,不会自动挑。)

四、多重继承的 this 偏移 / thunk(衔接 vtable 的 Q4)

多继承里,D* 转成后面的基类 C*地址要加偏移(C 子对象在 D 中靠后)。当通过 C 的虚函数指针调用覆写在 D 里的方法时,函数收到的是 C 部分的 this,需调回 D 开头 —— 编译器生成 thunk(调整桩):先把 this 减掉偏移回到 D* 再跳正体。这是“虚函数在不同基类视图地址不同”的机制注脚。能把这点讲顺 = 对 vtable 是真懂而非背。

五、虚函数表与多继承(简短归纳,详见 vtable.md)

  • 每一条含虚的基类支路通常配独立 vptr/vtable;D 实现对任一基类接口的覆写都进对应表槽。
  • 通过 A*/C* 调虚函数 → 查 A 表/ C 表同一槽 → 落在 D 实现(需 thunk 调整 this)。

六、this 指针(huihut 单列,补全)

  • this成员函数隐含参数,指向“调用该函数的对象”地址(=相应类子对象的起始地址,多重/虚继承时是绑定视图的指针)。
  • 只在非 static 成员函数内可用;不能取 this 本身更“改它”。
  • 编译器把它当隐藏首参传入:void A::f(int) 内部约同 f(A* this,int);由此可解释 this 作为 const 类对象的 this 是 const A*
  • nullptr 上不调(解引用 this 即 UB);delete this 合法性(旧题):自身为最后所有者、之后不再触 this 才可,工程基本避开。

七、面试串讲叙述(组织语言用)

「多继承把多个基类子对象平铺进派生对象,地址可能不在一处,因此 A*/C* 转 D* 要调偏移,经 C 的 vtable 调 D 覆写时编译器用 thunk 回正 this;菱形如果不虚继承则 D 含两份 A 导致二义与冗余,虚继承让共享的 A 只存在一份、用 vbptr+运行时偏移定位(代价是多存虚基类指针和间接),并改变构造顺序为『虚基类在最后派生类最先构造一次』。二义性靠显式限定/虚继承/override 消解。」

八、自测

  1. 菱形 + virtual 后 D 里 A 数据几份?访问 d.a 还歧义吗?—— 1 份;A 唯一故不歧义(除非 B/C 层又各自重名)。
  2. 虚继承的对象 vs 普通继承对象大小谁更大/为什么。
  3. delete this 的合法前置条件。

C++ 面试八股 · VitePress 版