虚函数

今天想明白多态,结果研究了一下虚函数表,结果苦思冥想了很久,终于在各种文章与帖子中,总结出了自己的想法。

先理解早绑定(early binding)、晚绑定(late binding)。所谓early binding:On compile time,就能明确一个函数调用是对哪个对象的哪个成员函数进行的,即编译时就晓得了确定的函数地址;所谓late binding:On compile time,对函数(虚函数)的调用被搞成了:pObj->_vptr->vtable[],从而导致不到runtime,完全不知道实际函数地址。直到程序运行时,执行到这里,去vtable里拿到函数地址,才晓得。其实,原理很简单,只是单看这些名词的话会觉得好像很magic一样。

具体看看下面

普通函数的处理:一个特定的函数都会映射到特定的代码,无论时编译阶段还是连接阶段,编译器都能计算出这个函数的地址,调用即可。(早绑定)
   
虚函数的处理:被调用的函数不仅依据调用的特定函数,还依据调用的对象的种类。通常是由虚函数表(vtable)来实现的。(晚绑定)

  虚函数表的结构:它是一个函数指针表,每一个表项都指向一个函数。任何一个包含至少一个虚函数的类都会有这样一张表。需要注意的是vtable只包含虚函数的指针,没有函数体。实现上是一个函数指针的数组。虚函数表既有继承性又有多态性。每个派生类的vtable继承了它各个基类的vtable,如果基类vtable中包含某一项,则其派生类的vtable中也将包含同样的一项,但是两项的值可能不同。如果派生类重载(override)了该项对应的虚函数,则派生类vtable的该项指向重载后的虚函数,没有重载的话,则沿用基类的值。并且这里需要注意的是基类与子类的虚函数表中的函数顺序是相同的。
  
每一个类只有唯一的一个vtable,不是每个对象都有一个vtable,恰恰是每个同一个类的对象都有一个指针,这个指针指向该类的vtable(当然,前提是这个类包含虚函数)。在类对象的内存布局中,首先是该类的vtable指针,然后才是对象数据。这样通过指针或者引用就很容易找到虚函数表那么,每个对象只额外增加了一个指针的大小,一般说来是4字节。分析一下这里的思想所在,问题的实质是这样,对于发出虚函数调用的这个对象指针,在编译期间缺乏更多的信息,而在运行期间具备足够的信息,但那时已不再进行绑定了而是直接执行好了,怎么在二者之间作一个过渡呢?把绑定所需的信息用一种通用的数据结构记录下来,该数据结构可以同对象指针相联系,在编译时只需要使用这个数据结构进行抽象的绑定,在运行期间将会得到真正的绑定。这个数据结构就是vtable。    

给个虚拟表的例子

假设我们有这样的一个类:

classBase {

public:

virtualvoidf() { cout <<"Base::f" << endl; }

virtualvoidg() { cout <<"Base::g" << endl; }

virtualvoidh() { cout <<"Base::h" << endl; }

};

按照上面的说法,我们可以通过Base的实例来得到虚函数表。下面是实际例程:

typedefvoid(*Fun)(void);

Base b;

Fun pFun =NULL;

cout << "虚函数表地址:" << (int*)(&b)<< endl;

cout << "虚函数表 —第一个函数地址:"<< (int*)*(int*)(&b)<< endl;

// Invoke thefirst virtual function

pFun =(Fun)*((int*)*(int*)(&b));

pFun();

实际运行经果如下:(WindowsXP+VS2003, Linux 2.6.22 + GCC 4.1.3)

虚函数表地址:0012FED4

虚函数表 — 第一个函数地址:0044F148

Base::f

通过这个示例,我们可以看到,我们可以通过强行把&b转成int *,取得虚函数表的地址,然后,再次取址就可以得到第一个虚函数的地址了,也就是Base::f(),这在上面的程序中得到了验证(把int*强制转成了函数指针)。

证明了虚拟表,再让我们理解一下具体的编译

我们来看一下虚函数调用的汇编代码,以加深理解。

void test(base*pBase)
{
pBase->vfun2();
}

int main(int argc,char* argv[])
{
derived td;


test(&td);

return 0;
}

derived td;编译生成的汇编代码如下:
mov DWORD PTR _td$[esp+24], OFFSET FLAT:??_7derived@@6B@ ; derived::`vftable'
由编译器的注释可知,此时PTR _td$[esp+24]中存储的就是derived类的VTABLE地址。

test(&td);编译生成的汇编代码如下:
lea eax, DWORD PTR _td$[esp+24] 
mov DWORD PTR __$EHRec$[esp+32], 0
push eax
call ?test@@YAXPAVbase@@@Z ; test 
调用test函数时完成了如下工作:取对象td的地址,将其压栈,然后调用test。

pBase->vfun2();编译生成的汇编代码如下:
mov ecx, DWORD PTR _pBase$[esp-4]
mov eax, DWORD PTR [ecx]
jmp DWORD PTR [eax+4]
首先从栈中取出pBase指针指向的对象地址赋给ecx,然后取对象开头的指针变量中的地址赋给eax,此时eax的值即为VPTR的值,也就是VTABLE的地址。(这里也就是多态实现的关键,编译器自动的识别到了多态,并且通过对象地址切换到了虚拟表地址,至于虚函数表中函数的偏移量我个人认为是基于基函数的虚函数表,因为毕竟顺序相同吗)最后就是调用虚函数了,由于vfun2位于VTABLE的第二个位置,相当于 VPTR+1,每个函数指针是4个字节长,所以最后的调用被编译器翻译为 jmp DWORD PTR [eax+4]。如果是调用pBase->vfun1(),这句就该被编译为 jmp DWORD PTR [eax]。

总结:

虚函数赖以生存的底层机制:vptr + vtable。虚函数的运行时实现采用了VPTR/VTBL的形式,这项技术的基础:
①编译器在后台为每个包含虚函数的类产生一个静态函数指针数组(虚函数表),在这个类或者它的基类中定义的每一个虚函数都有一个相应的函数指针。
②每个包含虚函数的类的每一个实例包含一个不可见的数据成员vptr(虚函数指针),这个指针被构造函数自动初始化,指向类的vtbl(虚函数表)
③当客户调用虚函数的时候,编译器产生代码反指向到vptr,索引到vtbl中,然后在指定的位置上找到函数指针,并发出调用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值