C 对象布局及多态实现探索之虚继承
2008-02-23 05:27:12来源:互联网 阅读 ()
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字节的偏移值,刚好得到父类的成员变量的地址。这样我们能够大致分析出直接虚继承的子类的对象布局。
|子类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 |