动态绑定(dynamic binding):动态绑定是指在执行期间(非编译期)判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。
C++中,通过基类的引用或指针调用虚函数时,发生动态绑定。引用(或指针)既可以指向基类对象也可以指向派生类对象,这一事实是动态绑定的关键。用引用(或指针)调用的虚函数在运行时确定,被调用的函数是引用(或指针)所指对象的实际类型所定义的。
联编:联编是指一个计算机程序自身彼此关联的过程,在这个联编过程中,需要确定程序中的操作调用(函数调用)与执行该操作(函数)的代码段之间的映射关系;按照联编所进行的阶段不同,可分为静态联编和动态联编;
静态联编:是指联编工作是在程序编译连接阶段进行的,这种联编又称为早期联编;因为这种联编是在程序开始运行之前完成的;在程序编译阶段进行的这种联编又称静态束定;在编译时就解决了程序中的操作调用与执行该操作代码间的关系,确定这种关系又被称为束定;编译时束定又称为静态束定;
动态联编:编译程序在编译阶段并不能确切地知道将要调用的函数,只有在程序执行时才能确定将要调用的函数,为此要确切地知道将要调用的函数,要求联编工作在程序运行时进行,这种在程序运行时进行的联编工作被称为动态联编,或动态束定,又叫晚期联编;C++规定:动态联编是在虚函数的支持下实现的;
静态联编和动态联编都是属于多态性的,它们是在不同的阶段进对不同的实现进行不同的选择。
1 虚函数表
C++中动态绑定是通过虚函数实现的。而虚函数是通过一张虚函数表(virtual table)实现的。这个表中记录了虚函数的地址,解决继承、覆盖的问题,保证动态绑定时能够根据对象的实际类型调用正确的函数。
先说这个虚函数表。据说在C++的标准规格说明书中说到,编译器必需要保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证正确取到虚函数的偏移量)。这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。
假设有如下基类:
- class Base
- {
- public:
- virtual void f(){cout<<"Base::f"<<endl;}
- virtual void g(){cout<<"Base::g"<<endl;}
- virtual void h(){cout<<"Base::h"<<endl;}
- };
按照上面的说法,写代码验证:
- void t1()
- {
- typedef void (*pFun)(void);
- Base b;
- pFun pf=0;
- int * p = (int*)(&b);///强制转换为int*,这样就取得
- ///b的vptr的地址
- cout<<"VTable addr: "<<p<<endl;
- int *q;
- q=(int*)*p;///*p取得vtable,强转为int*,
- ///取得指向第一个函数地址的指针q
- cout<<"virtual table addr: "<<q<<endl;
- pf = (pFun)*q;///对指针解引用,取得函数地址
- cout<<"first virtual fun addr: "<<(int*)(*q)<<endl;
- pf();///invoke
- }
运行结果:
运行环境为:
/************************
* g++ (GCC) 4.6.2
* Win7 x64
* Code::Blocks 12.11
************************/
其他环境没有测试,下同。
按照同样的方法,继续调用g()和h(),只要移动指针即可:
- void t2()
- {
- typedef void (*pFun)(void);
- Base b;
- pFun pf=0;
- int * p = (int*)(&b);///强制转换为int*,这样就取得
- ///b的vptr的地址
- cout<<"VTable addr: "<<p<<endl;
- int *q;
- q=(int*)*p;///*p取得vtable,强转为int*,
- ///取得指向第一个函数地址的指针q
- cout<<"virtual table addr: "<<q<<endl;
- pf = (pFun)*q;///对指针解引用,取得函数地址
- cout<<"first virtual fun addr: "<<(int*)(*q)<<endl;
- pf();///invoke
- ++q;///q指向第二个虚函数
- pf = (pFun)*q;///对指针解引用,取得函数地址
- cout<<"second virtual fun addr: "<<(int*)(*q)<<endl;
- pf();///invoke
- ++q;///q指向第3个虚函数
- pf = (pFun)*q;///对指针解引用,取得函数地址
- cout<<"third virtual fun addr: "<<(int*)(*q)<<endl;
- pf();///invoke
- /** ============================ **/
- ++q;///q指向第4个虚函数?
- cout<<"4th virtual fun addr: "<<(int*)(*q)<<endl;
- }
分割线以后的那部分说明:显然只有3个虚函数,再往后移动就没有了,那没有是什么?取出来看发现是0,如果把这个地址继续当做函数地址调用,那必然出错了。看来这个表的最后一个地址为0表示虚函数表的结束。结果如下:
如果只看代码,可能会晕。。。。
看个形象点的图吧:
其中标*的地方这里是0。
2 有覆盖时的虚函数表
没有覆盖的虚函数没有太大意义,要实现动态绑定,必须在派生类中覆盖基类的虚函数。
2.1 继承时没有覆盖
假设有如下继承关系:
在这里,派生类没有覆盖任何基类函数。那么在派生类的实例中,其虚函数表如下所示:
测试代码也很简单,只要修改对象的地址为新的地址,然后继续往后移动指针就行了:
- void t3()
- {
- typedef void (*pFun)(void);
- Derive d;
- pFun pf=0;
- int * p = (int*)(&d);///强制转换为int*,这样就取得
- ///d的vptr的地址
- cout<<"VTable addr: "<<p<<endl;
- int *q;
- q=(int*)*p;///*p取得vtable,强转为int*,
- ///取得指向第一个函数地址的指针q
- cout<<"virtual table addr: "<<q<<endl;
- for(int i=0; i<6; ++i)
- {
- pf = (pFun)*q;///对指针解引用,取得函数地址
- cout<<i+1<<"th virtual fun addr: "<<(int*)(*q)<<endl;
- pf();///invoke
- ++q;
- }
- cout<<"*q="<<(*q)<<endl;
- }
结果:
结论:
1) 虚函数按照其声明顺序放在VTable中;
2) 基类的虚函数在派生类虚函数前面;
2.2 继承时有虚函数覆盖
假设继承关系如下:
注意:这时函数f()在派生类中重写了。
先测试下,看看虚函数表是什么样子的:
测试代码:
- void t4()
- {
- typedef void (*pFun)(void);
- Derive d;
- pFun pf=0;
- int * p = (int*)(&d);///强制转换为int*,这样就取得
- ///b的vptr的地址
- cout<<"VTable addr: "<<p<<endl;
- int *q;
- q=(int*)*p;///*p取得vtable,强转为int*,
- ///取得指向第一个函数地址的指针q
- cout<<"virtual table addr: "<<q<<endl;
- for(int i=0; i<6; ++i)
- {
- pf = (pFun)*q;///对指针解引用,取得函数地址
- if(pf==0) break;
- cout<<i+1<<"th virtual fun addr: "<<(int*)(*q)<<endl;
- pf();///invoke
- ++q;
- }
- cout<<"*q="<<(*q)<<endl;
- }
结果:
可以看到,第一个输出的是Derive::f,然后是Base::g、Base::h、Derive::g1、Derive::h1、0。据此,虚函数表就明了了:
结论:
1) 派生类中重写的函数f()覆盖了基类的函数f(),并放在虚函数表中原来基类中f()的位置。
2) 没有覆盖的函数位置不变。
这样,对于下面的调用:
- Base *b;
- b = new Derive();
- b->f();///Derive::f
由于b指向的对象的虚函数表的位置已经是Derive::f()(就是上面的那个图),实际调用时,调用的就是Derive::f(),这就实现了多态。
3 多重继承(无虚函数覆盖)
假设继承关系如下:
对于派生类实例中的虚函数表,如下图所示:
(这个画起来太麻烦了,就借用下别人的图,但是注意:图中写函数的地方实际为指向该函数的指针)
测试代码:
- void t6()
- {
- typedef void (*pFun)(void);
- Derive d;
- pFun pf=0;
- int * p = (int*)(&d);///强制转换为int*,这样就取得
- ///b的vptr的地址
- cout<<"Object addr: "<<p<<endl;
- for(int j=0; j<3; j++)
- {
- int *q;
- q=(int*)*p;///*p取得vtable,强转为int*,
- ///取得指向第一个函数地址的指针q
- cout<<j+1<<"th virtual table addr: "<<(int*)q<<endl;
- for(int i=0; i<6; ++i)
- {
- pf = (pFun)*q;///对指针解引用,取得函数地址
- if((int)pf<=0) break;///到末尾了
- cout<<i+1<<"th virtual fun addr: "<<(int*)(*q)<<endl;
- pf();///invoke
- ++q;
- }
- cout<<"*q="<<(*q)<<"\n"<<endl;
- p++;///下一个vptr
- }
- }
结果验证:
从上面的运行结果可以看出,第一个虚函数表的末尾不再是0了,而是一个负数,第二个变成一个更小的负数,最后一个0表示结束。【这个应该和编译器版本有关,看别人的只要没有结束都是1,结束时是0。在本机上测试vc6.0每个虚函数表都是以0为结尾】
结论:
1) 每个基类都有自己的虚表;
2) 派生类虚函数放在第一个虚表的后半部分。
如果这时候运行如下代码:
Base1 *b = new Derive();
b->f();
结果为:Base1::f,因为在名字查找时,最先找到Base1::f后不再继续查找,然后类型检查没错,就调用这个了。
4 多重继承(有虚函数覆盖-- ??有疑问??)
现在继承关系修改为:
这时派生类实例的虚函数表为:
验证代码同上t6()。结果:
结论:基类中的虚函数被替换为派生类的函数。
但是为什么3个Derive::f的地址为什么不一样(见下图红框标注的部分)?难道编译器生成了三个同样的函数?感觉不应该这样。。。这和第二篇博主的图(见文章末尾)也不一样。。有没有高手解释下?
运行这个结果的完整代码:
- /************************
- * g++ (GCC) 4.6.2
- * Win7 x64
- * Code::Blocks 12.11
- ************************/
- #include <iostream>
- using namespace std;
- class Base1
- {
- public:
- virtual void f(){cout<<"Base1::f"<<endl;}
- virtual void g(){cout<<"Base1::g"<<endl;}
- virtual void h(){cout<<"Base1::h"<<endl;}
- };
- class Base2
- {
- public:
- virtual void f(){cout<<"Base2::f"<<endl;}
- virtual void g(){cout<<"Base2::g"<<endl;}
- virtual void h(){cout<<"Base2::h"<<endl;}
- };
- class Base3
- {
- public:
- virtual void f(){cout<<"Base3::f"<<endl;}
- virtual void g(){cout<<"Base3::g"<<endl;}
- virtual void h(){cout<<"Base3::h"<<endl;}
- };
- class Derive : public Base1, public Base2,public Base3
- {
- public:
- virtual void f(){cout<<"Derive::f"<<endl;}
- virtual void g1(){cout<<"Derive::g1"<<endl;}
- //virtual void h1(){cout<<"Derive::h1"<<endl;}
- };
- void t6()
- {
- typedef void (*pFun)(void);
- Derive d;
- pFun pf=0;
- int * p = (int*)(&d);///强制转换为int*,这样就取得
- ///b的vptr的地址
- cout<<"Object addr: "<<p<<endl;
- for(int j=0; j<3; j++)
- {
- int *q;
- q=(int*)*p;///*p取得vtable,强转为int*,
- ///取得指向第一个函数地址的指针q
- cout<<j+1<<"th virtual table addr: "<<(int*)q<<endl;
- for(int i=0; i<6; ++i)
- {
- pf = (pFun)*q;///对指针解引用,取得函数地址
- if((int)pf<=0) break;//
- cout<<i+1<<"th virtual fun addr: "<<(int*)(pf)<<endl;
- pf();///invoke
- ++q;
- }
- cout<<"*q="<<(*q)<<"\n"<<endl;
- p++;
- }
- }
- int main()
- {
- t6();
- return 0;
- }
这时,如果使用基类指针去调用相关函数,那么实际运行时将根据指针指向的实际类型调用相关函数了:
- Derive d;
- Base1 *b1 = &d;
- Base2 *b2 = &d;
- Base3 *b3 = &d;
- b1->f();///Derive::f
- b2->f();///Derive::f
- b3->f();///Derive::f
- b1->g();///Base1::f
- b2->g();///Base2::f
- b3->g();///Base3::f
5 缺点
5.1效率问题
动态绑定在函数调用时需要在虚函数表中查找,所以性能比静态函数调用稍低。
5.2通过基类类型的指针访问派生类自己的虚函数
虽然在上面的继承关系图中可以看到Base1 的虚表中有Derive的虚函数,但是以下语句是非法的:
Base1 *b1 = new Derive();
b1->g1();///编译错误:Base1没有成员g1
如果一定要访问,只能通过上面的强制转换指针类型来完成了。
5.3访问非public成员
把基类和派生类的成员f、h这些都改成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/
首先感谢上述三篇博主,你们的文章对我帮助很大。
第一篇文章将的挺清楚,但是貌似把函数重载和重写弄混了,至于第二个,那个图比较清晰,但是和我实验结果不符。。。还有就是一些概念不对,感觉博主对程序在计算机中的装载过程、地址变换、逻辑地址、物理地址这些东西不是很清楚。