C 的多型性實現機制剖析

2022-07-04 01:42:13 字數 4960 閱讀 2442

我們先看乙個例子:

例1- 1

#include

class animal

public:

void sleep()

voidbreathe()

class

fish:public animal

public:

voidbreathe()

void main()

fishfh;

animal*pan=&fh;

pan->breathe();

}注意,在例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

public:

void sleep()

virtualvoid breathe()

class

fish:public animal

public:

void breathe()

void main()

fish fh;

animal *pan=&fh;

pan->breathe();

}大家可以再次執行這個程式,你會發現結果是「fish bubble」,也就是根據物件的型別呼叫了正確的函式。

那麼當我們將breathe()宣告為virtual時,在背後發生了什麼呢?

編譯器在編譯的時候,發現animal類中有虛函式,此時編譯器會為每個包含虛函式的類建立乙個虛表(即

vtable

),該表是乙個一維陣列,在這個陣列中存放每個虛函式的位址。對於例1-2的程式,animal和fish類都包含

了乙個虛函式breathe(),因此編譯器會為這兩個類都建立乙個虛表,如下圖所示:

&animal::breathe()

animal類的

vtable

animal::breathe

()&fish::breathe()

fish類的

vtable

fish::breathe

()圖1- 2 animal類和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、 派生類的虛表中虛函式位址的排列順序和基類的虛表中虛函式位址排列順序相同。

例1- 3

#include

class base;

base * pbase;

class base

public:

base()

virtual void fn()

class

derived:public base

void fn()

derived

aa;void main()

pbase->fn();

}我在base類的建構函式中將this指標儲存到

pbase

全域性變數中。在定義全域性物件

aa,即呼叫derived aa;

時,要呼叫基類的建構函式,先構造基類的部分,然後是子類的部分,由這兩部分拼接出完整的物件

aa。這個this指標指向的當然也就是

aa物件,那麼我們在main()函式中利用

pbase

呼叫fn(),因為

pbase

實際指向的是

aa物件,而

aa物件內部的虛表指標指向的是自身的虛表,最終呼叫的當然是derived類中的fn()函式。

在這個例子中,由於我的疏忽,在derived類中宣告fn()函式時,忘了加public關鍵字,導致宣告為了private(預設為private),但通過前面我們所講述的虛函式呼叫機制,我們也就明白了這個地方並不影響它輸出正確的結果。不知道這算不算c++的乙個bug,因為虛函式的呼叫是在執行時確定呼叫哪乙個函式,所以編譯器在編譯時,並不知道

pbase

指向的是

aa物件,所以導致這個奇怪現象的發生。如果你直接用

aa物件去呼叫,由於物件型別是確定的(注意

aa是物件變數,不是指標變數),編譯器往往會採用早期繫結,在編譯時確定呼叫的函式,於是就會發現fn()是私有的,不能直接呼叫。:)

許多學員在寫這個例子時,直接在基類的建構函式中呼叫虛函式,前面已經說了,在呼叫基類的建構函式時,編譯器只「看到了」父類,並不知道後面是否後還有繼承者,它只是初始化父類物件的虛表指標,讓該虛表指標指向父類的虛表,所以你看到結果當然不正確。只有在子類的構造函式呼叫完畢後,整個虛表才構建完畢,此時才能真正應用c++的多型性。換句話說,我們不要在建構函式中去呼叫虛函式,當然如果你只是想呼叫本類的函式,也無所謂。

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 1 include class animal voidbreathe class fish public animal void main 注意,在例 1 1的程式中沒有定義虛函式。考慮一下例 1 1的程式執行的結果是什麼?答案是輸出 animal breathe 我們在m...