什么是菱形继承?
假设有一个类A,它有两个子类,分别为类B和类C,再有一个类D又继承了B类和C类;如图:
类似于这种,有子类对象包含多份父类对象的继承模型称为菱形继承。
上述菱形继承体系中,类D多重继承了类B和类C,因此,类D含有两份基类A的成员;此种继承方式会造成两个问题:二义性和数据冗余。
先看一段代码:
#include<iostream>
using namespace std;
class A //基类A
{
public:
int _a;
};
class B:public A //子类B
{
public:
int _b;
};
class C:public A //子类C
{
public:
int _c;
};
class D :public B, public C //多重继承B和C的子类D
{
public:
int _d;
};
void test()
{
D d;
d._a = 10;
}
int main()
{
test();
system("pause");
return 0;
}
看一下编译结果:
可以看到:编译器无法确定_a是属于类B的还是类C的,这就是二义性问题。那如何解决这个问题?
可以通过添加域的访问限定符解决:
void test()
{
D d;
d.B::_a = 10;
d.C::_a = 20;
d._b = 30;
d._c = 40;
d._d = 50;
}
看一下是否赋值成功:
从监视窗口可看到,对类B和类C的_a都成功赋值。
但是还有另一个问题没有解决:数据的冗余。
因此,引入虚拟继承机制;虚继承可以同时解决菱形继承的二义性和数据冗余的问题。
我们需要在类B和类C继承基类A时加入virtual,这样保证了在子对象创建时,只保存了基类A的一份拷贝。
(C++使用虚拟继承,解决了从不同路径继承来的相同基类的数据成员在内存中有不同的拷贝造成数据不一致的问题,将共同基类设置为虚基类,这时从不同路径继承的虚基类在内存就只有一个映射。)
#include<iostream>
using namespace std;
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;
};
void test()
{
D d;
d._a = 10;
d._a = 20;
d._b = 30;
d._c = 40;
d._d = 50;
}
int main()
{
test();
system("pause");
return 0;
}
看一下监视窗口:
可以发现_a被赋值为20,并没有10的出现,说明在类D的对象模型中只存在一个_a。
在探索菱形虚拟继承的实现原理前,我们分别看一下派生类D的大小和菱形虚拟继承派生类D的大小:
菱形继承派生类D的大小:
菱形虚拟继承派生类D的大小:
可以看到两者的区别:菱形虚拟继承比菱形继承多了四个字节的大小。
我们可以通过内存看一下菱形虚拟继承中派生类D的对象在内存中的情况:
通过内存可以看出基类A的成员变量_a在类D中只有一份,因此也已经解决了数据冗余的问题;但是,内存中蓝色框中的地址存的是什么呢?
我们称它为虚基表地址。可以看下这个地址存放的是什么:
总图:
下面我们可以看一下菱形继承的对象模型和菱形虚拟继承对象模型:
菱形继承对象模型:
菱形虚拟继承对象模型:
总结:
虚拟继承虽然解决了菱形继承的产生的二义性和数据冗余问题,但是如同上述的例子为了解决一个int数据冗余,却开辟了两个存放虚基表的空间;而且访问虚基类数据时,要通过虚基表进行间接访问,效率就会比较低,带来了性能上的损耗,所以非必要的时候尽量避免菱形继承,尝试换一种设计模式(但是当数据冗余的程度很大时,使用虚拟继承会更)。