C++中的动态绑定

来源:互联网 发布:抗日知乎 编辑:程序博客网 时间:2024/06/05 12:39

动态绑定(dynamic binding)动态绑定是指在执行期间(非编译期)判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。


 C++中,通过基类的引用或指针调用虚函数时,发生动态绑定。引用(或指针)既可以指向基类对象也可以指向派生类对象,这一事实是动态绑定的关键。用引用(或指针)调用的虚函数在运行时确定,被调用的函数是引用(或指针)所指对象的实际类型所定义的。


联编:联编是指一个计算机程序自身彼此关联的过程,在这个联编过程中,需要确定程序中的操作调用(函数调用)与执行该操作(函数)的代码段之间的映射关系;按照联编所进行的阶段不同,可分为静态联编和动态联编;


静态联编:是指联编工作是在程序编译连接阶段进行的,这种联编又称为早期联编;因为这种联编是在程序开始运行之前完成的;在程序编译阶段进行的这种联编又称静态束定;在编译时就解决了程序中的操作调用与执行该操作代码间的关系,确定这种关系又被称为束定;编译时束定又称为静态束定;


动态联编:编译程序在编译阶段并不能确切地知道将要调用的函数,只有在程序执行时才能确定将要调用的函数,为此要确切地知道将要调用的函数,要求联编工作在程序运行时进行,这种在程序运行时进行的联编工作被称为动态联编,或动态束定,又叫晚期联编;C++规定:动态联编是在虚函数的支持下实现的;


静态联编和动态联编都是属于多态性的,它们是在不同的阶段进对不同的实现进行不同的选择。


虚函数表

C++中动态绑定是通过虚函数实现的。而虚函数是通过一张虚函数表(virtual table)实现的。这个表中记录了虚函数的地址,解决继承、覆盖的问题,保证动态绑定时能够根据对象的实际类型调用正确的函数。


先说这个虚函数表。据说在C++的标准规格说明书中说到,编译器必需要保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证正确取到虚函数的偏移量)。这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。


假设有如下基类:


[cpp] view plaincopyprint?
  1. class Base  
  2. {  
  3. public:  
  4.     virtual void f(){cout<<"Base::f"<<endl;}  
  5.     virtual void g(){cout<<"Base::g"<<endl;}  
  6.     virtual void h(){cout<<"Base::h"<<endl;}  
  7. };  


按照上面的说法,写代码验证:

[cpp] view plaincopyprint?
  1. void t1()  
  2. {  
  3.     typedef void (*pFun)(void);  
  4.     Base b;  
  5.     pFun pf=0;  
  6.     int * p = (int*)(&b);///强制转换为int*,这样就取得  
  7.     ///b的vptr的地址  
  8.     cout<<"VTable addr: "<<p<<endl;  
  9.     int *q;  
  10.     q=(int*)*p;///*p取得vtable,强转为int*,  
  11.     ///取得指向第一个函数地址的指针q  
  12.     cout<<"virtual table addr: "<<q<<endl;  
  13.     pf = (pFun)*q;///对指针解引用,取得函数地址  
  14.     cout<<"first virtual fun addr: "<<(int*)(*q)<<endl;  
  15.     pf();///invoke  
  16. }  


运行结果:



运行环境为:

/************************

 * g++ (GCC) 4.6.2

 * Win7 x64

 * Code::Blocks 12.11

 ************************/

其他环境没有测试,下同。

按照同样的方法,继续调用g()h(),只要移动指针即可:


[cpp] view plaincopyprint?
  1. void t2()  
  2. {  
  3.     typedef void (*pFun)(void);  
  4.     Base b;  
  5.     pFun pf=0;  
  6.     int * p = (int*)(&b);///强制转换为int*,这样就取得  
  7.     ///b的vptr的地址  
  8.     cout<<"VTable addr: "<<p<<endl;  
  9.     int *q;  
  10.     q=(int*)*p;///*p取得vtable,强转为int*,  
  11.     ///取得指向第一个函数地址的指针q  
  12.     cout<<"virtual table addr: "<<q<<endl;  
  13.     pf = (pFun)*q;///对指针解引用,取得函数地址  
  14.     cout<<"first virtual fun addr: "<<(int*)(*q)<<endl;  
  15.     pf();///invoke  
  16.     ++q;///q指向第二个虚函数  
  17.     pf = (pFun)*q;///对指针解引用,取得函数地址  
  18.     cout<<"second virtual fun addr: "<<(int*)(*q)<<endl;  
  19.     pf();///invoke  
  20.     ++q;///q指向第3个虚函数  
  21.     pf = (pFun)*q;///对指针解引用,取得函数地址  
  22.     cout<<"third virtual fun addr: "<<(int*)(*q)<<endl;  
  23.     pf();///invoke  
  24.     /** ============================  **/  
  25.     ++q;///q指向第4个虚函数?  
  26.     cout<<"4th virtual fun addr: "<<(int*)(*q)<<endl;  
  27. }  


分割线以后的那部分说明:显然只有3个虚函数,再往后移动就没有了,那没有是什么?取出来看发现是0,如果把这个地址继续当做函数地址调用,那必然出错了。看来这个表的最后一个地址为0表示虚函数表的结束。结果如下:



如果只看代码,可能会晕。。。。

看个形象点的图吧:


其中标*的地方这里是0



有覆盖时的虚函数表

没有覆盖的虚函数没有太大意义,要实现动态绑定,必须在派生类中覆盖基类的虚函数。


2.1 继承时没有覆盖

假设有如下继承关系:


在这里,派生类没有覆盖任何基类函数。那么在派生类的实例中,其虚函数表如下所示:


测试代码也很简单,只要修改对象的地址为新的地址,然后继续往后移动指针就行了:

[cpp] view plaincopyprint?
  1. void t3()  
  2. {  
  3.     typedef void (*pFun)(void);  
  4.     Derive d;  
  5.     pFun pf=0;  
  6.     int * p = (int*)(&d);///强制转换为int*,这样就取得  
  7.     ///d的vptr的地址  
  8.     cout<<"VTable addr: "<<p<<endl;  
  9.     int *q;  
  10.     q=(int*)*p;///*p取得vtable,强转为int*,  
  11.     ///取得指向第一个函数地址的指针q  
  12.     cout<<"virtual table addr: "<<q<<endl;  
  13.     for(int i=0; i<6; ++i)  
  14.     {  
  15.         pf = (pFun)*q;///对指针解引用,取得函数地址  
  16.         cout<<i+1<<"th virtual fun addr: "<<(int*)(*q)<<endl;  
  17.         pf();///invoke  
  18.         ++q;  
  19.     }  
  20.     cout<<"*q="<<(*q)<<endl;  
  21. }  


结果:

结论:

1) 虚函数按照其声明顺序放在VTable中;

2) 基类的虚函数在派生类虚函数前面;


2.2 继承时有虚函数覆盖

假设继承关系如下:


注意:这时函数f()在派生类中重写了。

先测试下,看看虚函数表是什么样子的:

测试代码:

[cpp] view plaincopyprint?
  1. void t4()  
  2. {  
  3.     typedef void (*pFun)(void);  
  4.     Derive d;  
  5.     pFun pf=0;  
  6.     int * p = (int*)(&d);///强制转换为int*,这样就取得  
  7.     ///b的vptr的地址  
  8.     cout<<"VTable addr: "<<p<<endl;  
  9.     int *q;  
  10.     q=(int*)*p;///*p取得vtable,强转为int*,  
  11.     ///取得指向第一个函数地址的指针q  
  12.     cout<<"virtual table addr: "<<q<<endl;  
  13.     for(int i=0; i<6; ++i)  
  14.     {  
  15.         pf = (pFun)*q;///对指针解引用,取得函数地址  
  16.         if(pf==0) break;  
  17.         cout<<i+1<<"th virtual fun addr: "<<(int*)(*q)<<endl;  
  18.         pf();///invoke  
  19.         ++q;  
  20.     }  
  21.     cout<<"*q="<<(*q)<<endl;  
  22. }  


结果:


可以看到,第一个输出的是Derive::f,然后是Base::g、Base::h、Derive::g1、Derive::h1、0。据此,虚函数表就明了了:


结论:

1) 派生类中重写的函数f()覆盖了基类的函数f(),并放在虚函数表中原来基类中f()的位置。

2) 没有覆盖的函数位置不变。


这样,对于下面的调用:

[cpp] view plaincopyprint?
  1. Base *b;  
  2. b = new Derive();  
  3. b->f();///Derive::f  

由于b指向的对象的虚函数表的位置已经是Derive::f()(就是上面的那个图),实际调用时,调用的就是Derive::f(),这就实现了多态。


多重继承(无虚函数覆盖)


假设继承关系如下:


对于派生类实例中的虚函数表,如下图所示:


(这个画起来太麻烦了,就借用下别人的图,但是注意:图中写函数的地方实际为指向该函数的指针)

测试代码:

[cpp] view plaincopyprint?
  1. void t6()  
  2. {  
  3.     typedef void (*pFun)(void);  
  4.     Derive d;  
  5.     pFun pf=0;  
  6.     int * p = (int*)(&d);///强制转换为int*,这样就取得  
  7.     ///b的vptr的地址  
  8.     cout<<"Object addr: "<<p<<endl;  
  9.     for(int j=0; j<3; j++)  
  10.     {  
  11.         int *q;  
  12.         q=(int*)*p;///*p取得vtable,强转为int*,  
  13.         ///取得指向第一个函数地址的指针q  
  14.         cout<<j+1<<"th virtual table addr: "<<(int*)q<<endl;  
  15.         for(int i=0; i<6; ++i)  
  16.         {  
  17.             pf = (pFun)*q;///对指针解引用,取得函数地址  
  18.             if((int)pf<=0) break;///到末尾了  
  19.             cout<<i+1<<"th virtual fun addr: "<<(int*)(*q)<<endl;  
  20.             pf();///invoke  
  21.             ++q;  
  22.         }  
  23.         cout<<"*q="<<(*q)<<"\n"<<endl;  
  24.         p++;///下一个vptr  
  25.     }  
  26. }  


结果验证:


从上面的运行结果可以看出,第一个虚函数表的末尾不再是0了,而是一个负数,第二个变成一个更小的负数,最后一个0表示结束。【这个应该和编译器版本有关,看别人的只要没有结束都是1,结束时是0。在本机上测试vc6.0每个虚函数表都是以0为结尾】

结论:

1) 每个基类都有自己的虚表;

2) 派生类虚函数放在第一个虚表的后半部分。

如果这时候运行如下代码:

     Base1 *b = new Derive();

b->f();

结果为:Base1::f,因为在名字查找时,最先找到Base1::f后不再继续查找,然后类型检查没错,就调用这个了。

多重继承(有虚函数覆盖-- ??有疑问??)

现在继承关系修改为:


这时派生类实例的虚函数表为:


验证代码同上t6()。结果:



结论:基类中的虚函数被替换为派生类的函数。



但是为什么3Derive::f的地址为什么不一样(见下图红框标注的部分)?难道编译器生成了三个同样的函数?感觉不应该这样。。。这和第二篇博主的图(见文章末尾)也不一样。。有没有高手解释下?


运行这个结果的完整代码:

[cpp] view plaincopyprint?
  1. /************************ 
  2.  * g++ (GCC) 4.6.2 
  3.  * Win7 x64 
  4.  * Code::Blocks 12.11 
  5.  ************************/  
  6. #include <iostream>  
  7.   
  8. using namespace std;  
  9.   
  10.   
  11. class Base1  
  12. {  
  13. public:  
  14.     virtual void f(){cout<<"Base1::f"<<endl;}  
  15.     virtual void g(){cout<<"Base1::g"<<endl;}  
  16.     virtual void h(){cout<<"Base1::h"<<endl;}  
  17. };  
  18.   
  19. class Base2  
  20. {  
  21. public:  
  22.     virtual void f(){cout<<"Base2::f"<<endl;}  
  23.     virtual void g(){cout<<"Base2::g"<<endl;}  
  24.     virtual void h(){cout<<"Base2::h"<<endl;}  
  25. };  
  26. class Base3  
  27. {  
  28. public:  
  29.     virtual void f(){cout<<"Base3::f"<<endl;}  
  30.     virtual void g(){cout<<"Base3::g"<<endl;}  
  31.     virtual void h(){cout<<"Base3::h"<<endl;}  
  32. };  
  33.   
  34. class Derive : public Base1, public Base2,public Base3  
  35. {  
  36. public:  
  37.     virtual void f(){cout<<"Derive::f"<<endl;}  
  38.     virtual void g1(){cout<<"Derive::g1"<<endl;}  
  39.     //virtual void h1(){cout<<"Derive::h1"<<endl;}  
  40. };  
  41.   
  42. void t6()  
  43. {  
  44.     typedef void (*pFun)(void);  
  45.     Derive d;  
  46.     pFun pf=0;  
  47.     int * p = (int*)(&d);///强制转换为int*,这样就取得  
  48.     ///b的vptr的地址  
  49.     cout<<"Object addr: "<<p<<endl;  
  50.     for(int j=0; j<3; j++)  
  51.     {  
  52.         int *q;  
  53.         q=(int*)*p;///*p取得vtable,强转为int*,  
  54.         ///取得指向第一个函数地址的指针q  
  55.         cout<<j+1<<"th virtual table addr: "<<(int*)q<<endl;  
  56.         for(int i=0; i<6; ++i)  
  57.         {  
  58.             pf = (pFun)*q;///对指针解引用,取得函数地址  
  59.             if((int)pf<=0) break;//  
  60.             cout<<i+1<<"th virtual fun addr: "<<(int*)(pf)<<endl;  
  61.             pf();///invoke  
  62.             ++q;  
  63.         }  
  64.         cout<<"*q="<<(*q)<<"\n"<<endl;  
  65.         p++;  
  66.     }  
  67. }  
  68.   
  69. int main()  
  70. {  
  71.     t6();  
  72.     return 0;  
  73. }  



这时,如果使用基类指针去调用相关函数,那么实际运行时将根据指针指向的实际类型调用相关函数了:

    

[cpp] view plaincopyprint?
  1. Derive d;  
  2. Base1 *b1 = &d;  
  3. Base2 *b2 = &d;  
  4. Base3 *b3 = &d;  
  5. b1->f();///Derive::f  
  6. b2->f();///Derive::f  
  7. b3->f();///Derive::f  
  8. b1->g();///Base1::f  
  9. b2->g();///Base2::f  
  10. b3->g();///Base3::f  

缺点

5.1效率问题

动态绑定在函数调用时需要在虚函数表中查找,所以性能比静态函数调用稍低。


5.2通过基类类型的指针访问派生类自己的虚函数

虽然在上面的继承关系图中可以看到Base1 的虚表中有Derive的虚函数,但是以下语句是非法的:

    Base1 *b1 = new Derive();

    b1->g1();///编译错误:Base1没有成员g1

如果一定要访问,只能通过上面的强制转换指针类型来完成了。


5.3访问非public成员

把基类和派生类的成员fh这些都改成private的,你会发现上述通过指针访问成员函数没有任何问题。

这就是C++!



参考:

http://blog.163.com/cocoa_20/blog/static/25396006200972332219165/

http://blog.chinaunix.net/uid-24178783-id-370328.html

http://bdxnote.blog.163.com/blog/static/8444235200911311348529/

首先感谢上述三篇博主,你们的文章对我帮助很大。


第一篇文章将的挺清楚,但是貌似把函数重载和重写弄混了,至于第二个,那个图比较清晰,但是和我实验结果不符。。。还有就是一些概念不对,感觉博主对程序在计算机中的装载过程、地址变换、逻辑地址、物理地址这些东西不是很清楚。