副标题#e#
这次我们看看菱形布局的虚担任。虚担任的引入本就是为了办理巨大布局的担任体系问题。上一篇我们在接头虚担任时用的是一个简朴的担任布局,只是为了打个铺垫。
我们先看看这几个类,这是一个典范的菱形担任布局。C100和C101通过虚担任共享同一个父类C041。C110则从C100和C101多重担任而来。
struct C041
{
C041() : c_(0x01) {}
virtual void foo() { c_ = 0x02; }
char c_;
};
struct C100 : public virtual C041
{
C100() : c_(0x02) {}
char c_;
};
struct C101 : public virtual C041
{
C101() : c_(0x03) {}
char c_;
};
struct C110 : public C100, public C101
{
C110() : c_(0x04) {}
char c_;
};
运行如下代码:
PRINT_SIZE_DETAIL(C110)
功效为:
The size of C110 is 16
The detail of C110 is 28 c3 45 00 02 1c c3 45 00 03 04 18 c3 45 00 01
我们可以象上一篇一样,画出工具的内存机关。
|C100,5 |C101,5 |C110,1 |C041,5 |
|ospt,4,11 |m,1 |ospt,4,6 |m,1 |m,1 |vtpt,4 |m1 |
(注:为了不折行,我用了缩写。ospt代表偏移值指针、m代表成员变量、vtpt代表虚表指针。第一个数字是该区域的巨细,即字节数。只有偏移值指针有第二个数字,第二个数字就是偏移值指针指向的偏移值的巨细。)
可以看到工具的内存机关中只有一个C041,即祖父类的部门只有一份,且放在最后头。这就是菱形担任。比拟前面几篇的接头,我们可以知道,假如没有用虚担任机制,那么在C041工具的内存机关中会呈现两份C041部门,这也就是所谓的V型担任。相应的工具机关为:C041+C100+C041+C101 +C110。在V型担任中是不能直接从C110,即孙子类,直接转型到C041,即祖父类的。因为在工具的机关中有两份祖父类的实体,一份从C100而来,一份从C101而来。编译器在决策时会存在二义性,它不知道转型后到底用哪一份实体。固然可以通过先转型到某一父类,然后再转型到祖父类来办理。但利用这种要领时,假如改写了祖父类的成员变量的内容,runtime是不会同步两个祖父类实体的状态,因此大概会有语义错误。
#p#副标题#e#
我们再阐明一下上面的内存机关。普通担任的机关,顶层类在前面。多重担任时则按从左到右的顺序排。从C100和C101到C110的担任是普通担任,所以遵循这个原则,先是左父类再右父类,接下去是子类。而虚担任则要求将共享的父类放到整个工具机关的最后(纵然虚父类没有被真正的共享也是如此,前在一篇的C020类就是这样。不知道打开优化开关后会不会有变革。)所以在上例中的祖父类也是被置于最后的。
我们再看看对成员的会见环境。运行以下代码并查察相应的汇编代码。
C110 c110;
c110.c_ = 0x51;
c110.C100::c_ = 0x52;
c110.C101::c_ = 0x52;
c110.C041::c_ = 0x53;
c110.foo();
对应的汇编代码为:
01 00423993 push 1
02 00423995 lea ecx,[ebp+FFFFF7F0h]03 0042399B call 0041DE60
04 004239A0 mov byte ptr [ebp+FFFFF7FAh],51h
05 004239A7 mov byte ptr [ebp+FFFFF7F4h],52h
06 004239AE mov byte ptr [ebp+FFFFF7F9h],52h
07 004239B5 mov eax,dword ptr [ebp+FFFFF7F0h]08 004239BB mov ecx,dword ptr [eax+4]09 004239BE mov byte ptr [ebp+ecx+FFFFF7F4h],53h
10 004239C6 mov eax,dword ptr [ebp+FFFFF7F0h]11 004239CC mov ecx,dword ptr [eax+4]12 004239CF lea ecx,[ebp+ecx+FFFFF7F0h]13 004239D6 call 0041DF32
前3行是工具的初始化,挪用了工具的结构函数。4、5、6行是对子类、阁下父类的成员变量的赋值。我们可以看到是直接写的,因为这一层的担任是普通担任。第7、8、9行是对祖父类成员变量的赋值,和上篇接头过的一样,是通过偏移值指针指向的偏移值来间接会见的。最后的4行指令是对成员函数的挪用。我们可以看到挪用的函数地点是直接给出的(最后一行),因为我们是通过工具来挪用,纵然是虚函数挪用也不会有多态的行为。可是获得this指针的方法却是颇为间接,即第10、11、12行。因为这个函数在祖父类中界说,那么它操纵的数据成员应该是祖父类的。因此编译器要调解this指针的位置。而祖父类又是被虚担任,因此要通过偏移值指针指向的偏移值来举办调解。
再调查一下第9行和第12行,可以看到计较出来的地点值是纷歧样的。这是因为第9行为给祖父类的成员变量赋值,而祖父类中有虚表指针存在,所以在获得工具的起始地点后,编译器给它加了4字节的偏移量以跳过虚指针。实际的获得地点的运算为: [ebp+ecx+FFFFF7F0h+4h],编译器在生成代码时会直接把最后一步运算做掉。
#p#分页标题#e#
我们再看一个例子,这个例子的担任布局和上一篇中是一样的,也是菱形布局。差异的是,每一个类都重写了顶层类声明的虚函数。代码如下:
struct C041
{
C041() : c_(0x01) {}
virtual void foo() { c_ = 0x02; }
char c_;
};
struct C140 : public virtual C041
{
C140() : c_(0x02) {}
virtual void foo() { c_ = 0x11; }
char c_;
};
struct C141 : public virtual C041
{
C141() : c_(0x03) {}
virtual void foo() { c_ = 0x12; }
char c_;
};
struct C150 : public C140, public C141
{
C150() : c_(0x04) {}
virtual void foo() { c_ = 0x21; }
char c_;
};
首先我们运行下面的代码,看看它们的内存机关。
PRINT_SIZE_DETAIL(C041)
PRINT_SIZE_DETAIL(C140)
PRINT_SIZE_DETAIL(C141)
PRINT_SIZE_DETAIL(C150)
功效为:
The size of C041 is 5
The detail of C041 is f0 c2 45 00 01
The size of C140 is 14
The detail of C140 is 48 c3 45 00 02 00 00 00 00 44 c3 45 00 01
The size of C141 is 14
The detail of C141 is 58 c3 45 00 03 00 00 00 00 54 c3 45 00 01
The size of C150 is 20
The detail of C150 is 74 c3 45 00 02 68 c3 45 00 03 04 00 00 00 00 64 c3 45 00 01
和前面的机关差异之处在于,共享部门和前面的非共享部门之间多了4字节的0值。只有共享部门有虚表指针,这是因为派生类都没有界说本身的虚函数,只是重写了顶层类的虚函数。我们阐明一下C150的工具机关。
|C140,5 |C141,5 |C150,1 |zero,4 |C041,5 |
|ospt,4,15 |m,1 |ospt,4,10 |m,1 |m,1 |4 |vtpt,4 |m1 |
(注:为了不折行,我用了缩写。ospt代表偏移值指针、m代表成员变量、vtpt代表虚表指针。第一个数字是该区域的巨细,即字节数。只有偏移值指针有第二个数字,第二个数字就是偏移值指针指向的偏移值的巨细。)
再看函数的挪用:
C150 obj;
PRINT_OBJ_ADR(obj)
obj.foo();
输出的工具地点为:
obj's address is : 0012F624
最后一行函数挪用的代码对应的汇编代码为:
00423F74 lea ecx,[ebp+FFFFF757h]00423F7A call 0041DCA3
单步执行后,我们可以看到ecx中的值为:0x0012F633,这个地点也就是obj工具机关中的祖父类部门的起始地点。通过上面的机关阐明我们知道 C150起始的偏移值指针指向的值为15,即工具起始到共享部门(祖父类部门)的偏移值。上面输出的obj起始地点为0x0012F624加上十进制的 15后,正好是我们看到的ecx中的值0x0012f633。
由于函数挪用是浸染于工具上,我们看到第二行的call指令是直接到地点的。
在这里令人狐疑的问题是,我们知道ecx是用来通报this指针的。在前一篇中,我们阐明白在C110工具上的foo要领挪用。在谁人例子中,由于 foo是顶层类中界说的虚函数,而且没有被下面的派生类重写,因此通过子类工具挪用这个要领时,编译器发生的代码是通过子类起始的偏移指针指向的偏移值来计较出祖父类部门的起始地点,并将这个地点做为this指针所指向的地点。可是在C150类中,foo不再是从祖父类担任的,而是被子类本身所重写。照理这时的this指针应该指向子类的起始地点,也就是0x0012F62E,而不是ecx中的值0x0012F633。
我们跟进去看看C150::foo()的汇编代码,看它是奈何通过指向祖父类部门的this指针,来定位到子类的成员变量。
01 00426C00 push ebp
02 00426C01 mov ebp,esp
03 00426C03 sub esp,0CCh
04 00426C09 push ebx
05 00426C0A push esi
06 00426C0B push edi
07 00426C0C push ecx
08 00426C0D lea edi,[ebp+FFFFFF34h]09 00426C13 mov ecx,33h
10 00426C18 mov eax,0CCCCCCCCh
11 00426C1D rep stos dword ptr [edi]12 00426C1F pop ecx
13 00426C20 mov dword ptr [ebp-8],ecx
14 00426C23 mov eax,dword ptr [ebp-8]15 00426C26 mov byte ptr [eax-5],21h
16 00426C2A pop edi
17 00426C2B pop esi
18 00426C2C pop ebx
19 00426C2D mov esp,ebp
20 00426C2F pop ebp
21 00426C30 ret
公然,由于此时指针指向的不是子类的起始部门(而是祖父类的起始部门),因为是通过减于一个偏移值为向前定位成员变量的地点的。留意第15行,这时 eax中存放的是this指针的值,写入值的地点是[eax-5],团结前面的工具机关和工具的内存输出,我们可以知道this指针的值(此时指向祖父类 C041的起始部门)减去5个字节(4字节的0值和1字节的成员变量值)后,恰好是子类C150的起始地点。
为什么不直接用子类的地点而是通过祖父类的起始地点间接的举办定位?这牵涉到编译内部的实现限制和对一系统问题的全面的领略。只是通过阐明现象很难找到谜底。
我们再通过指针来挪用一次。
C150 * pt = &obj;
pt->foo();
#p#分页标题#e#
第二行代码对应的汇编指令为:
01 00423F8B mov eax,dword ptr [ebp+FFFFF73Ch]02 00423F91 mov ecx,dword ptr [eax]03 00423F93 mov edx,dword ptr [ecx+4]04 00423F96 mov eax,dword ptr [ebp+FFFFF73Ch]05 00423F9C mov ecx,dword ptr [eax]06 00423F9E mov eax,dword ptr [ebp+FFFFF73Ch]07 00423FA4 add eax,dword ptr [ecx+4]08 00423FA7 mov ecx,dword ptr [ebp+FFFFF73Ch]09 00423FAD mov edx,dword ptr [ecx+edx]10 00423FB0 mov esi,esp
11 00423FB2 mov ecx,eax
12 00423FB4 call dword ptr [edx]13 00423FB6 cmp esi,esp
14 00423FB8 call 0041DDF2
喔!越发迂回了。这段代码很是的低效,内里许多明明的冗余指令,如第1、4、6行,2、5行等,假如打开了优化开关大概这段指令的效率会好许多。
第9行通过祖父类的虚表指针获得了函数地点,第11行同样把祖父类部门的起始地点0x0012F633做为this指针指向的地点存入ecx。
最后我们做个指针的动态转型再挪用一次:
C141 * pt1 = dynamic_cast<C141*>(pt);
pt1->foo();
第1行代码对应的汇编指令如下:
01 00423FBD cmp dword ptr [ebp+FFFFF73Ch],0
02 00423FC4 je 00423FD7
03 00423FC6 mov eax,dword ptr [ebp+FFFFF73Ch]04 00423FCC add eax,5
05 00423FCF mov dword ptr [ebp+FFFFF014h],eax
06 00423FD5 jmp 00423FE1
07 00423FD7 mov dword ptr [ebp+FFFFF014h],0
08 00423FE1 mov ecx,dword ptr [ebp+FFFFF014h]09 00423FE7 mov dword ptr [ebp+FFFFF730h],ecx
这里实际做了一个pt是否为零的判定,第4条指令把pt指向的地点后移了5字节,最后赋给了pt1。这样pt1就指向了右父类部门的地点位置,也就是C141的起始位置。
第2行代码对应的汇编指令为:
01 00423FED mov eax,dword ptr [ebp+FFFFF730h]02 00423FF3 mov ecx,dword ptr [eax]03 00423FF5 mov edx,dword ptr [ecx+4]04 00423FF8 mov eax,dword ptr [ebp+FFFFF730h]05 00423FFE mov ecx,dword ptr [eax]06 00424000 mov eax,dword ptr [ebp+FFFFF730h]07 00424006 add eax,dword ptr [ecx+4]08 00424009 mov ecx,dword ptr [ebp+FFFFF730h]09 0042400F mov edx,dword ptr [ecx+edx]10 00424012 mov esi,esp
11 00424014 mov ecx,eax
12 00424016 call dword ptr [edx]13 00424018 cmp esi,esp
14 0042401A call 0041DDF2
由于是通过偏移值指针举办运算,最后在挪用时ecx和edx的值和前面通过pt指针挪用时是一样的,这也是正确的多态行为。