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