问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

C++虚继承原理与类布局分析

创作时间:
作者:
@小白创作中心

C++虚继承原理与类布局分析

引用
1
来源
1.
https://www.cnblogs.com/ThousandPine/p/18111381

C++的虚继承机制是解决菱形继承结构中数据冗余和语法二义性问题的关键。本文将深入探讨虚继承的实现原理,通过对比普通继承、多继承和虚继承的类布局,揭示虚基类表(vbtable)和虚表指针(vbptr)的工作机制。

引言

在C++中,多继承是一种强大的特性,但同时也带来了复杂性,尤其是菱形继承结构。当一个派生类从两个基类继承,而这两个基类又都继承自同一个基类时,就会形成菱形继承结构。这种结构会导致数据冗余和语法二义性问题,因为派生类会继承两份相同的基类数据。为了解决这个问题,C++引入了虚继承机制。

本文将通过详细的类布局分析,探讨虚继承的实现原理。主要内容基于《C++: Under the Hood》一书的解读和提炼,通过对比普通继承、多继承和虚继承的类布局,揭示虚基类表(vbtable)和虚表指针(vbptr)的工作机制。

请注意,以下分析基于MSVC编译器的结果,不同编译器的具体实现可能会有所不同。

单继承

首先来看一个简单的单继承例子:

class A
{
public:
    int a1;
    int a2;
};

class B : public A
{
public:
    int b1;
    int b2;
};

通过Visual Studio的Class Layout功能,我们可以得到以下布局信息:

class A	size(8):
    +---
 0	| a1
 4	| a2
    +---
class B	size(16):
    +---
 0	| +--- (base class A)
 0	| | a1
 4	| | a2
    | +---
 8	| b1
12	| b2
    +---

从布局图可以看出,派生类B的实例数据紧跟在基类A的实例数据之后。这种布局方式使得将B类的地址转换为A类的指针时,不需要额外的位移操作。

多继承

接下来,我们看一个多继承的例子:

class A
{
public:
    int a1;
    int a2;
};

class B
{
public:
    int b1;
    int b2;
};

class C : public A, public B
{
public:
    int c1;
    int c2;
};

在这种情况下,C类需要包含A和B两个基类的实例数据。由于不能让两个基类的数据都位于起始位置,编译器会按照声明顺序布局基类实例,然后布局派生类的新数据成员。

C c;
(void *)(A *)&c == (void *)&c
(void *)(B *)&c > (void *)&c
(void *)(B *)&c == (void*)(sizeof (A) + (char *)&c)

这些判断语句的结果表明,当C转换为B时,需要进行偏移操作。

菱形继承

现在来看菱形继承的例子:

class A
{
public:
    int a1;
    int a2;
};

class B : public A
{
public:
    int b1;
    int b2;
};

class C : public A
{
public:
    int c1;
    int c2;
};

class D : public B, public C
{
public:
    int d1;
    int d2;
};

在这种结构下,D类会继承两份A类的实例数据,导致数据冗余和语法二义性问题。例如,以下代码无法编译:

D d;
d.a1 = 1; 			// E0266	"D::a1" 不明确
A *p_a = (A *)&d; 	// C2594	“类型强制转换”: 从“D *”到“A *”的转换不明确

为了解决这个问题,需要显式地指定访问路径:

D d;
d.B::a1 = 1; 			// 或者d.C::a1
A *p_a = (A *)(B *)&d; 	// 或者(A *)(C *)&d

虚继承

虚继承通过引入虚基类表(vbtable)和虚表指针(vbptr)来解决菱形继承问题。我们先看一个虚继承的例子:

class A
{
public:
    int a1;
    int a2;
};

class B : public A
{
public:
    int b1;
    int b2;
};

class C : virtual public A
{
public:
    int c1;
    int c2;
};

对比B和C的类布局,可以看到两个主要差异:

  • 虚继承中,派生类布局的起始位置增加了vbptr指针,该指针指向vbtable
  • 虚继承中,基类的实例数据副本被放置在派生类的末尾

vbtable中的条目解释如下:

  • CdCvbptrC = 0 表示C类中,C的vbptr到C类入口的偏移量为0
  • CdCvbptrA = 16 表示C类中,C的vbptr到A类入口的偏移量为16

在虚继承中,通过查表和偏移的方式访问数据,虽然带来了额外的时间和内存开销,但解决了数据冗余和二义性问题。

虚继承——菱形继承

在菱形继承结构中,虚继承的优势更加明显:

class A
{
public:
    int a1;
    int a2;
};

class B : virtual public A
{
public:
    int b1;
    int b2;
};

class C : virtual public A
{
public:
    int c1;
    int c2;
};

class D : public B, public C
{
public:
    int d1;
    int d2;
};

在这种结构下,B和C的A类实例数据副本在D中被合并为一份。通过vbptr和vbtable,无论C*的来源是C类还是D类,都可以正确访问A类的数据。

虚表指针(vbptr)的位置

在某些情况下,vbptr的位置可能会发生变化。例如:

class A
{
public:
    int a1;
    int a2;
};

class B
{
public:
    int b1;
    int b2;
};

class C : virtual public A, public B
{
public:
    int c1;
    int c2;
};

在这种情况下,vbptr的位置从0变为8,位于基类B的成员之后。同时,vbtable中的偏移量也会相应调整。

共用虚基类表(vbtable)

当派生类同时进行虚继承和非虚继承时,如果非虚继承的基类中存在vbptr指针,派生类的虚继承会与之共用一个vbptr和vbtable。例如:

class A
{
public:
    int a1;
    int a2;
};

class B : virtual public A
{
public:
    int b1;
    int b2;
};

class C : virtual public A, public B
{
public:
    int c1;
    int c2;
};

在这种情况下,B和C会共用一个vbptr和vbtable。

参考资料

  • C++: Under the Hood
  • How virtual inheritance is implemented in memory by c++ compiler?
  • 深入理解C++ 虚函数表

本文发布于2024年4月2日
最后编辑于2024年4月2日

本文原文来自Cnblogs

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号