C++中的动态绑定

C++中的动态绑定

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


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


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


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


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


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


虚函数表

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.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(),这就实现了多态。


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


假设继承关系如下:


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


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

测试代码:

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后不再继续查找,然后类型检查没错,就调用这个了。

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

现在继承关系修改为:


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


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



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



但是为什么3Derive::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.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. 基类和派生类中使用同一名字的成员函数,其行为和数据成员一样。

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

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





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章