在虚继承/共享继承中,类对象,会额外增加一个表头指针——虚基类表。
这里设定的虚继承模型为:
class A
#if !defined(_A_H)
#define _A_H
/**
* \brief A类。
*/
class A
{
public:
virtual void TestA()
{
printf( "TestA\r\n" );
}
char cA;
};
#endif // _A_H
class B
#if !defined(_B_H)
#define _B_H
/**
* \brief B类。
*/
class B
{
public:
virtual void TestB()
{
printf( "TestB\r\n" );
}
char cB;
};
#endif // _B_H
class C : public virtual A, public virtual B
#if !defined(_C_H)
#define _C_H
#include "A.h"
#include "B.h"
/**
* \brief C类。
*/
class C : public virtual A, public virtual B
{
public:
virtual void TestC()
{
printf( "TestC\r\n" );
}
char cC;
};
#endif // _C_H
class D : public virtual A, public virtual B
#if !defined(_D_H)
#define _D_H
#include "A.h"
#include "B.h"
/**
* \brief D类。
*/
class D : public virtual A, public virtual B
{
public:
virtual void TestD()
{
printf( "TestD\r\n" );
}
char cD;
};
#endif // _D_H
class E : public C, public D
#if !defined(_E_H)
#define _E_H
#include "C.h"
#include "D.h"
/**
* \brief E类。
*/
class E : public C, public D
{
public:
virtual void TestE()
{
printf( "TestE\r\n" );
}
char cE;
};
#endif // _E_H
目的是想通过反汇编看一下,VC2003把虚继承如何实现,而在这期间,虚基类表起到什么作用。
根据以上模型,
首先,看一下多继承中,对象的内存布局。址址由低到高。
C的虚函数表
C的虚基类表
C的成员
D的虚函数表
D的虚基类表
D的成员
E的成员
A的虚函数表
A的成员
B的虚函数表
B的成员
f8 50 42 00 // + __vfptr0x004250f8 const E::`vftable'{for `C'}*
14 51 42 00 // 0x00425114 C's vbptr
03 cc cc cc // cC
ec 50 42 00 // + __vfptr0x004250ec const E::`vftable'{for `D'}*
04 51 42 00 // 0x00425104 D's vbptr
04 cc cc cc // cD
05 cc cc cc // cE
e0 50 42 00 // + __vfptr0x004250e0 const E::`vftable'{for `A'}*
01 cc cc cc // cA
d4 50 42 00 // + __vfptr0x004250d4 const E::`vftable'{for `B'}*
02 cc cc cc // cB
00411CF5 call A::TestA (4110B9h)
00411D04 call B::TestB (4113ACh)
00411D0C call C::TestC (41150Fh)
00411D14 call D::TestD (411069h)
00411D1C call E::TestE (411262h)
再看看虚函数表&虚基类表的内存情况。
+ __vfptr 0x004250d4 const E::`vftable'{for `B'} *
ac 13 41 00 // B::TestB (4113ACh)
00 00 00 00
cc 7a 42 00 //
+ __vfptr 0x004250e0 const E::`vftable'{for `A'} *
b9 10 41 00 // A::TestA (4110B9h)
00 00 00 00
e4 7a 42 00 //
+ __vfptr 0x004250ec const E::`vftable'{for `D'} *
69 10 41 00 // D::TestD (411069h)
00 00 00 00
c0 7b 42 00 //
+ __vfptr 0x004250f8 const E::`vftable'{for `C'} *
0f 15 41 00 // C::TestC (41150Fh)
62 12 41 00 // E::TestE (411262h)
00 00 00 00
0x00425104 D's vbptr
fc ff ff ff //
0c 00 00 00 // D对象转换为A对象时使用的内存偏移值(编译期间确定)
14 00 00 00 // D对象转换为B对象时使用的内存偏移值(编译期间确定)
00 00 00 00
0x00425114 C's vbptr
fc ff ff ff
18 00 00 00 // C对象转换为A对象时使用的内存偏移值(编译期间确定)
20 00 00 00 // D对象转换为B对象时使用的内存偏移值(编译期间确定)
00 00 00 00
10 7c 42 00 //
b9 10 41 00 // A::TestA (4110B9h)
00 00 00 00
60 7c 42 00 //
ac 13 41 00 // B::TestB (4113ACh)
00 00 00 00
78 7c 42 00 //
ac 13 41 00 // B::TestB (4113ACh)
00 00 00 00
90 7c 42 00 //
b9 10 41 00 // A::TestA (4110B9h)
00 00 00 00
cc 7c 42 00 //
0f 15 41 00 // C::TestC (41150Fh)
00 00 00 00
fc ff ff ff
08 00 00 00
10 00 00 00
00 00 00 00
好了,有了内存结构,以下来看一下,以各对象转换的汇编代码。
我也好久没有看汇编了,只记得部份。只在关键部份把注释加上,帮助阅读。
A* pA = &e;
00411B95 lea eax,[e]
00411B98 test eax,eax
00411B9A jne main+0A8h (411BA8h)
00411B9C mov dword ptr [ebp-164h],0
00411BA6 jmp main+0B8h (411BB8h)
00411BA8 mov ecx,dword ptr [ebp-2Ch]
00411BAB mov edx,dword ptr [ecx+4] // 此处,edx保存了18 00 00 00// C对象转换为A对象时使用的内存偏移值
00411BAE lea eax,[ebp+edx-2Ch]
00411BB2 mov dword ptr [ebp-164h],eax
00411BB8 mov ecx,dword ptr [ebp-164h]
00411BBE mov dword ptr [pA],ecx
B* pB = &e;
00411BC1 lea eax,[e]
00411BC4 test eax,eax
00411BC6 jne main+0D4h (411BD4h)
00411BC8 mov dword ptr [ebp-164h],0
00411BD2 jmp main+0E4h (411BE4h)
00411BD4 mov ecx,dword ptr [ebp-2Ch]
00411BD7 mov edx,dword ptr [ecx+8] // 此处,edx保存了20 00 00 00// C对象转换为B对象时使用的内存偏移值
00411BDA lea eax,[ebp+edx-2Ch]
00411BDE mov dword ptr [ebp-164h],eax
00411BE4 mov ecx,dword ptr [ebp-164h]
00411BEA mov dword ptr [pB],ecx
C* pC = &e;
00411BED lea eax,[e]
00411BF0 mov dword ptr [pC],eax // 这里不需要转换
D* pD = &e;
00411BF3 lea eax,[e]
00411BF6 test eax,eax
00411BF8 je main+105h (411C05h)
00411BFA lea ecx,[ebp-24h] // 这里编译时就已经知道D对象和E对象具体的偏移值,直接栈中照减
00411BFD mov dword ptr [ebp-164h],ecx
00411C03 jmp main+10Fh (411C0Fh)
00411C05 mov dword ptr [ebp-164h],0
00411C0F mov edx,dword ptr [ebp-164h]
00411C15 mov dword ptr [pD],edx
E* pE = &e;
00411C18 lea eax,[e]
00411C1B mov dword ptr [pE],eax
A* pCA = pC;
00411C1E cmp dword ptr [pC],0
00411C22 jne main+130h (411C30h)
00411C24 mov dword ptr [ebp-164h],0
00411C2E jmp main+146h (411C46h)
00411C30 mov eax,dword ptr [pC]
00411C33 mov ecx,dword ptr [eax+4]
00411C36 mov edx,dword ptr [ecx+4]
00411C39 mov eax,dword ptr [pC]
00411C3C lea ecx,[eax+edx+4]
00411C40 mov dword ptr [ebp-164h],ecx
00411C46 mov edx,dword ptr [ebp-164h]
00411C4C mov dword ptr [pCA],edx
A* pDA = pD;
00411C4F cmp dword ptr [pD],0
00411C53 jne main+161h (411C61h)
00411C55 mov dword ptr [ebp-164h],0
00411C5F jmp main+177h (411C77h)
00411C61 mov eax,dword ptr [pD]
00411C64 mov ecx,dword ptr [eax+4]
00411C67 mov edx,dword ptr [ecx+4]
00411C6A mov eax,dword ptr [pD]
00411C6D lea ecx,[eax+edx+4]
00411C71 mov dword ptr [ebp-164h],ecx
00411C77 mov edx,dword ptr [ebp-164h]
00411C7D mov dword ptr [pDA],edx
B* pDB = pD;
00411C83 cmp dword ptr [pD],0
00411C87 jne main+195h (411C95h)
00411C89 mov dword ptr [ebp-164h],0
00411C93 jmp main+1ABh (411CABh)
00411C95 mov eax,dword ptr [pD]
00411C98 mov ecx,dword ptr [eax+4]
00411C9B mov edx,dword ptr [ecx+8]
00411C9E mov eax,dword ptr [pD]
00411CA1 lea ecx,[eax+edx+4]
00411CA5 mov dword ptr [ebp-164h],ecx
00411CAB mov edx,dword ptr [ebp-164h]
00411CB1 mov dword ptr [pDB],edx
B* pCB = pC;
00411CB7 cmp dword ptr [pC],0
00411CBB jne main+1C9h (411CC9h)
00411CBD mov dword ptr [ebp-164h],0
00411CC7 jmp main+1DFh (411CDFh)
00411CC9 mov eax,dword ptr [pC]
00411CCC mov ecx,dword ptr [eax+4]
00411CCF mov edx,dword ptr [ecx+8]
00411CD2 mov eax,dword ptr [pC]
00411CD5 lea ecx,[eax+edx+4]
00411CD9 mov dword ptr [ebp-164h],ecx
00411CDF mov edx,dword ptr [ebp-164h]
00411CE5 mov dword ptr [pCB],edx
以下是转换后的地各地址。
大家可能会奇怪,为什么pE和pC的地址是一样的?这个是因为C是E的第一个基类。这个可以通过各种C++丛书找到答案。
而其他类对象的地址则是通过虚基类表转换而来,并非直接得出,所以会影响效率。
+ pC 0x0012feac {cE=0x05 '?' }C *
+ pD 0x0012feb8 {cE=0x05 '?' }D *
+ pE 0x0012feac {cE=0x05 '?' }E *
+ pA 0x0012fec8 {cE=??? }A * // 0x0012feac + 00 00 00 18
+ pB 0x0012fed0 {cE=??? }B * //0x0012feac + 00000020
+ pCB 0x0012fed0 {cE=??? }B *
+ pDB 0x0012fed0 {cE=??? }B *
+ pCA 0x0012fec8 {cE=??? }A *
+ pDA 0x0012fec8 {cE=??? }A *
总结一下,虚继承实际上就是通过对虚派生对象增加一个虚父类表指针,然后在向上转型时,通过此虚父类表找到向上转型时具体父类的偏移值,做:
父类对象 = ( this + 虚父类表偏移值 [ + sizeof(int)] ); // 当包括虚函数时。
运算得出具体向上转型父类对象。
本人理解不足的地方,希望大家指正,谢谢!