虚函数实现机制

来源:互联网 发布:linux c实现web服务器 编辑:程序博客网 时间:2024/06/10 23:31

虚函数实现机制

(2007-04-24 09:07:26)
转载
标签:

虚函数

实现机制

内存分析

反汇编

 
 

 注:本文系原创,如需转载,请注明出处和作者.
 
C++实现多态靠两种方案,静态的重载和动态的虚函数机制.下面通过VC6环境分析下虚函数的实现机制.
 (其实在VC6中,重载的实现方式是名字粉碎,即给函数通过某种方式起个别名,实现"多态",个人认为并不是真正的多态)
代码经VC6+WINXP SP2编译通过.
#include <iostream.h>
class CFather 
{
public:
 virtual void virFunc();{cout<<"CFather::virFunc()"<<endl;}
 CFather();
 virtual ~CFather(); //虚函数定义
};
class CSon1 : public CFather 
{
public:
 void virFunc();{cout<<"CSon1::virFunc()"<<endl;} //重写
 CSon1();
 virtual ~CSon1();
};
class CSon2 : public CFather 
{
public:
 void virFunc();{cout<< "CSon2::virFunc()" <<endl;} //重写
 CSon2();
 virtual ~CSon2();

};
在父类中定义了虚函数virFunc,两个子类分别重写,默认也为virtual类型.main 函数中调用如下:
int main(int argc, char* argv[])
{
 CFather father;
 CSon1 son1;
 CSon2 son2;

 CFather* pObj = &father; //父类指针

 pObj->virFunc();

 pObj = &son1; //子类指针

 pObj->virFunc();

 pObj = &son2; //子类指针

 pObj->virFunc();

 return 0;
}

根据虚函数机制,结果应该为:
CFather::virFunc()
CSon1::virFunc()
CSon2::virFunc()

这里解释下,在C++中,基类指针可以指向其派生类,是实现动态多态的必要前提.

那么,在VC6中是如何实现虚函数呢?答案是虚函数表。看下编译时的内存情况。
当执行至CFather father 后,观察。(不同机器地址可能有所不同)
&father 的地址为:0x0012ff70,打开memory,该地址内容为:
0012FF70  1C 80 42 00 B0 FF 12 00 3B 6A 41 00 00 00 00 00 
首地址的4字节内容为:0042801c,这就是虚表的入口,查看memory,该地址内容为:
0042801C  46 10 40 00 64 10 40 00 00 00 00 00 43 46 61 74 
由于虚表中存放的都是函数指针,所以,每4个字节为一个地址,可以从这个表中找到两个地址:0x00401046,0x00401064。分别对应virFunc和析构函数。

同理,看一下son2的情况,换个观察方式。
当执行至 CSon2时,变量监视窗口可以看到son2的__vfptr的值0x00428086,其下分别为__vfptr[0],__vfptr[1],值分别为0x00401014,0x00401023。有点眼熟,对,这就是son2类的虚表,只是在从CFather继承来的时候,重写的函数地址被覆盖了。

以上为编译时候内存的情况,通过分析我们可以得知,在实例化类的同时,实例的首地址存储其虚表的地址。当子类重写时,将其重写的地址覆盖从父类继承来的虚表。当不同指针调用时,根据其虚表选取不同的函数,实现多态性.也就是说,这样的调用是在运行时才绑定的,所以称为动态的多态性.

然后我们看下反汇编的情况。

编译时单步进入00401690   call    @ILT+55(CFather::CFather) (0040103c)

004010E9   pop         ecx
004010EA   mov         dword ptr [ebp-4],ecx
004010ED   mov         eax,dword ptr [ebp-4]
004010F0   mov         dword ptr [eax],offset CFather::`vftable' (0042801c)

我们来分析这四行代码。
首先,pop ecx,
      mov dword ptr [ebp-4],ecx
将ecx出栈,并将其值按两个字(4个字节)传给ebp-4中的地址。通过观察registers(寄存器窗口),ecx=0012ff70。这里
的ecx其实就是实例对象的this指针。
然后,mov eax,dword ptr [ebp-4]
将this指针传值给寄存器eax。因为汇编中不允许两个内存地址的操作,所以必须

采用eax做中转.
最后,mov dword ptr [eax],offset CFather::`vftable' (0042801c)
使用offset,将虚表的地址按两个字传给eax中的地址,即this指针指向了虚表地址。这就可以解释为什
么对象的首地址都是虚表的地址了。

子类实现方式同上,只是在call本身的构造之前,首先要进行父类的构造。

另外,在编译的时候,根据函数定义的先后顺序,在虚拟表中安排函数的位置,其实就是定义了一个函数指针的数组。当采用相应的对象调用函数时,在数组中相应的偏移寻找函数。

还要注意,一般在类定义的时候,构造函数不可以采用虚函数,而析构函数则最好采用虚拟函数

有关虚拟函数的定义,概念等以及汇编知识等请参考相应资料。本文不再讨论。

 

原创粉丝点击