C 对象布局及多态实现探索之虚继承

2008-02-23 05:27:12来源:互联网 阅读 ()

新老客户大回馈,云服务器低至5折

下面我们来看虚继承。首先看看这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字节的偏移值,刚好得到父类的成员变量的地址。这样我们能够大致分析出直接虚继承的子类的对象布局。

|子类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;

  这两行相应的汇编代码为:

标签:

版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有

上一篇: C 对象布局及多态之虚成员函数调用

下一篇: C 箴言:理解typename的两个含义

0042397D mov dword ptr [ebp FFFFF814h],0
00423987 mov eax,dword ptr [ebp FFFFF814h]
0042398D mov dword ptr [ebp FFFFF808h],eax