我們先看乙個例子: 例
1- 1
#include
class
animal
voidbreathe()
};
class
fish:public animal
}; void
main()
注意,在例
1-1的程式中沒有定義虛函式。考慮一下例
1-1的程式執行的結果是什麼?
答案是輸出:
animal
breathe
我們在main()
函式中首先定義了乙個
fish
類的物件
fh,接著定義了乙個指向
animal
類的指標變數
pan,將
fh的位址賦給了指標變數
pan,然後利用該變數呼叫
pan->breathe()
。許多學員往往將這種情況和
c++的多型性搞混淆,認為
fh實際上是
fish
類的物件,應該是呼叫
fish
類的breathe()
,輸出「
fish bubble
」,然後結果卻不是這樣。下面我們從兩個方面來講述原因。 1、
編譯的角度
c++編譯器在編譯的時候,要確定每個物件呼叫的函式的位址,這稱為早期繫結(
early binding
),當我們將
fish
類的物件
fh的位址賦給
pan時,
c++編譯器進行了型別轉換,此時
c++編譯器認為變數
pan儲存的就是
animal
物件的位址。當在
main()
函式中執行
pan->breathe()
時,呼叫的當然就是
animal
物件的breathe
函式。
2、記憶體模型的角度
我們給出了
fish
物件記憶體模型,如下圖所示:
animal
的物件所佔記憶體
fish
的物件自身增加的部分
fish
類的物件所佔記憶體
圖1- 1
fish
類物件的記憶體模型
我們構造
fish
類的物件時,首先要呼叫
animal
類的建構函式去構造
animal
類的物件,然後才呼叫
fish
類的建構函式完成自身部分的構造,從而拼接出乙個完整的
fish
物件。當我們將
fish
類的物件轉換為
animal
型別時,該物件就被認為是原物件整個記憶體模型的上半部分,也就是圖
1-1中的「
animal
的物件所佔記憶體」。那麼當我們利用型別轉換後的物件指標去呼叫它的方法時,當然也就是呼叫它所在的記憶體中的方法。因此,輸出
animal breathe
,也就順理成章了。
正如很多學員所想,在例
1-1的程式中,我們知道
pan實際指向的是
fish
類的物件,我們希望輸出的結果是魚的呼吸方法,即呼叫
fish
類的breathe
方法。這個時候,就該輪到虛函式登場了。
前面輸出的結果是因為編譯器在編譯的時候,就已經確定了物件呼叫的函式的位址,要解決這個問題就要使用遲繫結(
late binding
)技術。當編譯器使用遲繫結時,就會在執行時再去確定物件的型別以及正確的呼叫函式。而要讓編譯器採用遲繫結,就要在基類中宣告函式時使用
virtual
關鍵字(注意,這是必須的,很多學員就是因為沒有使用虛函式而寫出很多錯誤的例子),這樣的函式我們稱為虛函式。一旦某個函式在基類中宣告為
virtual
,那麼在所有的派生類中該函式都是
virtual
,而不需要再顯式地宣告為
virtual。
下面修改例
1-1的**,將
animal
類中的breathe()
函式宣告為
virtual
,如下: 例
1- 2
#include
class
animal
virtualvoid breathe()
}; class
fish:public animal
}; void
main()
大家可以再次執行這個程式,你會發現結果是「
fish bubble
」,也就是根據物件的型別呼叫了正確的函式。
那麼當我們將
breathe()
宣告為virtual
時,在背後發生了什麼呢?
編譯器在編譯的時候,發現
animal
類中有虛函式,此時編譯器會為每個包含虛函式的類建立乙個虛表(即
vtable
),該表是乙個一維陣列,在這個陣列中存放每個虛函式的位址。對於例
1-2的程式,
animal
和fish
類都包含
了乙個虛函式
breathe()
,因此編譯器會為這兩個類都建立乙個虛表,如下圖所示:
&animal::breathe
()
animal
類的vtable
animal::breathe()
&fish::breathe
()
fish
類的vtable
fish::breathe()
圖1-
2animal
類和fish
類的虛表
那麼如何定位虛表呢?編譯器另外還為每個類的物件提供了乙個虛表指標(即
vptr
),這個指標指向了物件所屬類的虛表。在程式執行時,根據物件的型別去初始化
vptr
,從而讓
vptr
正確的指向所屬類的虛表,從而在呼叫虛函式時,就能夠找到正確的函式。對於例
1-2的程式,由於
pan實際指向的物件型別是
fish
,因此vptr
指向的fish
類的vtable
,當呼叫
pan->breathe()
時,根據虛表中的函式位址找到的就是
fish
類的breathe()
函式。
正是由於每個物件呼叫的虛函式都是通過虛表指標來索引的,也就決定了虛表指標的正確初始化是非常重要的。換句話說,在虛表指標沒有正確初始化之前,我們不能夠去呼叫虛函式。那麼虛表指標在什麼時候,或者說在什麼地方初始化呢?
答案是在建構函式中進行虛表的建立和虛表指標的初始化。還記得建構函式的呼叫順序嗎,在構造子類物件時,要先呼叫父類的建構函式,此時編譯器只「看到了」父類,並不知道後面是否後還有繼承者,它初始化父類物件的虛表指標,該虛表指標指向父類的虛表。當執行子類的建構函式時,子類物件的虛表指標被初始化,指向自身的虛表。對於例
2-2的程式來說,當
fish
類的fh
物件構造完畢後,其內部的虛表指標也就被初始化為指向
fish
類的虛表。在型別轉換後,呼叫
pan->breathe()
,由於pan
實際指向的是
fish
類的物件,該物件內部的虛表指標指向的是
fish
類的虛表,因此最終呼叫的是
fish
類的breathe()
函式。
要注意:對於虛函式呼叫來說,每乙個物件內部都有乙個虛表指標,該虛表指標被初始化為本類的虛表。所以在程式中,不管你的物件型別如何轉換,但該物件內部的虛表指標是固定的,所以呢,才能實現動態的物件函式呼叫,這就是
c++多型性實現的原理。
總結(基類有虛函式): 1、
每乙個類都有虛表。 2、
虛表可以繼承,如果子類沒有重寫虛函式,那麼子類虛表中仍然會有該函式的位址,只不過這個位址指向的是基類的虛函式實現。如果基類3個虛函式,那麼基類的虛表中就有三項(虛函式位址),派生類也會有虛表,至少有三項,如果重寫了相應的虛函式,那麼虛表中的位址就會改變,指向自身的虛函式實現。如果派生類有自己的虛函式,那麼虛表中就會新增該項。 3、
派生類的虛表中虛函式位址的排列順序和基類的虛表中虛函式位址排列順序相同。
C 的多型性實現機制剖析
c 的多型性實現機制剖析 即 vc this 指標詳細說明 2006年1 月12日星期四 我們先看乙個例子 例 1 1 include class animal void breathe class fish public animal void main 注意,在例 1 1的程式中沒有定義虛函式。...
C 的多型性實現機制剖析
1 多型性和虛函式 我們先看乙個例子 考慮一下這段程式的輸出結果是什麼?答案是輸出 animal breath 我們在main函式中首先定義乙個fish類的物件fh,接著定義了乙個指向animal類的指標pan,將fn的位址賦給了指標變數pan,然後利用該變數呼叫pan breath 許多人往往將這...
C 的多型性實現機制剖析
1 多型性和虛函式 我們先看乙個例子 cpp view plain copy include class animal void breath class fish public animal void main 考慮一下這段程式的輸出結果是什麼?答案是輸出 animal breath 我們在mai...