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

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

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

菱形继承带来的问题:

拿上面菱形继承的图中的例子来看:因为有子类 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的接口。

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

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

你可能感兴趣的:(c++)