C++中在构造类似下面这种继承关系时,
(其中箭头表示父类到子类的继承关系)
我们可以对类以下面的形式进行声明:
class A
{
public:
int _ma;
virtual void Tst1(){printf("A::Tst1\n");}
virtual void Tst2(){printf("A::Tst2\n");}
};
class B : virtual public A
{
public:
virtual void Tst1(){printf("B::A::Tst1\n");}
};
class C : virtual public A
{
public:
virtual void Tst2(){printf("C::A::Tst2\n");}
};
class D : public B, public C
{
//
};
注意上面的类继承声明中用到了virtual关键字,该关键字用于声明这是一个虚拟继承。如果去掉该关键字会发生什么呢?那么在D类中,按照正常继承情况下的内存布局,无疑将包含两份A类的占据内存,假设代码中添加D类对象访问_ma成员的代码,那么编译器将无法定位_ma的地址,因为它存在两份实例,且这在函数调用时也会产生令编译器模凌两可(ambigous)的错误,因为是调用(B::*)还是(C::*)不得而知。
接下来,编写几行测试代码,并根据其汇编代码,我们可以了解C++编译器(VC 6.0)在实现虚拟继承时所用到的特殊手段:
为了便于后面的分析,我这里直接给出D的内存模型,同样这是我自己根据汇编代码的出来的:
D one;
push 1 ;
lea ecx,[ebp-10h] ;这里说明sizeof(D) == 10h(16 = sizeof(_ma) + 3 * sizeof(vptr))
call @ILT+25(D::D) (0040101e) ;这里调用构造函数
A* pA = &one;
lea eax,[ebp-10h] ;%eax = address one
test eax,eax;测试指令,执行and 操作
jne main+32h (0040d9c2)
mov dword ptr [ebp-20h],0
jmp main+3Fh (0040d9cf)
mov ecx,dword ptr [ebp-10h];%eax = address one
mov edx,dword ptr [ecx+4];
lea eax,[ebp+edx-10h];
mov dword ptr [ebp-20h],eax;
mov ecx,dword ptr [ebp-20h];
mov dword ptr [ebp-14h],ecx ;
这里应该注意的是pA的内容并不与变量one的首地址相同,代码中进行了调整:
(1)获取(B::vptr)所指向的第二个表项的内容,该内容存储着偏移量(mov edx,dword ptr [ecx+4];)(该内容在实际中为8)
(2)获取one中属于A部分的基地址(lea eax,[ebp+edx-10h];)
(3)将(2)中获得的地址存储在变量pA 中(mov dword ptr [ebp-14h],ecx ;)
B* pB = &one;
lea edx,[ebp-10h]
mov dword ptr [ebp-18h],edx
从这里可以看出pB指针的值即为one变量的地址,这也说明在D的内存分布的起始端的内容为B
C* pC = &one;
lea eax,[ebp-10h]
lea ecx,[ebp-0Ch]
mov dword ptr [ebp-24h],ecx
mov edx,dword ptr [ebp-24h]
mov dword ptr [ebp-1Ch],edx
(1)获取one中C部分的内存地址(lea ecx,[ebp-0Ch])
(2)存储到pC中(mov dword ptr [ebp-1Ch],edx)
所以根据上面的分析,我们可以知道D类对象的内存布局为:
接下来分析一下函数调用的过程,这主要涉及到虚表的操作:首先将整个程序所用到的栈内存布局画出来,个别内存的内容还未知,但不影响分析
mov eax,dword ptr [ebp-14h]
mov edx,dword ptr [eax]
mov esi,esp
mov ecx,dword ptr [ebp-14h]
call dword ptr [edx]
(1)首先获得pA指针所指向的内存的内容(mov eax,dword ptr [ebp-14h]),该内容实际为D的虚表地址
(2)获得虚表指针所指向内存的首4字节内容(edx,dword ptr [eax])
(3)调用D::Tst1(动态绑定技术),调用成员函数时,其this指针默认通过exc寄存器传递,这里我们可以看出在D::Tst1的成员函数中this指针的值并不为one的地址
pA->Tst2();
mov eax,dword ptr [ebp-14h]
mov edx,dword ptr [eax]
mov esi,esp
mov ecx,dword ptr [ebp-14h]
call dword ptr [edx+4]
调用D::Tst2成员函数,其过程与调用D::Tst1过程类似,但其函数函数地址在D的虚表中第二项,这样验证了上图中右上角所描绘的虚表内容
pB->Tst1();
mov eax,dword ptr [ebp-18h]
mov ecx,dword ptr [eax]
mov edx,dword ptr [ebp-18h]
add edx,dword ptr [ecx+4]
mov eax,dword ptr [ebp-18h]
mov ecx,dword ptr [eax]
mov eax,dword ptr [ecx+4]
mov ecx,dword ptr [ebp-18h]
mov eax,dword ptr [ecx+eax]
mov esi,esp
mov ecx,edx
call dword ptr [eax]
(1)获得pB的内容(mov eax,dword ptr [ebp-18h]; mov ecx,dword ptr [eax]),它是一个虚表指针
(2)获得虚表的第二项内容(mov eax,dword ptr [ecx+4]),它是一个偏移量,它等于D对象的虚表指针离到B部分起始地址的距离
(3)通过偏移量定位到虚表的地址(mov eax,dword ptr [ecx+eax])
(4)传递this指针,这里它的值同样不等于one的起始地址,而是one内存块中B部分起始地址(通过edx寄存器传递,在前4句汇编代码中获得)
(5)调用D::Tst1函数
/下面代码的分析与上面类似
pB->Tst2();
mov ecx,dword ptr [ebp-18h]
mov edx,dword ptr [ecx]
mov ecx,dword ptr [ebp-18h]
add ecx,dword ptr [edx+4]
mov eax,dword ptr [ebp-18h]
mov edx,dword ptr [eax]
mov eax,dword ptr [edx+4]
mov edx,dword ptr [ebp-18h]
mov eax,dword ptr [edx+eax]
mov esi,esp
dword ptr [eax+4]
//
pC->Tst1();
mov ecx,dword ptr [ebp-1Ch]
mov edx,dword ptr [ecx]
mov ecx,dword ptr [ebp-1Ch]
add ecx,dword ptr [edx+4]
mov eax,dword ptr [ebp-1Ch]
mov edx,dword ptr [eax]
mov eax,dword ptr [edx+4]
mov edx,dword ptr [ebp-1Ch]
mov eax,dword ptr [edx+eax]
mov esi,esp
call dword ptr [eax
/
pC->Tst2();
mov ecx,dword ptr [ebp-1Ch]
mov edx,dword ptr [ecx]
mov ecx,dword ptr [ebp-1Ch]
add ecx,dword ptr [edx+4]
mov eax,dword ptr [ebp-1Ch]
mov edx,dword ptr [eax]
mov eax,dword ptr [edx+4]
mov edx,dword ptr [ebp-1Ch]
mov eax,dword ptr [edx+eax]
mov esi,esp
call dword ptr [eax+4]
相关的优秀博文:http://blog.csdn.net/littlehedgehog/article/details/5442430