C++虚继承路径独立的判定、vbptr的层级访问

目录

一、路径独立的定义及判断方式

(1)场景一:同根分支共享vbptr

(2)场景二:锚点(vbptr的地址)不同,不能合并

(3)场景三:某一条路径是其他路径的一部分

(4)判断步骤:3 步快速识别

(5)总结

二、为什么要虚基类表,而不是硬编码偏移量?

三、虚基类表的偏移量是什么时候写入的?访问最底层的虚基类需要什么步骤?

​编辑

(1)为什么必须重新布局偏移量?

(2)总结:偏移量重新布局的核心逻辑

(3)随着继承体系的加深,访问底层基类的跳转


        在学习C++继承的底层原理的时候,我发现很多资料写的似乎不一致,有的资料说一个类的虚基类表指针数量永远只有一个,他记录着所有的虚基类偏移量。而有的资料则说一个类可能有多个虚基类表指针,他直接继承了多少个虚基类就会有多少个虚基类表指针。

        于是产生了矛盾,让我也百思不得其解。后续查阅了大量的资料才发现上述二者说法都不对,正确的方式应该是独立路径的个数=虚基类表指针的个数。本文阐明了菱形继承、路径合并的关键,让我们对C++继承的理解更上一层楼。

一、路径独立的定义及判断方式

        在路径中,我们喜欢把最上层的子类(继承了各个虚基类的子类)称为路径根

        这里 Final 就是 “路径根”,它的 virtual 继承分支都以 Final 为起点

判断两条继承路径是否独立,核心看路径的中间基类是否完全无重叠,即 “中间路径原则”:

 如果路径上有重叠,则可以合并成一个虚基类表指针。

(1)场景一:同根分支共享vbptr

class Base {};
class Base2 {};

// 路径根:Derived1
class Derived1 : 
    virtual public Base,  // 分支1:从Derived1出发 → Base
    virtual public Base2  // 分支2:从Derived1出发 → Base2
{ /* ... */ };

它们是 Derived1 继承列表里的平行分支(没有嵌套、没有继承关系),共享同一个 “根(Derived1)”。

核心:因为两个分支的起点(根)相同,编译器会把它们合并到同一张虚基类表里,用 1 个 vbptr 管理。

(2)场景二:锚点(vbptr的地址)不同,不能合并

class Base {};

// Derived1 有自己的虚基类路径
class Derived1 : virtual public Base { /* ... */ };
// Derived2 有自己的虚基类路径
class Derived2 : virtual public Base { /* ... */ };

// 路径根:Final
class Final : 
    virtual public Derived1,  // 分支1:Final → Derived1 → Base
    virtual public Derived2   // 分支2:Final → Derived2 → Base
{ /* ... */ };

       

        你可能会疑惑:“场景 2 里 Final 继承 Derived1 和 Derived2,根不也是 Final 吗?为啥路径不合并?”
                —— 因为 Derived1 和 Derived2 本身有独立的虚基类分支,即他们自己有vbptr作为独立的锚点。导致路径无法合并。

为什么必须拆分路径?

  • 两个 virtual 继承(Derived1 和 Derived2)的起点虽然都是 Final,但:
    • Derived1 自己有独立的虚基类路径(Derived1→Base
    • Derived2 自己有独立的虚基类路径(Derived2→Base
  • 这两条路径在 Final 之外是分离的Derived1 和 Derived2 没有继承关系),无法合并。

也就是说想要合并到一个虚基类表中,要满足的条件是:

1.同一个根出发(锚点都在根处)

2.两条路径不是完全独立的,有相互继承关系

3.如果两条路径完全独立,且该路径上只有根和一个基类(没有独立的锚点),也可以合并

所以Final 的内存里,有 2 个 vbptr(对应两条独立路径),每个 vbptr 指向不同的虚基类表

(3)场景三:某一条路径是其他路径的一部分

class X {};
class A : virtual public X {};
// B 继承 A(有直接继承关系)
class B : virtual public A {};  

// 路径根:Final,虚继承A和B
class Final : virtual public A, virtual public B {};

  B 直接继承 AB 是 A 的子类),所以A有的B全都有,出现X为共享基类。两条分支有交叉依赖B 的路径包含 A);
→ 两条分支不独立 → Final 只需要 1 个 vbptr(因为 B 的路径包含 A,可合并)。

访问路径如下:

(4)判断步骤:3 步快速识别

(1)看是否有直接 / 间接继承
检查两个分支的 “直接父类”(如 A 和 B):如果 A 是 B 的父类(或反之),则不独立

(2)看是否共享基类但平行
即使共享基类(如场景 2 的 Base),只要两个分支的直接父类(A 和 B)没有继承关系,就是独立的。

(3)看继承链是否交叉
从路径根(如 Final)出发,两条分支的继承链(Final→A→... 和 Final→B→...如果没有重叠(除了可能的共享基类),则独立;如果有重叠(如场景 3 中 B→A),则不独立

(5)总结

        独立分支” 的核心是:从路径根出发后,两条分支的继承链像 “平行线” 一样分开,不会出现 “一条链包含另一条链” 的情况

  • 独立 → 需要多个 vbptr(数量 = 分支数);
  • 不独立 → 可合并路径,只需 1 个 vbptr

二、为什么要虚基类表,而不是硬编码偏移量?

在上面的场景一中可以看到,他们是共用一个vbptr的。内存布局如下

        如果不用虚基类表指针,而直接硬编码偏移量,则会发现需要两个偏移量,但是此时只需要一个指针大小即可存放。我们完全有理由可以推断:如果虚基类足够多,则可能需要记录很多个偏移量,此时采取指针和偏移量数组的方式才能更好的节省内存,否则一个类对象将会有大量的空间用于记录偏移量,得不偿失。

三、虚基类表的偏移量是什么时候写入的?访问最底层的虚基类需要什么步骤?

Derived也同理类似。

(1)为什么必须重新布局偏移量?

  1. 共享基类的地址变化
    Base 在 FinalDerived 中被放到末尾,地址与 Derived1 单独存在时完全不同,必须重新计算偏移。

  2. 继承体系的动态性
    当 FinalDerived 新增成员(如 int f)时,Derived1 和 Derived2 的子对象地址会变化,导致 vbptr 中的偏移量必须动态调整。

  3. 虚基类表的复用性
    Derived1 的 vbtable 可以被多个子类(如 FinalDerivedFinalDerived2)复用,只需在子类中生成新的 vbptr 和 vbtable,无需修改 Derived1 本身的布局。

(2)总结:偏移量重新布局的核心逻辑

  • 编译期计算:每个类的偏移量在编译时确定,子类继承时会重新计算父类 vbptr 的偏移量。
  • 动态适配布局:虚基类表(vbtable)存储 “相对偏移”,确保无论父类在子类中的地址如何变化,都能正确找到共享基类。
  • 复用与覆盖:父类的 vbtable 作为 “模板”,子类通过生成新的 vbtable 覆盖偏移量,保证继承体系的灵活性。

        简单说:虚继承的偏移量会随着继承层次的加深动态重新布局,虚基类表是编译器应对这种动态性的核心机制,让复杂继承体系的地址计算始终正确。

(3)随着继承体系的加深,访问底层基类的跳转

        注意:这里的偏移量是以当前虚基类表指针地址为锚点,所计算的偏移量。而并非是大类Finaldervied类的起始地址的偏移量。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值