关于基类构造函数调用虚函数实际调用的不是派生类的问题的原因
关于基类构造函数调用虚函数实际调用的不是派生类的问题的原因
我们知道,类的构造函数里面编译器插入了很多代码,比如异常安全,虚函数表指针的设置,基类构造,等等。
而且,关键是这些代码时在任何用户的代码(非初始化)的地方之前插入的,问题就来了···
如果在基类构造函数里面调用基类的虚函数,那么,实际调用的却不像我们当初认为的多态效果,为什么呢?
下面看看编译怎么实现的就知道了····
下面是测试类:
class A {
public:
A() {
vfunc() ;
}
virtual vfunc() {
;
}
} ;
class CC : public A {
public:
virtual vfunc() {
;
}
};
int main()
{
CC c ;
return 0 ;
}
原因:
004012F8 lea ecx,[ebp-4] //装载this指针到ECX,这是thiscall的调研约定
004012FB call @ILT+5(CC::CC) (0040100a) //调用CC的构造函数
30: return 0 ;
00401300 xor eax,eax
31: }
///////////////////////////////////////////////////////////////////////
CC::CC:
····
00401339 pop ecx //this指针到ECX
0040133A mov dword ptr [ebp-4],ecx
0040133D mov ecx,dword ptr [ebp-4]
00401340 call @ILT+15(A::A) (00401014) //派生类构造函数进入后,完全没干什么事就“马上”调用了基类的构造函数,那么,其vptr虚函数表指针也没有赋值!
00401345 mov eax,dword ptr [ebp-4]
00401348 mov dword ptr [eax],offset CC::`vftable' (00431068)//直到这才对自己的虚函数表指针赋值,因此,此前的调用,绝不会涉及到派生类CC的虚函数··
0040134E mov eax,dword ptr [ebp-4] //此后如果有任何虚函数调用,访问虚函数表时访问的就正常的是派生类的虚函数表了,调用的也正确了
00401351 pop edi
00401352 pop esi
00401353 pop ebx
00401354 add esp,44h
00401357 cmp ebp,esp
00401359 call __chkesp (00401590)
0040135E mov esp,ebp
00401360 pop ebp
00401361 ret
///////////////////////////////////////////////////////////////////////
10: A() {
····
00401399 pop ecx
0040139A mov dword ptr [ebp-4],ecx
0040139D mov eax,dword ptr [ebp-4]
004013A0 mov dword ptr [eax],offset A::`vftable' (0043106c) //注意到,基类第一件事情是:为虚函数表指针地址赋值,
//此指针传下来其实是CC派生类的this指针的地址,也就是:类的第一个字节
11: vfunc() ;//此时调用的时候,理所当然会传入this指针,此this的vptr此时还不是派生类的,而是上面一句设置的基类的虚函数表地址!
004013A6 mov ecx,dword ptr [ebp-4] //传递这个很多人都会“错误的以为的”this
004013A9 call @ILT+10(A::vfunc) (0040100f) //调用这个“虚函数”,这里没有访问虚函数表是因为编译器优化的原因吧,因为此时肯定为A的vfunc,
//感兴趣可以试一下:看下面的例子
12: }
·····
004013C1 ret
上面没有访问虚函数表,是因为编译器优化了一下,如果我们的A类构造函数是这样的,那么···
class A {
public:
A() {
init() ;//先调用A的非虚函数,里面再调用vfunc虚函数就可以看出要访问虚函数表了,不过此时的虚函数表指针指向A的虚函数表
}
virtual vir() { ;} ;//故意加了一项,待会会看到+4的字样
virtual vfunc() {//应该在虚函数表的第二项
;
}
init() {
vfunc() ;
}
} ;
10: A() {
····
004013A9 pop ecx
004013AA mov dword ptr [ebp-4],ecx
004013AD mov eax,dword ptr [ebp-4]
004013B0 mov dword ptr [eax],offset A::`vftable' (00431074)//设置此时的虚函数表指针为基类的!!不是派生类的,调用完成后会被派生类覆盖
11: init() ;
004013B6 mov ecx,dword ptr [ebp-4]
004013B9 call @ILT+40(A::init) (0040102d) //不是虚函数,直接调用,不用访问虚函数表
12: }
····
004013D1 ret
///////////////////////////////////////////////////////////////////////
18: init() {
····
00401469 pop ecx
0040146A mov dword ptr [ebp-4],ecx //this指针放到栈顶
19: vfunc() ;
0040146D mov eax,dword ptr [ebp-4] //下面准备虚函数表的查询,可以看出,虚函数调用机制还是会多用几次寻址操作的,访问内存。
00401470 mov edx,dword ptr [eax] //this-》edx。其实其内容是A的虚函数表的指针
00401472 mov esi,esp
00401474 mov ecx,dword ptr [ebp-4]
00401477 call dword ptr [edx+4] //vfunc在第二项,所以+4,这里
0040147A cmp esi,esp
0040147C call __chkesp (00401610)
20: }
····
00401491 ret
近期评论