直接调用类成员函数地址
2010-07-25 20:45:42 来源:WEB开发网现在tt1和tt2都定义了虚函数foo3,按C++语法,如果通过指针调用foo3,应该发生多态行为。下面的代码:
DWORD tt1_foo3,tt2_foo3;
GetMemberFuncAddr_VC6(tt1_foo3,&tt1::foo3);
GetMemberFuncAddr_VC6(tt2_foo3,&tt2::foo3);
tt1 x;
tt2 y;
CallMemberFunc(0,tt1_foo3,&x,0); // tt1::foo3
CallMemberFunc(0,tt2_foo3,&x,0); // tt2::foo3
CallMemberFunc(0,tt1_foo3,&y,0); // tt1::foo3
CallMemberFunc(0,tt2_foo3,&y,0); // tt2::foo3
输出如下:
hi, i am in tt1::foo3
hi, i am in tt1::foo3
hi, i am in tt2::foo3
hi, i am in tt2::foo3
请注意第二行输出,tt2_foo3取的是&tt2::foo3,但由于传递的this指针产生是&x,所以实际上调用了tt1::foo3。同样,第三行输出,取的是基类的函数地址,但由于实际对象是派生类,最后调用了派生类的函数。这说明取得的成员函数地址在虚拟函数的情况下仍然保持了正确的行为。
你若真的理解了上面所说的,一定会觉得奇怪。取函数地址的时候就得到了一个整数(成员函数地址),为何调用的时候却进了不同的函数? 只要看看汇编代码就都清楚了,"源码之前,了无秘密"。源代码: GetMemberFuncAddr_VC6(tt1_foo3,&tt1::foo3); 产生的汇编代码如下:
push offset @ILT+90(`vcall') (0040105f)
...
原来取tt1::foo3地址的时候,并不是真的就将tt1::foo3的地址传给了函数,而是传了一个vcall函数的地址。顾名思义,vcall当然是虚拟调用的意思。我们找到地址0040105f,看看这个函数到底干了些什么。
@ILT+90(??_9@$BA@AE):
0040105F jmp `vcall' (00401380)
该地址只是ILT的一个项,直接跳转到真正的vcall函数,00401380。找到00401380,就可以看到vcall的代码。
`vcall':
00401380 mov eax,dword ptr [ecx] ;//将this指针视为dword类型,并将指向的内容(对象的首个dword)放入eax.
00401382 jmp dword ptr [eax] ;//跳转到eax所指向的地址.
代码执行的时候,ecx就是this指针,具体说就是上面对象x或y的地址。而eax就是对象x或y的第一个dword的值。我们知道,对于有虚拟函数的类对象,其对象的首地址处总是一个指针,该指针指向一个虚函数的地址表。上面的对象由于只有一个虚函数,所以虚函数表也只有一项。因此,直接跳转到eax指向的地址就好。如果有多个虚函数,则eax还要加上一个偏移量,以定位到不同的虚函数。比如,如果有两个虚函数,则会有两个vcall代码,分别对应不同的虚函数,其代码大概是下面的样子:
`vcall':
00401BE0 mov eax,dword ptr [ecx]
00401BE2 jmp dword ptr [eax]
`vcall':
00401190 mov eax,dword ptr [ecx]
00401192 jmp dword ptr [eax+4]
编译器根据取的是哪个虚函数的地址,则相应的用对应的vcall地址代替。
总结一下:用前面方法取得的成员函数地址在虚拟函数的情况下仍然保持正确的行为,是因为编译器实际上传递了对应的vcall地址。而vcall代码会根据上下文this指针定位到对应的虚函数表,进而调用正确的虚函数。
最后,我们看一下多继承情况。很明显,现在情况要复杂得多。如果实际试一下,会碰到很多困难。首先,指定成员函数的时候可能会碰到冲突。其次,给定this指针的时候需要经过调整。另外,对虚拟继承可能还要特别处理。解决所有这些问题已经超出了这篇文章的范围,并且我想要的成员函数指针是一个真正的指针,而在多继承的情况下,很多时候成员函数指针已经变成了一个结构体(见参考文献),这时要正确调用该指针就变得格外困难。因此结论是,上面讨论的方法并不适用于多继承的情况,要想在多继承的情况下直接调用成员函数地址,必须手工处理各种调整,没有简单的统一方法。
本文配套源码
更多精彩
赞助商链接