C++指向类成员函数指针的多态原理解析

966 阅读2分钟

一些框架代码里面会用到类的成员函数指针去实现一些类似回调函数的功能,而且通过类的成员函数指针去调用类的成员函数的时候,是不是可以实现多态的效果呢?这个问题不好回答,还是上代码探究一下吧

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实际指向的是一个编译器生成的“桩函数”,这个“桩函数”会根据虚函数表去查找调用真正的成员函数,从而实现多态。