C++中的动态绑定 对虚表的一些测试

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


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


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


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


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


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


虚函数表

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


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


假设有如下基类:


[cpp]  view plain  copy
 print ?
  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 plain  copy
 print ?
  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 plain  copy
 print ?
  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 plain  copy
 print ?
  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 plain  copy
 print ?
  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 plain  copy
 print ?
  1. Base *b;  
  2. b = new Derive();  
  3. b->f();///Derive::f  

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


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


假设继承关系如下:


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


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

测试代码:

[cpp]  view plain  copy
 print ?
  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 plain  copy
 print ?
  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 plain  copy
 print ?
  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/

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


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


=========================20130924更新===============================

6. 补充 继承与名字查找规则

规则0. 名字查找在编译时发生


规则1. 与基类同名的派生类成员将屏蔽对基类成员的直接访问。

如果一定要访问,则必须使用作用域操作符限定访问基类成员。一般来说,派生类中重新定义的成员最好不要和基类中的成员同名。


规则2. 基类和派生类中使用同一名字的成员函数,其行为和数据成员一样。

即使函数原型不同,基类成员也会被屏蔽。

[cpp]  view plain  copy
 print ?
  1. struct Base  
  2. {  
  3.     int  f();  
  4. };  
  5. struct Derived: Base  
  6. {  
  7.     int f(int);  
  8. };  
  9.   
  10. Derived d;  
  11. Base  b;  
  12. b.f();<span style="white-space:pre">    </span>//ok, Base::f()  
  13. d.f(100);  //ok  Derived::f(int)  
  14. d.f();//error  no Derived::f()  
  15. d.Base::f(); //ok Base::f()  
 

d.f()调用出错的原因是:编译器在Derived中查找名字f,一旦找到该名字,就不在继续查找了。然后进行参数类型检查,发现类型错误,于是报错。


规则3. 如果在派生类中对基类的虚函数进行重写,则原型必须完全一样。

否则就会出现2中的情况,并没有真正实现多态。


规则4. 函数调用遵循以下四个步奏:

a.首先确定进行函数调用的对象、引用或指针的静态类型;

b.在该类中查找函数,如果找不到,就在直接基类中查找,如此沿着类的继承链往上找,直到找到该函数或者查找完最后一个类。

如果不能找到该名字,则调用出错。

c.一旦找到该名字,就进行常规类型检查,如果匹配,则调用合法;如果类型不匹配,则报错。(停止继续查找)

d.假设函数调用合法,编译器就生成代码。如果是虚函数且通过引用或者指针调用,则编译器生成代码以确定根据对象的动态类型运行哪个函数版本,否则编译器生成代码直接调用函数。


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值