1、 单个虚拟继承
只是为了分析而已,实际中并没有太大的作用。跟虚拟继承相关的派生类对象的内存布局跟具体的编译器相关。
(1)VS编译器:无论有无虚函数,必然含有虚基类表指针。虚基类表中的内容为本类实例的偏移和基类实例的相对偏移值。如果有虚函数,那么基类的虚函数表跟派生类的虚函数表是分开的。
在内存布局上,地址从低到高,顺序如下:派生类的虚函数表指针+虚基类表指针+派生类的成员变量+“间隔”(4个字节)+基类的虚函数表指针+基类的成员变量。派生类跟基类实例的位置关系跟普通继承正好相反。
说明:“间隔”产生的原因是派生类重写了基类的虚函数。如果没重写,则这一项没有。"本类地址"指的是包含有虚基类的对象(或部分对象),也就是继承链上的直接子类对象的地址,本例比较简单,就是派生类对象地址。“本类地址跟虚基类表指针地址只差”,这个值经常是-4、0,-4表明“本类”还有一个虚函数表指针;0则表明“本类”的第一个4字节保存的就是虚基类表指针,没有虚函数表指针。
图 1 VS编译器—单个虚拟继承
(2)GNU的GCC编译器:跟VS的编译器类似,有不同的地方是,虚基类表跟派生类的虚函数表合并。另外通过虚基类表指针往正负两个方向寻址,可以获得不同偏移值,也就是说有两个功能一样的虚函数表。不过在实际应用的时候,不知道虚基类表是否真的有用,测试了简单的情况发现编译器做了优化,根本就没有用虚基类表来寻址虚基类实例。
图 2 GCC编译器—单个虚拟继承
2、 虚拟继承多个基类
虚基类表要增加内容,有N个虚基类就有N项基类实例偏移值,再加上1项本类实例的偏移值,也就是N+1。
假设C虚拟继承了A类和B类,考虑最复杂的情况(都有虚函数),那么C类对象的内存布局如下
(VS编译器):
C类虚函数表指针+虚基类表指针+C类成员变量+A类间隔(4个字节) + A类虚函数表指针+ A类成员变量+ B类间隔(4个字节)+B类虚函数表指针+ B类成员变量。
说明:当派生类重写了该基类的虚函数,才会有“间隔”。“间隔”属于虚函数被重新实现了的虚基类,可能是一个标志,也有可能是在函数调用的时候用上。不是很清楚。
图 3 VS编译器—虚拟继承多个基类
(GCC编译器):
C类虚函数表指针(包含虚基类表) + C类成员变量 + A类虚函数表指针 + A类成员变量 + B类虚函数表指针 + B类成员变量。
相比较执行,使用GCC编译器,派生类对象小一些。(图略)
3、 虚拟继承之菱形继承
这里的菱形继承指的是:B、C虚拟继承A,然后D普通继承B、C。
D类的对象的内存布局如下
(VS编译器)
B类虚函数表指针(该虚函数表包含D类独有的虚函数的地址)+B类虚基类表指针+B类成员变量+C类虚函数表指针+C类虚基类表指针+C类成员变量+D类成员变量+“间隔”+A类虚函数表指针+A类成员变量。
说明:如果A类的虚函数没有被重写,那么就没有“间隔”。
图 4 VS编译器—菱形继承
(GCC编译器)
把B、C类的虚函数表跟虚基类表合并就是了。(图略)
4、VS编译器,“间隔”的疑问
“间隔”的问题,在没有虚函数的情况下,重写是没有“间隔”的,所以觉得可能跟虚函数有关,也就是说是为了实现多态,具体是用在哪个地方,做了简单的反汇编调试(父类指针指向子类对象,调用被子类重写了的虚函数),并没有发现哪里用到了“间隔”,可能要在复杂的调用才会用上吧,目前搞不清楚。
5、虚基类表的问题
通过反汇编调试发现在使用多态的时候,VS编译器会去使用虚基类表,用于寻址虚基类地址。而GCC编译器则没有这么做,测试了比较简单的情况,发现它做了优化,并没有利用虚基类表,而是直接在派生类对象地址上加上一个常数,获得虚基类实例的地址。
总结:
1、对于单一继承,不管继承链有多长,都是通过叠加的方式添加成员变量,直到有包含虚函数的类(称为A)存在,开始在这个类中存在vfptr,如果A类作为父类,那么它继承的子类都存在一个vfptr.
2、对于多重继承来(包括重复继承,也就是超类一样的情况)说,如果一直没有虚拟函数,那么都是父类的叠加,如果存在了虚拟函数,那么就存在了vfptr,在子类中,vfptr的个数最多核父类的个数一样,父类中一旦存在vfptr,那么子类中的虚函数就和第一个存在vfptr的父类进行融合,子类中vfptr个数最少为0,也就是所有的父类、超类,子类都没有虚拟函数
3、虚拟继承,虚拟继承存在的意识是为了解决重复继承的问题,虚拟继承后可以保证超累在子类中存在一份,首先明确一点就是,在哪个子类以virtual方式继承,那么在这个子类中就多一个虚基类的指针,剩下的方式就是和一般继承方式一样。可以参考上一篇文章。
http://www.cnblogs.com/cswuyg/archive/2010/08/20/1804113.html