副标题#e#
下面我们来看虚担任。首先看看这C020类,它从C010虚担任:}
struct C010
{
C010() : c_(0x01) {}
void foo() { c_ = 0x02; }
char c_;
};
struct C020 : public virtual C010
{
C020() : c_(0x02) {}
char c_;
};
运行如下代码,查察工具的内存机关:
PRINT_SIZE_DETAIL(C020)
功效为:
The size of C020 is 6
The detail of C020 is c0 c2 45 00 02 01
很明明工具的起始处是一个指针,然后是子类的成员变量,接下来是父类的成员变量。和以前的接头差异的是由于利用了虚担任,父类的成员变量被放到了最后头。
运行如下的代码:
C020 c020;
c020.C010::c_ = 0x04;
由于子类中的变量和父类中的变量重名,所以我们必需用这种方法来会见属于父类的成员变量,普通环境下不需要这种写法。我们看看后头这行代码对应的汇编代码:
0042387E mov eax,dword ptr [ebp+FFFFF82Ch]00423884 mov ecx,dword ptr [eax+4]00423887 mov byte ptr [ebp+ecx+FFFFF82Ch],4
前面说过工具的起始是一个指针,第1行指令取到这个指针的值,第2行把这个指针指向的地点后移4字节后的值(做为一个4字节的值)取出来。执行完这句我们看看ecx寄存器,可知取出来的值为5。最后一行是真正的赋值指令,它通过在工具的起始处(即[ebp+FFFFF32Ch])加上ecx中的值做偏移值(即5)来获得赋值的目标地点。接合前面的工具机关输出,我们可以发明从工具起始地点开始加5字节的偏移值,恰好获得父类的成员变量的地点。这样我们可以大抵阐明出直接虚担任的子类的工具机关。
#p#副标题#e#
|子类5 |父类1 |
|偏移值指针4,5|子类成员变量1|父类成员变量1|
(注:第一个数字为地址区域的长度(字节数),偏移值指针后的第二个数字为该指针指向的偏移值。后同。)
通过查察内存可以发明偏移值指针指向的内存前4字节为0,我不知道它的详细的用途是什么。接下来的4字节是一个32位的整数,也就是真正的偏移值。即从子类的起始位置到被虚担任的父类的起始位置的偏移值,在我们前面的例子中这个值为5(一个指针加一个char成员变量)。
通过这个阐明我们可以看到在虚承继的环境下,通过子类的工具会见父类的普通成员变量的效率是相当低的。假如必需用到虚担任,也应该只管不要在父类中安排普通成员变量(静态成员变量不受影响)。
别的为什么微软不把偏移值直接放到子类中,而是回收偏移值指针。我想是因为回收指针的方法更为机动,纵然今后需要扩展也不影响类工具的机关。
按下来我们再看看这几行代码:
PRINT_OBJ_ADR(c020);
C010 * pt = &c020;
PRINT_PT(pt);
pt->c_ = 0x03;
第2行声明白一个父类指针,并让它指向一个子类的工具。第3行打印出这个指针的值。运行功效为:
c020's address is : 0012F708
pt's value is : 0012F70D
我们可以看到赋值后的指针的值并不便是赋给它的工具地点值。也就是说在这个赋值进程中编译器举办了特另外事情,即调解了指针的值。我们看看第2行对应的汇编代码,看看编译器毕竟做了些什么?
01 004238EA lea eax,[ebp+FFFFF82Ch]02 004238F0 test eax,eax
03 004238F2 jne 00423900
04 004238F4 mov dword ptr [ebp+FFFFF014h],0
05 004238FE jmp 00423916
06 00423900 mov ecx,dword ptr [ebp+FFFFF82Ch]07 00423906 mov edx,dword ptr [ecx+4]08 00423909 lea eax,[ebp+edx+FFFFF82Ch]09 00423910 mov dword ptr [ebp+FFFFF014h],eax
10 00423916 mov ecx,dword ptr [ebp+FFFFF014h]11 0042391C mov dword ptr [ebp+FFFFF820h],ecx
喔!比想象的要巨大的多。一行简朴的指针赋值语句却发生了这么多的汇编代码。这行代码自己的语义是取工具的地点赋给一个指针,对付编译器来说它把这做为指针到指针的赋值来处理惩罚。由于牵涉到了向上的范例转换,同时又有虚担任存在。按照前面的机关阐明,在虚担任的环境下,父类位于工具机关的后部。因此在这里要做一个指针位置的调解。由于调解要按照源指针来举办计较,所以先要对源指针的正当性举办查抄,以制止运行时的指针异常错误。前3行的汇编指令就是在做这件事,查抄源指针是否为NULL。假如为NULL则执行4、5、10、11行,最终给pt赋0。假如不为NULL跳至第6行执行到最后。重要的是第6、7、8行代码,它们通过偏移值指针找到偏移值,并以此来调解指针的位置,让目标指针最终指向工具中的父类部门的数据成员。
比拟一下普通的指针赋值,我们可以对上面赋值的巨大性和低效有更深的认识。
C010 * pt1 = NULL;
C010 * pt2 = pt1;
这两行相应的汇编代码为:
0042397D mov dword ptr [ebp+FFFFF814h],0
00423987 mov eax,dword ptr [ebp+FFFFF814h]0042398D mov dword ptr [ebp+FFFFF808h],eax
#p#分页标题#e#
第1行是普通的赋值,编译器并不做任何的查抄,纵然源指针为NULL。因为它不需要按照源指针(本处为NULL)做任何计较。第2个赋值也很直接,只是通过eax做了一其中转。这里我们就可以看到前面的虚担任下的子类指针到父类指针的赋值是我么的低效。在措施中应只管的制止这种代码。