虚拟继承在继承权限前加上virtual关键字即可构成虚拟继承,如:
我们可以深入了解一下虚拟继承的对象模型;如现有一个基类B,含有一个公有类型的整形成员_b,派生类D公有继承基类B,且派生类D含有自己的一个整形数据成员_d,我们写一个测试函数来研究一下派生类D的对象模型:
通过编译器单步调试,可以先看一下基类B和派生类D的空间大小,在查看结果之前,我们可以先用自己目前所了解的知识分析一下他们的空间大小是多少,派生类D单继承基类B,基类B只有一个int类型的成员_b,所以基类B的空间大小应该是4字节,根据单继承的特点派生类D先拷贝基类B的内存空间,在加上自己的内存空间,即派生类D空间大小应该是基类的空间大小4字节加上B的int型_d数据成员的大小4字节,共8字节,因此我们得到的结果是:基类是4字节,派生类是8字节。
好了,让我们看一下运行结果:
这个结果貌似跟我们预想的并不一样,对于基类B是4个字节毫无疑问,可是派生类D为什么多4个字节,且多的4字节存放的是什么呢?
我们可以通过查看汇编代码了解一下编译器在后面到做了什么:
我们可以看到在定义基类对象时并没有汇编代码,说明这里仅仅是进行了声明,并没有进行什么具体的步骤,而在定义派生类对象B时,会发现多了三条汇编代码:第一步将1压栈,第二步取派生类对象_d的地址放到ECX寄存器中,第三步调用了派生类D的构造函数D:😄()。可是在这里我们并没有给出派生类D的构造函数,那么为什么编译器会调用呢?
这里就涉及到编译器会自动合成构造函数的第三种情况:虚拟继承时派生类会自动合成构造函数。我们再来总结一下编译器会自动合成构造函数的三种情况:
当一个类包含另外一个类的对象,且该对象有自己的缺省构造函数。(含子类情况)
当一个类继承另外一个类,且基类有缺省的构造函数。(派生类情况)
当一个类虚拟继承另外一个类,无论基类是否有缺省的构造函数,编译器都会自动合成派生类的构造函数。(虚拟继承情况)
回到我们的主题,那么这多出来的这三步,到底做了什么,我们接着运行,来看一下系统生成的派生类构造函数(D:😄())有哪些操作:
① 号汇编语句mov dword ptr [this],ecx,表示将寄存器ecx的值赋值给this指针(直接寻址),此时这里的ecx装的就是派生类对象_d的地址;即之前的三句汇编代码中第二句汇编代码所做的操作,将_d的地址放到ecx寄存器中。因此,使得this指针指向当前对象_d。
② 号汇编语句cmp dword ptr [ebp+8],0,通过寄存器间接寻址,将ebp堆栈基指针向下偏移8个字节,取其空间双字字节大小(dword ptr[])的内容,于0进行比较,ebp地址如下:
其向下偏移8字节后,所取到的内容为1,即有之前的那三句汇编语句中的第一句,将1压栈得到。
③ 号汇编语句je D::D+32h (0FA17F2h),表示上面的比较成立,即进行本次步骤
④ 号汇编语句mov eax,dword ptr [this],通过直接寻址将this地址空间内容前4个字节放到eax寄存器当中,即将对象_d的地址放到eax中。
⑤ 号汇编语句mov dword ptr [eax],offset D::`vbtable’ (0FA7B30h),将地址0x00FA7B30放到eax寄存器间接寻址(地址偏移eax内容的空间)后地址空间的前4个字节中,因为eax存放的是_d的地址,即将0x0FA7B30放到对象空间的前4个字节当中。
0x00FA7B30所指向的空间内容。
此时this指针所指向空间的内容(即对象_d空间内容)的前4个字节为0x00FA7B30。
⑥ 号汇编语句mov eax,dword ptr [this] ,最后通过直接寻址将this空间的内容,放到eax寄存器当中。
此时,派生类的构造函数D:😄()的基本汇编代码已经讲解完毕,同时将上面三条汇编语句的作用也详细的讲解了。整个过程大概来讲,就是编译器给派生类自动生成一个构造函数,并且在生成派生类对象,调用构造函数的同时,将1作为参数传给构造函数,在构造函数内部,将一个指针放在了对象内存空间的前4个字节。因此派生类多出来的4个字节就是该指针。
至于为什么多出来4个字节,我们已经有所了解,即多出来的4个直接又来存放指针,现在我们再来深入探究一下这个指针,所指空间的内容。
在上面可以看到指针0x00FA7B30所指向空间前4个字节的内容为0,后4个字节的内容为8,对于他们的作用,接着来运行的我们的代码进行测试,通过汇编来查看编译器底层的运行过程来看看赋值情况是怎样的:
对于派生类成员_d通过直接取值的方式进行赋值,而然对于从基类继承下来的_b的赋值,通过直接寻址先将对象d的前4个字节,存放到寄存器eax中,即取到那个多出来的未知指针,然后通过寄存器间接取值得到eax偏移4字节后所指空间的值,即得到未知指针向后偏移4字节所指的内容8,然后将8存放到exc寄存器中,第三步将要赋的值2赋值给d偏移ecx内容后所指向的地址空间。在此完成d._b = 2;的赋值。可以看出该指针像是一个存放偏移量的指针。
来查看一下此时派生类对象的对象模型(由于多次重新运行,导致对象的地址发生改变,但这并不会影响我们的结果):
可以看到对象当中首先是一个指针,通过上面我们可以得到,这是一个偏移量指针,第一个4字节内容存放的是派生类对象的地址偏移量,第二个4字节内容存放的是派生类当中基类对象的偏移量;因此该对象的第二个地址内容装的派生类自己的数据成员_d,而第三个地址存放的是从基类继承下来的数据成员_b。
最后,我们可以得到虚拟继承的对象模型:
对于虚拟继承的对象模型我们已经讨论清楚了,这里要注意的是虚拟继承看似单继承,但是与单继承完全不一样,因为虚拟继承的特点,派生类多了4字节的偏移量指针,并且与单继承不同的点是,在单继承中从基类继承下来的数据成员存放在低地址,而在虚拟继承中从基类继承下来的数据成员存放在到地址处。
部分摘自于https://www.2cto.com/database/201805/749268.html