本篇要分享的内容是C++中多继承的缺陷:菱形继承。
以下为本篇目录
目录
1.多继承的缺陷与解决方法
2.虚继承的底层原理
3.虚继承底层原理的设计原因
首先观察下面的图片判断它是否为多继承
这实际上是一个单继承,单继承的特点是一个子类只有一个直接继承的父类,即使又多层继承关系,但是只有一个直接父类,都称作单继承
多继承的图示如下
可以看到多继承中的子类扮演了两个角色,就相当于桃花既能开出好看的桃花,也能结果。
所以多继承的特点是一个子类有两个或以上的直接父类时称这个关系叫做多继承。
那在上图中使用多继承是没有错误的,他可以在一个类中结合多个类的特点,多继承的本身并没有错误,但是出错的往往是在一些使用场景下会有缺陷,如下图
有了多继承可能就会导致菱形继承(如上图)。
可以看到Student类和Teacher类都会继承Person中的属性,
但是此时Assistant同时又继承了Student类和Teacher类的话,Person中的属性在Assistant中就会出现两次,会有二义性。
这也是为什么java语言中没多继承用法的原因。
观察如下代码
#include
using namespace std;
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 a;
a._name = "peter";
// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
}
可以看到在报错中它有规定使用的访问限定符,也就是说它会将同样的属性及信息再继承一份,也就是同时具有两份数据信息从而导致数据冗余占用空间,
如果Person类的空间很大,那么浪费的空间会更大。
那如何解决这样的问题呢?
首先我们可以使用访问限定符解决二义性
其次是在出现菱形继承的玩儿法之后C++祖师爷又更新了一个关键字:virtual(虚拟)
我们只需要在被多继承的类的继承方法前加上virtual,即可使用虚继承,
#include
using namespace std;
class Person
{
public:
string _name; // 姓名
};
class Student : virtual public Person
{
protected:
int _num; //学号
};
class Teacher : virtual public Person
{
protected:
int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; // 主修课程
};
void Test()
{
// 这样会有二义性无法明确知道访问的是哪一个
Assistant a;
a._name = "peter";
// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
}
使用了虚继承之后就可以调用冗余数据的属性了
可以看到,使用了虚继承之后不管是Student类中的_name还是Person类中的_name都不管用了,使用a直接可以调用_name,并且所用的空间也是同一份地址空间。
这样就解决了在菱形继承中数据冗余的问题。
但是在一个庞大的项目中这样的问题语法依旧会坑害不少人,所以尽量的能少用多继承,就要少用多继承。
既然要了解菱形继承的底层原理,我们不妨设计一个简单一点的代码便于观察,代码如下
class A
{
public:
int _a;
};
class B : public A
//class B : virtual public A
{
public:
int _b;
};
class C : public A
//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;
}
那上面的菱形继承关系也很简单,如下图
除了观察菱形继承外,main函数中的内容对菱形继承的测试也同样重要;
我们将代码调试,并观察内存窗口。
可以看到的是在B类存放了两个两个值,1和3
C类中也存放了两个值,2和4
D类中存放了一个值2,他与对象d中修改_d的值相同;
那这样存放数据是什么意思呢?
上面的代码没有使用虚函数,所以存放了两个值,导致了数据的二义性;
接下来我们使用虚函数,虚函数可以解决数据冗余和二义性的问题,我们继续观察内存的变化
class A
{
public:
int _a;
};
//class B : public A
class B : virtual public A
{
public:
int _b;
};
//class C : public A
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类中存放了两行数据:第一行为一个地址,第二行为所修改的数据;
在C类中,存放的数据与B类相似
D和A中的数据被修改为最后所修改的数据;
可以看到在B类和C类中将地址取代了第一次所存的数据,从而达到解决数据二义性的目的;
那这个存放的地址又是什么意思是呢?
我们继续再调出一个监视内存的窗口来观察
再第二个观察内存的窗口中输入B类的地址,这时你就会发现在第二个内存表中存在一个数,这个数子就是距离最终修改a的偏移量,上图为十六进制的14
当我们将B类中的第一行存放的地址加上十六进制的14,就会得到_a最终的值,_a=0;
我们再来举出一组例子来证明不是巧合
可以看到_a只被赋值,而B中不仅存放了_b的值,同样也存放了一个指针,指向了距离A的偏移量,也同样是将B类中第一行的地址加上指针所指的偏移量(8),就是1所在的位置。
以上就是设计的原理,虽然设计很多内存和地址的关系,但是这就是虚函数底层的实现设计。
那为什么要这么设计呢?
如以下情况
一个B类创建的指针会指向bb对象,也有可能指向d对象,
所以我们无法得知这个指针所指向的对象,就只能靠指针来检查另一块内存上所存放的偏移量,通过计算偏移量来计算虚继承中二义性的变量。
以上就是菱形继承的设计缺陷以及后序的设计的解决思路,以及解决思路的底层设计。
其实多继承本身没有问题,只是菱形继承的用法让多继承成为了大坑。
即使本人水平有限,尽管不遗余力但本篇对虚继承的探索仍有不足,还请读者指正,感谢您的阅读。