天天看點

淺析靜态綁定和動态綁定 靜态綁定 & 動态綁定

靜态綁定 & 動态綁定

  • 靜态綁定(statically bound),又名前期綁定(early binding);
  • 動态綁定(dynamically bound),又名延期綁定(late binding)。

ps 英文名稱摘自《Effective C++》 條款37。此條款中有關于“靜态類型、動态類型”的描述。

在 C 語言中并沒有“靜态綁定”、“動态綁定”的概念(至少我沒有查到)。

我了解的 C++ 類的記憶體模型其實就是 C 語言中的 struct 結構體。但成員函數又是歸屬于類所有的,是以就存在函數到類的綁定。

  • 靜态綁定指的就是這種綁定關系(映射關系)是在編譯期間确定的;
  • 動态綁定指的就是這種綁定關系(映射關系)在編譯期間确定不了,得等到程式運作、執行期間才能最終确定

我們樸素地分析一下靜态綁定、動态綁定。函數都有其位址,函數調用翻譯成彙編代碼其實就是直接用位址,很明顯在彙編代碼這個層次(甚至不用這麼底層,C語言層次就行)不同的函數實作有不同的位址(使用C 語言的話,就有不同的函數名),但在 C++、Java 進階語言這個層次,不同的實作可能有相同的函數名(有很多種情況:重載、不同類裡面相同名稱、模闆、繼承體系中的重寫),在 C++ 代碼中出現一個函數調用(函數名),怎麼正确地找到對應的實作(位址)呢?

  1. 重載:因為參數類型或者個數不同其實是有區分的,直接在進階語言下一個層次(比如C語言層面)使用不同的命名重新包裝就可以了。調用時根據傳參的情況,再映射就可以了。
  2. 不同的 class 裡面相同名稱:編譯器實作這個完全可以和重載情形使用同樣的方案。維護一個映射表就可以。
  3. 模闆:暫時不了解
  4. 繼承體系的重寫:

我們都知道 override 函數時,兩個函數的聲明式肯定是一模一樣的,如果不考慮 override 的概念(具體到代碼中就是不使用 

virtual

 關鍵字),那麼其場景和上述第 2 中就是一樣的——如果是 base 類型(即便是指針)就調用 base class 的函數,如果是 derived 類型就調用 derived class 的函數。事實上都是編譯期間根據維護的映射表“偷梁換柱”(映射是在聲明的類型(函數聲明式、類類型)-具體的函數實作之間),直接把對應的位址拿過來,ok,彙編代碼完成了。

繼承體系中允許 base 指針是可以指向 derived 對象,但編譯器依舊是根據聲明指針時的類型去映射具體的函數實作的,是以會出現一些變态的現象:

  • class Base 無 void func(),class Derived 有 void func(),我們執行 

    Base *p=new Derived(); p->func();

     會報錯找不到
  • class Base 的 void func() 列印 base,class Derived 的 void func() 列印 derived,我們執行 

    Base *p=new Derived(); p->func();

     會列印出 base 納尼
  • class myclass 有函數 simple(),函數實作中沒有對 this 解引用的操作(不管是顯式的還是隐在的),我們執行 

    myclass *p=NULL; p->simple();

     能夠正确執行

so,正如我們看到的,這就是靜态綁定。

随着 OO 越來與流行,為了獲得多态性,我們想要打破這種規則——繼承體系中允許基類指針指向派生類對象,在此基礎上我們想讓基類指針可以調用派生類的函數,我們要讓例二 

p->func()

 列印 derive 怎麼辦?

好吧,增添新的語言特性,使用關鍵詞 

virtual

,用來表明碰到這個類的指針(或引用)調用此函數時不要根據靜态類型(聲明指針的類型)映射具體實作,你們要根據這個指針指向的對象的實際類型來映射具體實作(即動态綁定)。編譯器說,納尼,我靠,我哪知道啊?我隻解析 declaration,隻分析了變量聲明的類型,記憶體的初始化、指派在運作期才發生呢……好吧,編譯器感覺為難做不了這個事情,隻能把這個找到(綁定)函數具體實作的步驟放到運作期間了,可是效率會低一些呢。如果你要多态性,隻能接受了。

具體實作中通過 

virtual

 關鍵詞标記延遲綁定,然後在運作過程中,根據對象記憶體中的虛函數表指針獲得函數位址。

如果不是通過指針或引用調用虛函數,也是在編譯期間就綁定的;而沒有 

virtual

 修飾的函數根據調用者的靜态類型在編譯期間直接綁定。

// Base 類有虛函數 func()
// Derived 繼承自 Base,且 override 了 func()
   Base *p=new Derived(); 
   p->func();
           

我們覺得一目了然的事情,比如編譯器彙編時直接把 

p->func()

 調用換成 

derived::func(p)

 不就好了嗎,實作起來很難嗎?事實上編譯器是卡在它隻知道 p 是 

base *

 類型,它并不知曉 p 被初始化(指派)了什麼,它隻生成“得到一個位址,把這個位址賦給 base 指針 p;根據 p 指明的位址調用 func()” 的指令,至于前一條“配置設定記憶體,初始化 derived 對象”的指令,現在是前後相鄰緊挨着,其他場景可能這兩條指令相差十萬八千裡呢。事實上我們隻會在測試時寫 

Base *p=new Derived(); p->func();

 這樣的例子,在真實的業務場景中為了效率至少應該寫成 

Derived derived; derived.func();

,在能夠确定類型的時候使用靜态綁定效率更高。實際上真實的業務場景多是

繼續閱讀