C++多態性:虛函數的調用原理

多態性給我們帶來了好處:多態使得我們可以通過基類的引用或指針來指明一個對象(包含其派生類的對象),當調用函數時可以自動判斷調用的是哪個對象的函數。
一個函數說明爲虛函數,表明在繼承的類中重載這個函數時,當調用這個函數時應當查看以確定調用哪個對象的這個函數。
普通函數的處理:一個特定的函數都會映射到特定的代碼,無論時編譯階段還是連接階段,編譯器都能計算出這個函數的地址,調用即可。
虛函數的處理:被調用的函數不僅依據調用的特定函數,還依據調用的對象的種類。通常是由虛函數表(vtable)來實現的。
虛函數表的結構:它是一個函數指針表,每一個表項都指向一個函數。任何一個包含至少一個虛函數的類都會有這樣一張表。需要注意的是vtable只包含虛函數的指針,沒有函數體。實現上是一個函數指針的數組。虛函數表既有繼承性又有多態性。每個派生類的vtable繼承了它各個基類的vtable,如果基類vtable中包含某一項,則其派生類的vtable中也將包含同樣的一項,但是兩項的值可能不同。如果派生類重載(override)了該項對應的虛函數,則派生類vtable的該項指向重載後的虛函數,沒有重載的話,則沿用基類的值。
每一個類只有唯一的一個vtable,不是每個對象都有一個vtable,恰恰是每個同一個類的對象都有一個指針,這個指針指向該類的vtable(當然,前提是這個類包含虛函數)。那麼,每個對象只額外增加了一個指針的大小,一般說來是4字節。
      在類對象的內存佈局中,首先是該類的vtable指針,然後纔是對象數據。

      在通過對象指針調用一個虛函數時,編譯器生成的代碼將先獲取對象類的vtable指針,然後調用vtable中對應的項。對於通過對象指針調用的情況,在編譯期間無法確定指針指向的是基類對象還是派生類對象,或者是哪個派生類的對象(見代碼中的函數f在編譯期間是無法判斷的)。但是在運行期間執行到調用語句時,這一點已經確定,編譯後的調用代碼能夠根據具體對象獲取正確的vtable,調用正確的虛函數,從而實現多態性。

給出實例代碼:
class A {
public :
    virtual void run (){......}
}
class B :public A{
public:
    void run(){......}
}
int f (A *pA){
    pA->run();
}
分析一下這裏的思想所在,問題的實質是這樣,對於發出虛函數調用的這個對象指針,在編譯期間缺乏更多的信息,而在運行期間具備足夠的信息,但那時已不再進行綁定了而是直接執行好了,怎麼在二者之間作一個過渡呢?把綁定所需的信息用一種通用的數據結構記錄下來,該數據結構可以同對象指針相聯繫,在編譯時只需要使用這個數據結構進行抽象的綁定,而在運行期間將會得到真正的綁定。這個數據結構就是vtable,也就是編譯期間建立vtable表,執行期間查表執行。可以看到,實現用戶所需的抽象和多態需要進行後綁定,而編譯器又是通過抽象和多態而實現後綁定的。
下面是通過基類的指針來調用虛函數時,所發生的一切:
step 1:開始執行調用 pA->run();(這裏能判斷到底是哪個對象)
step 2:取得對象的vtable的指針
step 3:從vtable那裏獲得函數入口的偏移量,即得到要調用的函數的指針
step 4:根據vtable的地址找到函數,並調用函數。
step 1和step 4對於一般函數是一樣的,虛函數只是多了step 2和step 3。
解惑:
1基類和派生類是共用一表,還是各有各的表(物理上)
答:基類和派生類是各有各的表,也就是說他們的物理地址是分開的,基類和派生類的虛表的唯一關聯是:當派生類沒有實現基類虛函數的重載時,派生類會直接把自己表的該函數地址值寫爲基類的該函數地址值.


本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/cxyol/archive/2006/03/12/622319.aspx

發佈了34 篇原創文章 · 獲贊 4 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章