C++中的动态绑定
动态绑定(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/
首先感谢上述三篇博主,你们的文章对我帮助很大。
第一篇文章将的挺清楚,但是貌似把函数重载和重写弄混了,至于第二个,那个图比较清晰,但是和我实验结果不符。。。还有就是一些概念不对,感觉博主对程序在计算机中的装载过程、地址变换、逻辑地址、物理地址这些东西不是很清楚。
=========================20130924更新===============================
6. 补充 继承与名字查找规则
规则0. 名字查找在编译时发生
规则1. 与基类同名的派生类成员将屏蔽对基类成员的直接访问。
如果一定要访问,则必须使用作用域操作符限定访问基类成员。一般来说,派生类中重新定义的成员最好不要和基类中的成员同名。
规则2. 基类和派生类中使用同一名字的成员函数,其行为和数据成员一样。
即使函数原型不同,基类成员也会被屏蔽。
struct Base
{
int f();
};
struct Derived: Base
{
int f(int);
};
Derived d;
Base b;
b.f(); //ok, Base::f()
d.f(100); //ok Derived::f(int)
d.f();//error no Derived::f()
d.Base::f(); //ok Base::f()
d.f()调用出错的原因是:编译器在Derived中查找名字f,一旦找到该名字,就不在继续查找了。然后进行参数类型检查,发现类型错误,于是报错。
规则3. 如果在派生类中对基类的虚函数进行重写,则原型必须完全一样。
否则就会出现2中的情况,并没有真正实现多态。
规则4. 函数调用遵循以下四个步奏:
a. 首先确定进行函数调用的对象、引用或指针的静态类型;
b. 在该类中查找函数,如果找不到,就在直接基类中查找,如此沿着类的继承链往上找,直到找到该函数或者查找完最后一个类。
如果不能找到该名字,则调用出错。
c. 一旦找到该名字,就进行常规类型检查,如果匹配,则调用合法;如果类型不匹配,则报错。(停止继续查找)
d. 假设函数调用合法,编译器就生成代码。如果是虚函数且通过引用或者指针调用,则编译器生成代码以确定根据对象的动态类型运行哪个函数版本,否则编译器生成代码直接调用函数。