首先,阅读该内容需要有继承的基本概念,以方便后续内容的理解。
一、菱形继承的原理
菱形继承的原理基于单继承和多继承,当多个继承了同一个父类的子类再被一个子类继承时,便可称为菱形继承。如下图:
从图可以看到菱形继承的大致原理,虽然叫菱形继承,但并非继承关系只呈现菱形,菱形只是这种继承的一种,千万不要被名字误导,不要被名字误导,不要被名字误导!重要的事情说三遍。
二、菱形继承的危害
了解了菱形继承的原理,和基本模式之后,你是否有了许多的奇思妙想?比如什么五边形继承,梭状继承,树状继承等等。那么当你把这些菱形继承的派生实现后,是不是会发现一些问题。
1.二义性
当源类中有一个变量时,如果你直接使用,编译器并不知道使用的是哪一个类的变量,只有显式指定访问才能解决。
2.数据冗余
上图中,虽然指定访问可以使得一些重复数据可以分开使用,模拟了如外号这种多name的场景,可是当源类中存在绝对的单一变量,如身份证号时,菱形继承的数据冗余性便暴露无疑,因为一个人不可能有多个身份证号!
三、菱形继承问题的解决
那么为了解决二义性和数据冗余的问题,C++3.0加入了虚继承的概念,即在继承的时加上virtual关键字,便可以形成虚继承,如下:
形成虚继承后,再次看向内存,便如下图:
可以看到,B1,B2这两个派生类中,因为使用虚继承,出现了两个指针(黑色边框),这里不买关子,这个指针被称为虚基表指针,那么顾名思义,被虚基表指针指向的那块区域当然就叫虚基表了。
虚基表中储存的便是派生类对基类变量的偏移量,细心的同学一定发现了,虚基表指针的地址加上虚基表中的偏移量,便是基类A的地址。
*如B1的虚基表指针地址0x00AFFC00+14=0x00AFFC14,即基类A的地址。
如此,数据的二义性与冗余被解决,无论是使用B1或者B2的A类变量,都是同一个变量!但是但是,有聪明的小伙伴可能会说,这不就是把多的冗余变量变成了一个变量,哪里需要整的这么花里胡哨的,直接将多余的变量不再分配内存就好了嘛?的确,这样很简洁暴力,可是暴露了很多问题。
1.切片时无法找到属于基类的数据。
2.分开赋值(泛型编程时)无法成功,因为不知道这个唯一基类变量属于哪一个派生类,又形成了一种二义性。
对比之下,虚基表的优势便显现了出来,无论是切片时还是赋值,派生类都可以通过偏移量找到基类的变量,就跟你爸和你妈都可以通过导航走不同路线,回到家一样。所以不得不说虚基表的设计很巧妙,👍。