菱形继承和菱形虚拟继承(原理:虚基表)

博客介绍了C++继承体系中的单继承、多继承和菱形继承,重点阐述菱形继承带来的数据冗余和二义性问题,以及通过菱形虚拟继承解决数据冗余的方法,还讲解了虚基表的作用。此外,对比了is-a(继承)和has-a(组合)的关系,建议优先使用组合。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在继承体系中有单继承、多继承、和菱形继承,(菱形继承其实是多继承的一种特殊情况)。

单继承:一个子类只有一个直接父类时称这个继承关系为单继承
在这里插入图片描述
多继承:一个子类有两个及以上个直接父类称这个继承关系为多继承
在这里插入图片描述
菱形继承:多继承的一种特殊情况。一个子类有多个直接父类并且这些直接父类的父类是同一个父类
在这里插入图片描述

菱形继承带来的问题:

拿上面菱形继承的图中的例子来看:因为有子类 Assistant 是继承自多个父类的 (Student Teacher),那么多个父类又继承自同一个父类 Person,因此会导致来自总父类这样就引发了两个问题:
① 数据冗余:来自 Person 的成员变量有多份(在Assistant中有来自Student和Teacher分别继承自Person的成员变量,这两份是相同的)
② 数据二义性:在Assistant中对这些冗余的数据不能直接进行赋值,因为不知道是在给谁赋值

class Person
{
public:
	string _name; // 姓名
};
class Student : public Person
{
protected:
	int _num; //学号
};
class Teacher : public Person
{
protected:
	int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};
void Test()
{
	// Assistant中有两份来自Person继承给子类的成员_name;
	//这样会有二义性无法明确知道访问的是哪一个
	Assistant a;
	a._name = "Mark";
}

可以看到对 _name 访问不明确导致程序出错。
在这里插入图片描述
但是我们可以通过类名访问到不同的类中的成员分别进行赋值。这样就可以具体到给某一个类中成员赋值。比如上面的程序中 Test 可以修改如下:

void Test()
{
	Assistant a;
	a.Student::_name = "Mark";
	a.Teacher::_name = "lzl";
}

在这里插入图片描述
可以看到已经将来自不同的成员分别进行了赋值。这样就解决的数据二义性的问题。但是,这样并没有解决数据冗余的问题。那么接下来就是祭出菱形虚拟继承的时候了。


菱形虚拟继承是什么呢?
我们来看看下面没有虚拟继承的代码:

class A
{
public:
	int _a;
};
class B : public A
{
public:
	int _b;
};
class C : public A
{
public:
	int _c;
};
class D : public B, public C
{
public:
	int _d;
};

来看看对象模型中存的是什么:
在这里插入图片描述
再看看下面有虚拟继承的代码以及对象模型中存储着什么:

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;
};

对象模型:
在这里插入图片描述
可以看出通过菱形虚拟继承的对象模型中多出了几行不知道是什么的数据!!!
其实那就是一串地址:指向了虚基表的位置。
那么虚基表中存的是什么呢?
虚基表中存放的是一个偏移量,一个当前位置相对于公共父类成员的偏移量。
这样就可以通过偏移量来找到公共父类成员的位置并对其进行操作。

为什么菱形虚拟继承后的对象模型有 24 个字节,而菱形继承的对象模型有 20 个字节,看起来菱形虚拟继承的数据成员还比虚拟继承的数据成员多。
  其实这是因为我们这里用的数据量比较小的原因。当继承下来的数据变多时,那么差距就显示出来了,虚拟继承中的 B 和 D 中都会有一份继承自公共父类的数据成员,这样就有了两份相同的数据。
  而菱形虚拟继承中只是在 B 和 D 中多出了四个字节的虚基表的地址。继承自公共父类的数据成员只有一份。这样就解决了数据冗余的问题了。

那么为什么需要让 B 和 D 找到属于自己的继承自公共父类的成员呢?
原因就是为了在赋值的时候能够执行一个完整的切片的操作:看下面的代码:

D d;
B b = d;
C c = d;

就是为了让最终的子类给父类赋值时,要能够找出 B 或 C 成员中继承自 A 的成员,这样才能正确赋值。


is-a 和 has-a 的区别:

public继承就是一种 is-a 的关系,因为子类继承了父类,也就是说每个子类对象的都是父类对象。类继承允许根据其他类来定义一个类,也就是说通过父类来生成一个子类。这种生成子类的复用通常被称为 “ 白盒复用 ” 。“白盒” 是针对于可视性而言的,父类内部的细节实现在子类中是可见的。

组合就是一种 has-a 的关系,在B对象中组合另一个A对象,那么每个B对象都有了一个A对象。这要求A对象具有良好定义的接口。这样就是一个 “ 黑盒复用 ”,在B中并不能看到A的细节实现,只用到了A的接口。

那么继承和组合到底用哪个好呢?
其实主要还是根据需求来判断使用哪个。
继承的缺点:可以看做是破坏了封装,因为在子类中暴露的父类的实现细节。这样本来就违反了面向对象的三大特性之一。而且基类和派生类之间还有很强的依赖关系,耦合度高,这样也违反了 “ 高内聚,低耦合 ” 的特性。
而组合相比于继承来说并没有破坏封装,并且对象之间也没有很强的依赖性。这样针对于用户也比较友好。

总而言之,能使用组合就使用组合,尽量不要使用继承,除非你知道自己在做什么!

<think>嗯,用户问的是C++虚拟继承的组成元素,除了偏移量还有什么。首先,我需要回顾一下继承本概念。根据引用[2],继承用于解决菱形继承中的数据冗余问题,确保子类只有一份间接父类的数据。应该函数类似,存储一些必要的信息来支持继承的实现。 用户已经知道存储偏移量,但想知道还有什么其他内容。我记得在继承中,类子对象的位置是在派生类对象中动态确定的,因此需要某种机制在运行时计算偏移。这可能涉及到中存储不止一种偏移量。例如,可能存在两种偏移量:一种是派生类对象到类子对象的偏移,另一种可能是其他相关结构的偏移,比如函数指针类的偏移? 另外,可能还需要存储一些类型信息或者额外的标志位来帮助处理多个类的情况。不过,C++标准并没有规定具体的实现细节,这取决于编译器。常见的实现方式,比如在Visual C++中,可能包含多个偏移量,用来处理不同层次的继承关系。例如,类可能在派生类对象的不同位置,因此需要不同的偏移量来计算正确的地址。 还需要考虑指针本身的位置。在继承的类中,通常会在对象布局中添加一个指针,指向对应的中的每个条目可能对应不同的用途,比如第一个条目是当前派生类到类的偏移,第二个条目可能是顶层的类偏移,或者其他辅助信息。 不过,有没有可能除了偏移量之外,还包含其他类型的信息?例如,RTTI(运行时类型信息)的指针?但通常RTTI信息是存储在函数中的,而不是中。所以可能主要还是偏移量相关的数据。 总结一下,可能的组成元素包括: 1. 类子对象相对于当前对象的偏移量。 2. 类子对象相对于完整对象的偏移量(在多层继承中可能需要)。 3. 可能有多个偏移量来应对不同的继承层次结构。 需要验证这些猜想是否正确。根据引用[1]中的示例代码,菱形虚拟继承的情况下,D类的对象d1包含BC两个类,而它们都继承自A。在这种情况下,BC各自的可能存储了到A的偏移量。当D对象被构造时,BC的指针可能指向各自的,其中存储了从B或C到A的偏移量。此外,可能还有其他偏移量用于处理不同的情况,比如函数的存在或不同层次的结构。 因此,回答用户的问题时,应该指出除了偏移量之外,可能还包含其他偏移量,具体取决于编译器的实现,但通常主要存储的是偏移量信息,没有其他复杂的数据结构。不同的编译器可能有不同的实现方式,但核心功能是提供正确的偏移以定位类子对象。</think>在C++虚拟继承中,(Virtual Base Table)是编译器用于管理类子对象位置的核心机制。根据典型编译器的实现(如Visual C++GCC),除了存储**偏移量**外,还可能包含以下元素: 1. **多层级偏移量** 中可能包含**两个偏移量**: - **当前派生类到类子对象的偏移量**:用于在访问类成员时,从当前对象地址计算类子对象的实际地址。 - **类子对象到完整对象的偏移量**:在多层继承中,帮助确定类在最终派生类中的位置[^1][^2]。 例如,菱形继承时,`B``C`的分别存储从`B`或`C`到`A`的偏移量,而`D`的可能需要额外的偏移量支持。 2. **指针的间接寻址** 本身通过**指针(vbptr)** 访问,该指针存储在派生类对象中。的条目通过索引访问,例如: - 第一个条目:当前类到类的偏移量 - 第二个条目:类到完整对象的偏移量(若有需要)。 3. **编译器特定的元数据** 某些编译器可能在中添加**标志位或类型信息**,用于调试或动态类型识别(RTTI),但这类信息通常与函数(vtable)结合使用,而非独立存储在中。 ### 示例分析 以菱形继承为例: ```cpp class D : public B, public C { int d; }; ``` - `B``C`各自包含指针,指向其- `B`的存储从`B`到`A`的偏移量,`C`同理。 - 当通过`D`访问`A::a`时,需通过`B`或`C`的找到`A`的位置[^2]。 ### 总结 的核心内容是**偏移量**,但具体实现可能包含多个偏移量以支持复杂继承关系。不同编译器可能有细微差异,但均围绕动态定位类子对象展开。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值