一些框架代码里面会用到类的成员函数指针去实现一些类似回调函数的功能,而且通过类的成员函数指针去调用类的成员函数的时候,是不是可以实现多态的效果呢?这个问题不好回答,还是上代码探究一下吧
class Base
{
public:
Base(void);
~Base(void);
void func();
virtual void vfunc();
void printvfuncaddress();
};
Base::Base(void)
{
}
Base::~Base(void)
{
}
void Base::func()
{
cout << "Base::func()"<<endl;
}
void Base::vfunc()
{
cout << "Base::vfunc()"<<endl;
}
void Base::printvfuncaddress()
{
void** p = (void**)this;
void* vtable = *p;
cout<<"vtable address: "<< vtable<<endl;
cout<<"vtable[0]: "<<((void**)vtable)[0]<<endl;
}
首先定义一个基类Base,定义了一个普通函数func和一个虚函数vfunc,具体实现见上面的代码,还有一个函数printvfuncaddress,这个需要解释一下,这个是输出类的虚函数表的地址,以及第一个虚函数的地址,因为类中只有一个虚函数,所以其实就是vfunc函数的地址。关于C++虚函数表的原理这里就不解释了,大家可以查阅相关资料。
在定义一个从Base的派生类Derived,如下所示:
class Derived :
public Base
{
public:
Derived(void);
~Derived(void);
void func();
virtual void vfunc();
};
Derived::Derived(void)
{
}
Derived::~Derived(void)
{
}
void Derived::func()
{
cout << "Derived::func()"<<endl;
}
void Derived::vfunc()
{
cout << "Derived::vfunc()"<<endl;
}
可以预见的是,通过Derived类调用printvfuncaddress函数,输出的虚函数地址和Base类不一样,因为虚函数重写了。
在主函数里面,通过下面的代码测试类的成员函数指针,如下所示:
typedef void (Base::*Pfuntion)();
int _tmain(int argc, _TCHAR* argv[])
{
Base base;
Derived derived;
//普通函数指针
Pfuntion pfunc = &Base::func;
(base.*pfunc)(); //输出"Base::func()"
(derived.*pfunc)();//输出"Base::func()"
//虚函数指针
Pfuntion pvfunc = &Base::vfunc;
(base.*pvfunc)(); //输出"Base::vfunc()"
base.printvfuncaddress();
(derived.*pvfunc)();//输出"Derived::vfunc()"
derived.printvfuncaddress();
return 0;
}
注意这一句,typedef void (Base::*Pfuntion)(),这是定义了一个指针类型Pfuntion,指向Base类的成员函数,成员函数是无参数和返回值的,这跟我们代码中定义的func和vfunc的类型是一致的。上面的代码,第一次是定义了一个指针pfunc,指向普通函数Base::func,然年后通过base和derived两个对象对调用指针,都会输出Base::func(),这个结果还是比较自然的,因为Base::func的地址是固定的,通过哪个对象对调用,结果都应该一样。
第二次我们定义了指针pvfunc,然后赋值&Base::vfunc,结果通过base和derived两个对象对调用指针,分别输出Base::vfunc()和Derived::vfunc(),这里结论是很明确了,通过指向成员函数的指针对调用成员函数时,是能够实现多态的效果的,似乎没有什么特别的地方,虚函数不就是可以实现多态吗?有道理,不过我们看这一句 Pfuntion pvfunc = &Base::vfunc,明明就是把基类虚函数的地址赋值给了pvfunc,为啥在调用的时候会调用到派生类的函数呢?因为base.*pvfunc)()和(derived.*pvfunc)()这样的调用并不是虚函数的调用,因为编译器是不知道pvfunc指向的是虚函数还是普通函数的,这是一个运行时才能确定的事情,因此不可能在此时施加什么编译器魔法,通过反汇编可以验证这一点,就是一个普通的函数调用。
(derived.*pvfunc)();
012E4A2B mov esi,esp
012E4A2D lea ecx,[ebp-20h]
012E4A30 call dword ptr [ebp-38h]
那聪明的你告诉我,为什么会出现这样的结果呢?我猜想问题一定是出现在Pfuntion pvfunc = &Base::vfunc这一句,直接反汇编看个究竟,在这一句加上断点,进行调试。
Pfuntion pvfunc = &Base::vfunc;
00294A0D mov dword ptr [ebp-38h],offset Base::`vcall'{0}' (2912A8h)
[ebp-38h]就是局部变量pvfunc,反汇编很有意思,似乎是把Base的虚函数表中的第一个项赋值给pvfunc,那不就是Base的vfunc函数地址吗?简直不要太合理,这一句执行完成后pvfunc的值为0x2912A8h,接下来看这一句(base.*pvfunc)():
(base.*pvfunc)(); //输出"Base::vfunc()"
00294A14 mov esi,esp
00294A16 lea ecx,[ebp-14h]
00294A19 call dword ptr [ebp-38h]
00294A1C cmp esi,esp
00294A1E call @ILT+455(__RTC_CheckEsp) (2911CCh)
继续跟进 call dword ptr [ebp-38h],应该会转到0x2912A8h
Base::`vcall'{0}':
002912A8 jmp Base::`vcall'{0}' (291590h)
这里只有一个jmp语句,跳转到0x291590h,继续跟进
Base::`vcall'{0}':
00291590 mov eax,dword ptr [ecx]
00291592 jmp dword ptr [eax]
天哪噜,又有一个jmp,这是要去哪呢,其实这个就是关键了,ecx是什么呢?就是this指针,this指针跟虚函数表的指针的值是相同的,上面的jmp会执行到虚函数表的第一个函数,不信你看,会跳转到0x0029100A
Base::vfunc:
0029100A jmp Base::vfunc (291660h)
这里又有一个imp,跟进去,调用到0x00291660,就是Base::vfunc的具体实现了。
void Base::vfunc()
{
00291660 push ebp
00291661 mov ebp,esp
00291663 sub esp,0CCh
00291669 push ebx
0029166A push esi
0029166B push edi
0029166C push ecx
0029166D lea edi,[ebp-0CCh]
00291673 mov ecx,33h
00291678 mov eax,0CCCCCCCCh
0029167D rep stos dword ptr es:[edi]
0029167F pop ecx
00291680 mov dword ptr [ebp-8],ecx
cout << "Base::vfunc()"<<endl;
00291683 mov esi,esp
... ...
}
通过以上分析,pvfunc的值为0x2912A8h,通过层层调用0x2912A8h->0x291590h->0x0029100A(虚函数地址)->0x00291660h,最终调用到Base::vfunc()。 下面接着分析一下这一句(derived.*pvfunc)(),如下:
(derived.*pvfunc)();//输出"Derived::vfunc()"
00294A2B mov esi,esp
00294A2D lea ecx,[ebp-20h]
00294A30 call dword ptr [ebp-38h]
同样是会到0x2912A8h,
Base::`vcall'{0}':
002912A8 jmp Base::`vcall'{0}' (291590h)
然后jmp到0x291590h
Base::`vcall'{0}':
00291590 mov eax,dword ptr [ecx]
00291592 jmp dword ptr [eax]
然后通过虚函数表调用到0x002912B7,这里的虚函数表是Derived类的,如下所示:
Derived::vfunc:
002912B7 jmp Derived::vfunc (292080h)
继续调用到0x292080h,就是Derived::vfunc的具体实现了
void Derived::vfunc()
{
00292080 push ebp
00292081 mov ebp,esp
00292083 sub esp,0CCh
00292089 push ebx
0029208A push esi
0029208B push edi
0029208C push ecx
0029208D lea edi,[ebp-0CCh]
00292093 mov ecx,33h
00292098 mov eax,0CCCCCCCCh
0029209D rep stos dword ptr es:[edi]
0029209F pop ecx
002920A0 mov dword ptr [ebp-8],ecx
cout << "Derived::vfunc()"<<endl;
002920A3 mov esi,esp
... ...
}
pvfunc的值为0x2912A8h,通过层层调用0x2912A8h->0x291590h->0x002912B7(虚函数地址)->0x292080,最终调用到Derived::vfunc()。
通过上述的分析,就比较清楚了,Pfuntion pvfunc = &Base::vfunc并不是把虚函数的地址赋值给了pvfunc,真正的虚函数地址用printvfuncaddress打印出来,可以看到是0x0029100A和0x002912B7,和我们上面分析的一致。
Base::func()
Base::func()
Base::vfunc()
vtable address: 00297834
vtable[0]: 0029100A
Derived::vfunc()
vtable address: 00297860
vtable[0]: 002912B7
也就是说,pvfunc实际指向的是一个编译器生成的“桩函数”,这个“桩函数”会根据虚函数表去查找调用真正的成员函数,从而实现多态。