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. 假設函數調用合法,編譯器就生成代碼。如果是虛函數且通過引用或者指針調用,則編譯器生成代碼以確定根據對象的動態類型運行哪個函數版本,否則編譯器生成代碼直接調用函數。





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