我们知道面向对象的三大特性分别为封装,继承,多态。在继承中,我们知道一个类可以继承另一个类,这样的关系被叫做子类(派生类)继承父类(基类),并且子类可以使用到父类的接口。但是在C++中还被设计了一个多继承,是什么意思呢。指的就是一个类可以继承多个类,就好比一个人可能他在这个学校担任老师,但是在另一边他可能是某个资历更深老师的某个学生,这样也产生了,如果一个老师类里面有名称了,学生里面也有一个名称,不可能他担任老师的时候叫做张三,担任学生的时候又叫做王五了。这里也就是二义性。但是我们可以通过对数据的指定性来解决二义性的问题,例如写下这样一段代码
代码中写到,B继承了A,C继承了A,D继承了B,C,且作为基类中的A有一个_a的成员变量。可以大致把他们理解为这样的关系
这就是一个菱形继承。接下来我们通过内存去看它们
通过对d的取地址观察我们可以看到确实是解决了数据二义性,但是数据的冗余是如何去处理的呢。这里我们可以用到一个虚继承,虚继承主要解决的就是命名冲突和数据冗余的问题,只保留一份基类的成员,关键字是virtual。当我们把关键字加上去之后再去对d取地址看到的内容就是这样的
当把虚继承关键字加上去再去观察内存的时候明显发现,此时B,C类中原本为A的数据区被写道了最后一个位置,而原本的位置变成了一个地址,如果我们通过地址找过去看看又会发现什么
通过地址过去之后发现第一个数据是00 00 00 00是因为它需要给其它的值预留空间,但是第二个值分别是十六进制的14,0c,对应的也就是十进制的20,和12。但是这两个值能代表什么呢。如果我们对比一下就能发现,从地址开始,B便宜20字节之后刚好是到A类,而C类偏移12字节后也恰好是到A类。
那么这个偏移量是用来干什么的呢。我们拿这样一段代码来举例。
在没有使用虚继承之前它的存储方式会是这样
但是当使用了虚继承之后它的模型也会跟着发生变化
可以看到b对象的模型也跟着发生了变化,也是一个指针指向一块区域,区域内存放着偏移量。
此时,我们利用B类定义一个对象,然后在用B类定义一个指针,让指针指向这个对象并且对对象中的_a进行操作,然后在让这个指针指向对象d,同样对d对象中的_a进行操作,那么此时的指针是无法知道具体指向谁的,它可能指向b对象,也可能指向d对象,所以这是就会利用偏移量,加上偏移量的值之后无论是b还是d都可以找到_a。
当我们从反汇编的角度来看它的时候,也可以看到它是先利用当前的地址去加上偏移量来找到_a
可能会有疑惑,为什么不能把地址直接存在对象里面,却偏偏要弄一个地址出来去存到那里,因为是要考虑到如果里面有多个值的情况,一个值就要一个地址,多个值就会造成对象空间的变大,所以会用一个指针来维护,管理一块区域。我们可以通过多加一个对象来观察确定。