程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> C++的動態綁定和靜態綁定

C++的動態綁定和靜態綁定

編輯:C++入門知識

為了支持c++的多態性,才用了動態綁定和靜態綁定。理解他們的區別有助於更好的理解多態性,以及在編程的過程中避免犯錯誤,需要理解四個名詞:
1、對象的靜態類型:對象在聲明時采用的類型。是在編譯期確定的。
2、對象的動態類型:目前所指對象的類型。是在運行期決定的。

對象的動態類型可以更改,但是靜態類型無法更改。關於對象的靜態類型和動態類型,看一個示例:

class B
{
}
class C : public B
{
}
class D : public B
{
}
D* pD = new D();//pD的靜態類型是它聲明的類型D*,動態類型也是D*
B* pB = pD;//pB的靜態類型是它聲明的類型B*,動態類型是pB所指向的對象pD的類型D*
C* pC = new C();
pB = pC;//pB的動態類型是可以更改的,現在它的動態類型是C*
3、靜態綁定:綁定的是對象的靜態類型,某特性(比如函數)依賴於對象的靜態類型,發生在編譯期。

4、動態綁定:綁定的是對象的動態類型,某特性(比如函數)依賴於對象的動態類型,發生在運行期。

class B
{
    void DoSomething();
    virtual void vfun();
}
class C : public B
{
    void DoSomething();//首先說明一下,這個子類重新定義了父類的no-virtual函數,這是一個不好的設計,會導致名稱遮掩;這裡只是為了說明動態綁定和靜態綁定才這樣使用。
    virtual void vfun();
}
class D : public B
{
    void DoSomething();
    virtual void vfun();
}
D* pD = new D();
B* pB = pD;
讓我們看一下,pD->DoSomething()和pB->DoSomething()調用的是同一個函數嗎?
不是的,雖然pD和pB都指向同一個對象。因為函數DoSomething是一個no-virtual函數,它是靜態綁定的,也就是編譯器會在編譯期根據對象的靜態類型來選擇函數。pD的靜態類型是D*,那麼編譯器在處理pD->DoSomething()的時候會將它指向D::DoSomething()。同理,pB的靜態類型是B*,那pB->DoSomething()調用的就是B::DoSomething()。
 
讓我們再來看一下,pD->vfun()和pB->vfun()調用的是同一個函數嗎?
是的。因為vfun是一個虛函數,它動態綁定的,也就是說它綁定的是對象的動態類型,pB和pD雖然靜態類型不同,但是他們同時指向一個對象,他們的動態類型是相同的,都是D*,所以,他們的調用的是同一個函數:D::vfun()。
 
上面都是針對對象指針的情況,對於引用(reference)的情況同樣適用。
 
指針和引用的動態類型和靜態類型可能會不一致,但是對象的動態類型和靜態類型是一致的。
D D;
D.DoSomething()和D.vfun()永遠調用的都是D::DoSomething()和D::vfun()。
 
至於那些事動態綁定,那些事靜態綁定,有篇文章總結的非常好:
我總結了一句話:只有虛函數才使用的是動態綁定,其他的全部是靜態綁定。目前我還沒有發現不適用這句話的,如果有錯誤,希望你可以指出來。
 
特別需要注意的地方 www.2cto.com
當缺省參數和虛函數一起出現的時候情況有點復雜,極易出錯。我們知道,虛函數是動態綁定的,但是為了執行效率,缺省參數是靜態綁定的。

class B
{
 virtual void vfun(int i = 10);
}
class D : public B
{
 virtual void vfun(int i = 20);
}
D* pD = new D();
B* pB = pD;
pD->vfun();
pB->vfun();

有上面的分析可知pD->vfun()和pB->vfun()調用都是函數D::vfun(),但是他們的缺省參數是多少?
分析一下,缺省參數是靜態綁定的,pD->vfun()時,pD的靜態類型是D*,所以它的缺省參數應該是20;同理,pB->vfun()的缺省參數應該是10。編寫代碼驗證了一下,正確。
對於這個特性,估計沒有人會喜歡。所以,永遠記住:
“絕不重新定義繼承而來的缺省參數(Never redefine function’s inherited default parameters value.)”
 
關於c++語言
目前我基本上都是在c++的子集“面向對象編程”下工作,對於更復雜的知識了解的還不是很多。即便如此,到目前為止編程時需要注意的東西已經很多,而且後面可能還會繼續增多,這也許是很多人反對c++的原因。
c++是Google的四大官方語言之一。但是Google近幾年確推出了go語言,而且定位是和c/c++相似。考慮這種情況,我認為可能是Google的程序員們深感c++的復雜,所以想開發一種c++的替代語言。有時間要了解一下go語言,看它在類似c++的問題上時如何取捨的。

 

對於C++動態綁定的理解,一句話,就是編譯器用靜態分析的方法加上虛擬函數的設計實現在程序運行時動態

智能執行正確虛擬函數的技術。因此要徹底理解動態綁定技術,只需要掌握兩點,一是編譯器的靜態編譯過程,二是
虛擬函數的基本知識。只要有了這兩點理解,任何動態綁定的分析都是很容易的。

下面就以例子代碼說明:


#include <iostream>
using namespace std;

class A
...{
public:
void fA() ...{ cout << "A::fA()" << endl; }
virtual void vfA() ...{ cout << "A::vfA()" << endl; }
void emptyB() ...{ cout << "A::emptyB()" << endl; }
void vfAonly() ...{ cout << "A::vfAonly()" << endl; }
};

class B : public A
...{
public:
void fB() ...{ cout << "B::fB()" << endl; }
virtual void vfA() ...{ cout << "B::vfA()" << endl; }
virtual void vfB() ...{ cout << "B::vfB()" << endl; }
void emptyA() ...{ cout << "B::emptyA()" << endl; }
virtual void vfAonly() ...{ cout << "B::vfAonly()" << endl; }
};

int main()
...{
A* p = new B;
B& r = *(B*)p;

        p->fA();            // 1
//p->fB();            // 2
p->vfA();            // 3
//p->vfB();            // 4
//p->emptyA();        // 5
p->emptyB();        // 6
p->vfAonly();        // 7

        cout << endl;

        r.fA();                // 8
r.fB();                // 9
r.vfA();            // 10
r.vfB();            // 11
r.emptyA();            // 12
r.emptyB();            // 13
r.vfAonly();        // 14

        delete p;
return 0;
}

 輸出結果:

A::fA()
B::vfA()
A::emptyB()
A::vfAonly()

A::fA()
B::fB()
B::vfA()
B::vfB()
B::emptyA()
A::emptyB()
B::vfAonly()

 

分析:

我們通過模擬編譯器的編譯過程來進行解釋。只看編譯器是怎麼編譯帶有標號的那些函數調用的行的。

行1. 在編譯器眼中,p就是一個純粹的A類指針,跟他指向的B類對象沒有任何聯系。因此,當看到
p->fA()時,編譯器便去A的定義中尋找fA,找到了,於是生成調用代碼。
行2. 這行如果不被注釋,編譯器去A的定義中尋找定義fB,但是找不到這個名字,便會輸出錯誤信息。
行3. 編譯器繼續去A定義中尋找vfA,這次找到了,而且發現關鍵字virtual,於是,采用虛擬函數調用
代碼生成技術,根據vfA的偏移值,生成代碼調用虛擬函數表中該偏移值指向的函數。特別指出的
是,在靜態編譯期間,編譯器只知道偏移值,並不知道運行時該偏移到底指向什麼函數。實際效果
是,因為運行時,p指向的是B對象,因此調用的是B的虛擬函數vfA().
行4. 這行如果不被注釋,編譯器去A的定義中尋找名字vfB,找不到,出錯。記住第一條原則,編譯器
是靜態編譯,不知道p和類B有聯系。
行5. 同4,找不到名字emptyA。
行6. 簡單,找到名字emptyB.
行7. 簡單,找到名字vfAonly。

行8. 從這裡開始,函數由B類引用r調用。在編譯器眼中,r就是一個純粹的B類引用,他不假設r和A有任何
關系。因此這一行,編譯器去B類定義尋找名字fA。由於B繼承自A,包括所有A的public函數定義,
編譯器成功找到A::fA。
行9. 類似行8,找到B自身的函數定義fB。
行10. 類似行3,編譯器生成代碼調用虛擬函數表某偏移指向的函數。運行時該偏移指向B::vfA.
行11. 編譯器生成代碼調用虛擬函數表某偏移指向的函數。運行時該偏移指向B::vfB.
行12. 簡單,找到名字emptyA.
行13. 簡單,找到名字A::emptyB. 因為B繼承自A。
行14. 編譯器生成代碼調用虛擬函數表某偏移指向的函數。運行時該偏移指向B::vfAonly. 為什麼編譯器知道
指向的是B的虛擬函數vfAonly而不是A的非虛擬函數呢?這跟另一個靜態編譯規則,名字隱藏,有關。
繼承類的作用域中如果有基類的同名函數,繼承類中的名字將隱藏基類同名函數,因此這時,編譯器看
不見A::vfAonly。

 


摘自 Slow Dance

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved