nimal//动物。它的函数为breathe()//呼吸。再定义一个类class F
ish//鱼 。它的函数也为breathe()再定义一个类class Sheep //羊。它的函数也为breathe()为了简化代码,将F
ish,Sheep定义成基类A
nimal的派生类。然而F
ish与Sheep的breathe不一样,一个是在水中通过水来呼吸,一个是直接呼吸空气。所以基类不能确定该如何定义breathe,所以在基类中只定义了一个virtual breathe,它是一个空的虚函数。具本的函数在子类中分别定义。程序一般运行时,找到类,如果它有基类,再找它的基类,最后运行的是基类中的函数,这时,它在基类中找到的是virtual标识的函数,它就会再回到子类中找同名函数。派生类也叫子类。基类也叫父类。这就是虚函数的产生,和类的多态性(breathe)的体现.这里的多态性是指类的多态性。函数的多态性是指一个函数被定义成多个不同参数的函数,它们一般被存在头文件中,当你调用这个函数,针对不同的参数,就会调用不同的同名函数。例:Rect()//矩形。它的参数可以是两个坐标点(point,point)也可能是四个坐标(x1,y1,x2,y2)这叫函数的多态性与函数的重载。 虚函数是如何做到因对象的不同而调用其相应的函数的呢?现在我们就来剖析虚函数。我们先定义两个类class A
{ //虚函数示例代码public:virtual void fun(){cout<<1<<endl;}virtual void fun2(){cout<<2<<endl;}};class B
:public A
{public:void fun(){cout<<3<<endl;}void fun2(){cout<<4<<endl;}};由于这两个类中有虚函数存在,所以编译器就会为他们两个分别插入一段你不知道的数据,并为他们分别创建一个表。那段数据叫做vptr指针,指向那个表。那个表叫做vtbl,每个类都有自己的vtbl,vtbl的作用就是保存自己类中虚函数的地址,我们可以把vtbl形象地看成一个数组,这个数组的每个元素存放的就是虚函数的地址,请看图通过上图,可以看到这两个vtbl分别为class A
和class B
服务。现在有了这个模型之后,我们来分析下面的代码A
*p=new A
;p->fun();毫无疑问,调用了A
::fun(),但是A
::fun()是如何被调用的呢?它像普通函数那样直接跳转到函数的代码处吗?No,其实是这样的,首先是取出vptr的值,这个值就是vtbl的地址,再根据这个值来到vtbl这里,由于调用的函数A
::fun()是第一个虚函数,所以取出vtbl第一个slot里的值,这个值就是A
::fun()的地址了,最后调用这个函数。现在我们可以看出来了,只要vptr不同,指向的vtbl就不同,而不同的vtbl里装着对应类的虚函数地址,所以这样虚函数就可以完成它的任务。而对于class A
和class B
来说,他们的vptr指针存放在何处呢?其实这个指针就放在他们各自的实例对象里。由于class A
和class B
都没有数据成员,所以他们的实例对象里就只有一个vptr指针。通过上面的分析,现在我们来实作一段代码,来描述这个带有虚函数的类的简单模型。#include<iostream>using namespace std;//将上面“虚函数示例代码”添加在这里int main(){void (*fun)(A
*);A
*p=new B
;long lVptrA
ddr;memcpy(&lVptrA
ddr,p,4);memcpy(&fun,reinterpret_cast<long*>(lVptrA
ddr),4);fun(p);delete p;system("pause");}用VC
或D
ev-C
++编译运行一下,看看结果是不是输出3,如果不是,那么太阳明天肯定是从西边出来。现在一步一步开始分析void (*fun)(A
*); 这段定义了一个函数指针名字叫做fun,而且有一个A
*类型的参数,这个函数指针待会儿用来保存从vtbl里取出的函数地址A
* p=new B
; 这个我不太了解,算了,不解释这个了long lVptrA
ddr; 这个long类型的变量待会儿用来保存vptr的值memcpy(&lVptrA
ddr,p,4); 前面说了,他们的实例对象里只有vptr指针,所以我们就放心大胆地把p所指的4bytes内存里的东西复制到lVptrA
ddr中,所以复制出来的4bytes内容就是vptr的值,即vtbl的地址现在有了vtbl的地址了,那么我们现在就取出vtbl第一个slot里的内容memcpy(&fun,reinterpret_cast<long*>(lVptrA
ddr),4); 取出vtbl第一个slot里的内容,并存放在函数指针fun里。需要注意的是lVptrA
ddr里面是vtbl的地址,但lVptrA
ddr不是指针,所以我们要把它先转变成指针类型fun(p); 这里就调用了刚才取出的函数地址里的函数,也就是调用了B
::fun()这个函数,也许你发现了为什么会有参数p,其实类成员函数调用时,会有个this指针,这个p就是那个this指针,只是在一般的调用中编译器自动帮你处理了而已,而在这里则需要自己处理。delete p;和system("pause"); 这个我不太了解,算了,不解释这个了如果调用B
::fun2()怎么办?那就取出vtbl的第二个slot里的值就行了memcpy(&fun,reinterpret_cast<long*>(lVptrA
ddr+4),4); 为什么是加4呢?因为一个指针的长度是4bytes,所以加4。或者memcpy(&fun,reinterpret_cast<long*>(lVptrA
ddr)+1,4); 这更符合数组的用法,因为lVptrA
ddr被转成了long*型别,所以+1就是往后移sizeof(long)的长度三, 以一段代码开始#include<iostream>using namespace std;class A
{ //虚函数示例代码2public:virtual void fun(){ cout<<"A
::fun"<<endl;}virtual void fun2(){cout<<"A
::fun2"<<endl;}};class B
:public A
{public:void fun(){ cout<<"B
::fun"<<endl;}void fun2(){ cout<<"B
::fun2"<<endl;}}; //end//虚函数示例代码2int main(){void (A
::*fun)(); //定义一个函数指针A
*p=new B
;fun=&A
::fun;(p->*fun)();fun = &A
::fun2;(p->*fun)();delete p;system("pause");}你能估算出输出结果吗?如果你估算出的结果是A
::fun和A
::fun2,呵呵,恭喜恭喜,你中圈套了。其实真正的结果是B
::fun和B
::fun2,如果你想不通就接着往下看。给个提示,&A
::fun和&A
::fun2是真正获得了虚函数的地址吗?首先我们回到第二部分,通过段实作代码,得到一个“通用”的获得虚函数地址的方法#include<iostream>using namespace std;//将上面“虚函数示例代码2”添加在这里void C
allVirtualF
un(void* pThis,int index=0){void (*funptr)(void*);long lVptrA
ddr;memcpy(&lVptrA
ddr,pThis,4);memcpy(&funptr,reinterpret_cast<long*>(lVptrA
ddr)+index,4);funptr(pThis); //调用}int main(){A
* p=new B
;C
allVirtualF
un(p); //调用虚函数p->fun()C
allVirtualF
un(p,1);//调用虚函数p->fun2()system("pause");}现在我们拥有一个“通用”的C
allVirtualF
un方法。这个通用方法和第三部分开始处的代码有何联系呢?联系很大。由于A
::fun()和A
::fun2()是虚函数,所以&A
::fun和&A
::fun2获得的不是函数的地址,而是一段间接获得虚函数地址的一段代码的地址,我们形象地把这段代码看作那段C
allVirtualF
un。编译器在编译时,会提供类似于C
allVirtualF
un这样的代码,当你调用虚函数时,其实就是先调用的那段类似C
allVirtualF
un的代码,通过这段代码,获得虚函数地址后,最后调用虚函数,这样就真正保证了多态性。同时大家都说虚函数的效率低,其原因就是,在调用虚函数之前,还调用了获得虚函数地址的代码。最后的说明:本文的代码可以用VC
6和D
ev-C
++4.9.8.0通过编译,且运行无。其他的编译器小弟不敢保证。其中,里面的类比方法只能看成模型,因为不同的编译器的低层实现是不同的。例如this指针,D
ev-C
++的gcc就是通过压栈,当作参数传递,而VC
的编译器则通过取出地址保存在ecx中。所以这些类比方法不能当作具体实现
C£«£«中多态(虚函数)是如何实现的?