C++的继承中有一种复杂的继承方式,这就是菱形继承。
菱形继承
(1)什么是菱形继承?
假设现在有四个类,分别是A、B、C、D四个类。如果B类和C类同时继承于A类,并且D类又同时继承于B类和C类,那么这四个类之间的关系就叫做菱形继承。可以用下面的图来表示。
(2)菱形继承的问题
菱形继承会存在两个问题,一个是数据冗余,另外一个是二义性。
我用一段简单的代码来说明这两个问题:
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;
};
1. 为什么说会存在数据冗余?
首先B类和C类都继承了A类,所以B类和C类中都一定包含A类的成员变量a,也就是说B类中有一个int a
,同样C类中也有一个int a
。那么当D类继承B类和C类后,那么在D类中一定包含两个int a
,其中一个来自B类,另外一个来自C类。这就是所谓的数据冗余。
2. 为什么说会造成二义性?
根据上面的分析,D类中存在两个int a
。所以当我们想要用D类的对象访问int a
时,那么编译器就不知道究竟访问的是哪一个a
。究竟访问的是从B类继承的a
,还是访问的是从C类继承的a
,这就是所谓的二义性。当然对于二义性我们可以用“类名::
”的方式来解决。如下所示:
cout<<d1.B::a<<endl; //这访问的继承于B类的a(其中d1是D类的对象)
cout<<d1.C::a<<endl; //这访问的继承于C类的a(其中d1是D类的对象)
但是这样的办法却解决不了数据冗余的问题,所以就有了虚拟继承。
虚拟继承
虚拟继承不仅解决了菱形继承中的数据冗余问题,还解决了二义性问题。
(1)虚拟继承的方式
我在上面的代码上进行一些修改,这样可以增加对比性。
class A
{
public:
int a;
};
class B:virtual public A //仅仅是比之前多加了一个关键字virtual
{
public:
int b;
};
class C:virtual public A //仅仅是比之前多加了一个关键字virtual
{
public:
int c;
};
class D:public B,public C
{
public:
int d;
};
解析:
在上述代码中,当B类和C类从A类派生出来时,我们使用virtual关键字将A类声明为虚基类。这样从B类和C类派生词来的子类D,在继承时只是继承了A类的一份,也就是只保留了A类的成员变量a一份。于是便很好的解决了数据冗余和二义性问题。
(2)虚拟继承的原理
我结合下面一段代码来具体解释。
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;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
首先看下面这张图:这张图片是没有使用虚拟继承时候的内存情况
在没有使用虚拟继承时,在内存中从上到下分别存储了从B类中继承下来的成员(包括int _a 和int _b
)、从C类中继承下来的成员(包括int _a和int _c
)以及D类中新增的成员(int _d
)。
下面这张图是使用了虚拟继承的内存状态图:
使用了虚拟继承之后,在B类和C类的同名成员的位置发生了改变,由原来的具体的值变成了一个地址,这其实就是一个指针,我们把这个指针叫做虚基表指针。而这个地址表示的就是一个虚基表的首地址。而在虚基表中存放的是内容是:当前位置相当于公共父类成员的偏移量。
举个例子:
假设现在要访问D类中的成员变量int a
。那么编译器首先会找到该类对象的内存空间,然后在此空间寻找a
的位置,但是当他找到a
的位置时,发现原本应该是具体数值结果变成了一个地址,也就是说原本的值被一个虚基表指针所取代。然后编译器会根据这个虚基表指针找到这个虚基表的首地址,然后根据虚基表中的内容,最后才找到实际上a
的具体地址,然后访问a
。