C++虚继承中的虚基类表

虚继承主要解决多重继承会在子类中存在多份拷贝的问题,这不仅浪费空间,而且存在二义性。

在之前的 C++ 继承中已经说过虚继承基本概念,这里不再赘述。这篇文章主要探究虚继承的原理。文章中多处给出了类实例对象的内存布局,查看其内存布局时,使用 VS 工具 /d1 reportAllClassLayout 进行查看,关于这个工具的详细介绍,请点击这里

虚继承的实现原理

虚继承的底层实现一般与编译器相关,一般会通过虚基类表指针虚基类表实现,先看如下这个程序:

#pragma pack(1)

class A
{
public:
    int a;
};

class B : virtual public A
{
public:
    int b;
};

class C : virtual public A
{
public:
    int c;
};

其内存布局图如下所示:

> cl /d1 reportSingleClassLayoutB CppTest.cpp
class B size(12):
        +---
 0      | {vbptr}
 4      | b
        +---
        +--- (virtual base A)
 8      | a
        +---

B::$vbtable@:
 0      | 0
 1      | 8 (Bd(B+0)A)
vbi:       class  offset o.vbptr  o.vbte fVtorDisp
               A       8       0       4 0
----------------------------------------------------
> cl /d1 reportSingleClassLayoutC CppTest.cpp
class C size(12):
        +---
 0      | {vbptr}
 4      | c
        +---
        +--- (virtual base A)
 8      | a
        +---

C::$vbtable@:
 0      | 0
 1      | 8 (Cd(C+0)A)
vbi:       class  offset o.vbptr  o.vbte fVtorDisp
               A       8       0       4 0

从中可以看出,每个虚继承的子类都有一个虚基类表指针 vbptr(virtual base table pointer,占用一个指针的大小,32 位下为4字节,64 位为 8 字节)和该指针指向一个虚基类表(额外的空间,不占用实例对象的空间),上面的程序中 B 虚继承 A,那么 B 类中将会有一个虚基类表指针 vbptr,C 类虚继承 A 类,也是一样。该 vbptr 指针指向一个虚基类表,关于虚基类表下面再详细说。

现在我们再看一个例子:

#pragma pack(1)

class A
{
public:
    int a;
};

class B : virtual public A
{
public:
    int b;
};

class C : virtual public A
{
public:
    int c;
};

class D : public B, public C
{
public:
    int d;
};

这次我们使用类 D 多重继承类 B 和类 C(B 和 C 都是虚继承 A),看一下内存布局图:

> cl /d1 reportSingleClassLayoutD CppTest.cpp
class D size(24):
        +---
 0      | +--- (base class B)
 0      | | {vbptr}
 4      | | b
        | +---
 8      | +--- (base class C)
 8      | | {vbptr}
12      | | c
        | +---
16      | d
        +---
        +--- (virtual base A)
20      | a
        +---

D::$vbtable@B@:
 0      | 0
 1      | 20 (Dd(B+0)A)

D::$vbtable@C@:
 0      | 0
 1      | 12 (Dd(C+0)A)
vbi:       class  offset o.vbptr  o.vbte fVtorDisp
               A      20       0       4 0

需要强调的是,虚基类依旧会在子类中存在拷贝,但仅仅只存在一份;我们看到虚基类 A 仅仅在类 D 的实例对象中存在一份,也就是上面内存布局里的 virtual base A。当继承的子类被当作父类继承时,虚基类表指针也会被继承;我们上面已经测试过 B 虚继承 A,C 虚继承 A 都会在各自的实例对象中多出一个虚基类表指针 vbptr 指向一个虚基类表,现在 D 类继承与 B 类和 C 类,其类中的虚基类表指针也会被继承。我们也可以从上面的内存布局中看到 D 中类 B 和类 C 中都没有类 A 的成员而是都多了一个虚基类表指针 vbptr,而且类 D 中只有一份虚基类 A 的成员,所以奥秘就在 类 B 和类 C 中的虚基类表指针 vbptr 上。

我们再看类 B 和类 C 的虚基类表的结构,该表中记录了虚基类 A 与本类的偏移地址,因为现在虚基类 A 在类 D 的实例对象中只能存在一份,所以类 B 和类 C 中只要通过虚基类表中给的偏移量访问就能访问到虚基类 A。上面的内存布局中,仅存的一份虚基类 A 的地址相较于类 D 的实例对象的首地址偏移量为 20,而类 B 的 vbptr 相较于类 D 的实例对象的首地址偏移量为 0,所以类 B 中想要访问虚基类 A 需要偏移 20 个字节,所以虚基类表中记录的与虚基类 A 的偏移量为 20,而类 C 的访问虚基类 A 的偏移量为 12,也是同样可以这样计算出来。这样就可以找到虚基类 A 的数据成员,而且类 B 和类 C 对虚基类 A 的数据的修改都是在同一地址,这样就不用像普通菱形继承那样维持着公共基类(也就是这里说的虚基类)的两份同样的拷贝了,同时也能节省一点空间。

我们可以联系到多态中虚函数表,这两者有一定的相似之处,不过虚基类表存储的时虚基类相对当前实例对象的偏移,而虚函数表则存储的是虚函数地址。

到这里,关于虚基类表的探究就已经结束了,我们对虚继承的原理也有了一定的认识,还可以自己在程序中访问虚基类表,其基本思路如下:

D* d = new D;
std::cout << "[0] ->" << *(int*)(*(int*)d) << std::endl;  
std::cout << "[1] ->" << *(((int*)(*(int*)d)) + 1)<< std::endl; // 偏移量20
std::cout << "b value  : " << *((int*)d + 1) << std::endl;
std::cout << "[0] ->" << *(int*)(*((int*)d + 2)) << std::endl;  
std::cout << "[1] ->" << *((int*)(*((int*)d + 2)) + 1) << std::endl; // 偏移量12  
std::cout << "c value  : " << *((int*)d + 3) << std::endl;  
std::cout << "d value  : " << *((int*)d + 4) << std::endl;
std::cout << "a value  : " << *((int*)d + 5) << std::endl;

参考:
C++ 虚继承实现原理(虚基类表指针与虚基类表)
【C++拾遗】 从内存布局看C++虚继承的实现原理

  • 4
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

code_peak

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值