菱形结构的虚继承(3)
最后我们看看,如果在上篇例子的基础上,子类及左、右父类都各自定义了自己的虚函数,这时的情况又会怎样。
struct C140 : public virtual C041
{
C140() : c_(0x02) {}
virtual void foo() { c_ = 0x11; }
char c_;
};
struct C160 : public virtual C041
{
C160() : c_(0x02) {}
virtual void foo() { c_ = 0x12; }
virtual void f160() { c_ = 0x12; }
char c_;
};
struct C161 : public virtual C041
{
C161() : c_(0x03) {}
virtual void foo() { c_ = 0x13; }
virtual void f161() { c_ = 0x13; }
char c_;
};
struct C170 : public C160, public C161
{
C170() : c_(0x04) {}
virtual void foo() { c_ = 0x14; }
virtual void f170() { c_ = 0x14; }
char c_;
};
首先运行如下的代码,看看内存的布局。
PRINT_SIZE_DETAIL(C041)
PRINT_SIZE_DETAIL(C160)
PRINT_SIZE_DETAIL(C161)
PRINT_SIZE_DETAIL(C170)
结果为:
The size of C041 is 5
The detail of C041 is f0 b2 45 00 01
The size of C160 is 18
The detail of C160 is 84 b3 45 00 88 b3 45 00 02 00 00 00 00 80 b3 45 00 01
The size of C161 is 18
The detail of C161 is 98 b3 45 00 9c b3 45 00 03 00 00 00 00 94 b3 45 00 01
The size of C170 is 28
The detail of C170 is b0 b3 45 00 c8 b3 45 00 02 ac b3 45 00 bc b3 45 00 03 04 00 00 00 00 a8 b3 45 00 01
C170对象的布局为:
|C160,9 |C161,9 |C170,1 |zero,4 |C041,5 |
|vp,4 |op,4,19 |m,1 |vp,4 |op,4,10 |m,1 |m,1 | |vp,4 |m1 |
(注:为了不折行,我用了缩写。op代表偏移值指针、m代表成员变量、vp代表虚表指针。第一个数字是该区域的大小,即字节数。只有偏移值指针有第二个数字,第二个数字就是偏移值指针指向的偏移值的大小。)
左右父类由于各自定义了自己的新的虚函数,因此都拥有了自己的虚表指针。奇怪的是子类虽然也定义了自己的新的虚函数,我们在上面的布局中却看到它并没有自己的虚表指针。那么它应该是和顶层类或是某一父类共用了虚表。我们可以在后面通过对调用的跟踪来找到答案。
另一个奇怪的地方是在左右父类中的偏移值指针指向的偏移值不再是到祖父类的偏移量,而是变成了到祖父类之前的4字节0值的偏移量。同时在前面第八篇中我们说过偏移值指针指向的地址的前4个字节为零,接下来的4个字节才是真正的偏移量。在这个例子中,前4个字节不再为0,而是0xFFFFFFFC,即整数-4.
照例我们先通过对象来调用一下。
C170 obj;
PRINT_OBJ_ADR(obj);
obj.foo();
结果为:
obj's address is : 0012F54C
最后一行调用对应的汇编指令为:
004245B8 lea ecx,[ebp+FFFFF687h]
004245BE call 0041D122
ecx中的值(即this指针的值)为0x0012F563,和前面一样是指向祖父类的起始部分。同样函数中的指令也是通过将this-5字节来定位到正确的成员变量的地址,这里不再列出函数的汇编指令。
再看看调用它自己新定义的虚函数。
obj.f170();
对应的汇编指令为:
004245C3 lea ecx,[ebp+FFFFF670h]
004245C9 call 0041D127
让我非常惊奇的是这次this指针的值居然是0x0012F54C.和前面的对象地址输出是一样的,也就是指向了整个对象的起始位置。这就让人非常的奇怪了,在同一个对象上调用的两个虚函数,编译器为它们传递的this指针却是不同的。
让我们跟到函数中,看它怎样取得正确的成员变量的地址。