C 多型實現機制剖析

2021-07-14 17:27:16 字數 3860 閱讀 8367

物件導向的三大概念:封裝,繼承,多型。

封裝突破了c語言函式的概念;繼承實現了**的復用,那麼多型實現了什麼價值呢,簡單理解就是前人寫的**(框架)可以呼叫後人寫的**。

1 什麼是多型?

多型性可以簡單的概括為「1個介面,多種方法」,在程式執行的過程中才決定呼叫的機制

程式實現上是這樣:通過父類指標呼叫子類的函式,可以讓父類指標有多種形態。

2. 理解多型成立的三個條件

對比思考間接賦值成立的三個條件:1. 定義兩個變數,2.建立變數之間的聯絡,3.*p通過位址間接賦值。

多型的單個條件:1.要有繼承。2. 要有虛函式重寫。3.要有父類指標(引用)指向子類物件。

我們先從乙個例子來**

#include

class animal

void breathe()

};class fish:public animal

};void main()

答案是輸出:

animal breathe

結果分析:

1從編譯的角度

c++編譯器在編譯的時候,要確定每個物件呼叫的函式的位址,這稱為早期繫結(early binding),當我們將fish類的物件fh的位址賦給pan時,c++編譯器進行了型別轉換,此時c++編譯器認為變數pan儲存的就是animal物件的位址。當在main()函式中執行pan->breathe()時,呼叫的當然就是animal物件的breathe函式。

2 記憶體模型的角度

我們構造fish類的物件時,首先要呼叫animal類的建構函式去構造animal類的物件,然後才呼叫fish類的建構函式完成自身部分的構造,從而拼接出乙個完整的fish物件。當我們將fish類的物件轉換為animal型別時,該物件就被認為是原物件整個記憶體模型的上半部分,也就是圖1-1中的「animal的物件所佔記憶體」。那麼當我們利用型別轉換後的物件指標去呼叫它的方法時,當然也就是呼叫它所在的記憶體中的方法。因此,輸出animal breathe,也就順理成章了。

那麼為了得到我們想要的結果,就要使用虛函式

前面輸出的結果是因為編譯器在編譯的時候,就已經確定了物件呼叫的函式的位址,要解決這個問題就要使用

遲繫結(late binding)技術。

當編譯器使用遲繫結時,就會在執行時再去確定物件的型別以及正確的呼叫函式

。而要讓編譯器採用遲繫結,就要在基類中宣告函式時使用virtual關鍵字(注意,這是必須的,很多學員就是因為沒有使用虛函式而寫出很多錯誤的例子),這樣的函式我們稱為虛函式。一旦某個函式在基類中宣告為virtual,

那麼在所有的派生類中該函式都是virtual

,而不需要再顯式地宣告為virtual。

下面我們將上面一段**進行部分修改

virtual void breathe()

執行結果:fish bubble

結果分析

編譯器為每個類的物件都有乙個指向虛函式表的指標(vptr指標)。在程式執行時,根據

物件的型別去初始化vptr

,從而讓vptr正確的指向所屬類的虛表,從而在呼叫虛函式時,就能夠找到正確的函式。

例中,由於pan實際指向的物件型別是fish,因此vptr指向的fish類的vtable(虛函式表),當呼叫pan->breathe()時,根據虛表中的函式位址找到的就是fish類的breathe()函式。

正是由於

每個物件呼叫的虛函式都是通過虛表指標來索引的

,也就決定了虛表指標的正確初始化是非常重要的。換句話說,在虛表指標沒有正確初始化之前,我們不能夠去呼叫虛函式。那麼虛表指標在什麼時候,或者說在什麼地方初始化呢?

答案是在

建構函式中進行虛表的建立和虛表指標的初始化

。還記得建構函式的呼叫順序嗎,在構造子類物件時,要先呼叫父類的建構函式,此時編譯器只「看到了」父類,並不知道後面是否後還有繼承者,它初始化父類物件的虛表指標,該虛表指標指向父類的虛表。當執行子類的建構函式時,子類物件的虛表指標被初始化,指向自身的虛表。

當fish類的fh物件構造完畢後,其內部的虛表指標也就被初始化為指向fish類的虛表。在型別轉換後,呼叫pan->breathe(),由於pan實際指向的是fish類的物件,該物件內部的虛表指標指向的是fish類的虛表,因此最終呼叫的是fish類的breathe()函式。

為了更加清楚的說明記憶體分布:下面詳細的介紹記憶體的分布

1 基類的記憶體分布情況

請看下面的sample

class a

};則sizeof(a)=1;

如果改為如下:

class a

void g()

}則sizeof(a)=4! 這是因為在類a中存在virtual function,為了實現多型,每個含有virtual function的類中都隱式包含著乙個靜態虛指標vfptr指向該類的靜態虛表vtable, vtable中的表項指向類中的每個virtual function的入口位址

例如 我們declare 乙個a型別的object :

a c;

a d;

則編譯後其記憶體分布如下:

從 vfptr所指向的vtable可以看出,每個virtual function都占有乙個entry,例如本例中的f函式。而g函式因為不是virtual型別,故不在vtable的表項之內。說明:vtab屬於類成員靜態pointer,而vfptr屬於物件pointer

2 繼承類的記憶體分布狀況

假設**如下:

public b:public a};則

a c;

a d;

b e;

編譯後,其記憶體分布如下:

從中我們可以看出,b型別的物件e有乙個vfptr指向vtable address:0x00400030 ,而a型別的物件c和d共同指向類的vtable address:0x00400050a

3 動態繫結過程的實現

我們說多型是在程式進行動態繫結得以實現的,而不是編譯時就確定物件的呼叫方法的靜態繫結。

其過程如下:

程式執行到動態繫結時,通過

基類的指標所指向的物件型別

,通過vfptr找到其所指向的vtable,然後呼叫其相應的方法,即可實現多型。

例如:a c;

b e;

a *pc=&e; //設定breakpoint,執行到此處

pc=&c;

此時記憶體中各指標狀況如下:

可以看出,此時pc指向類b的虛表位址,從而呼叫物件e的方法。

繼續執行,當執行至pc=&c時候,此時pc的vptr值為0x00420050,即指向類a的vtable位址,從而呼叫c的方法。

這就是動態繫結!(dynamic binding)或者叫做遲後聯編(lazy compile)。

總結:對於虛函式呼叫來說,每乙個物件內部都有乙個虛表指標,該虛表指標被初始化為本類的虛表。所以在程式中,不管你的物件型別如何轉換,但該物件內部的虛表指標是固定的,所以呢,才能實現動態的物件函式呼叫,這就是c++多型性實現的原理。

需要注意的幾點

總結(基類有虛函式):

1、每乙個類都有虛表。

2、虛表可以繼承,如果子類沒有重寫虛函式,那麼子類虛表中仍然會有該函式的位址,只不過這個位址指向的是基類的虛函式實現。如果基類3個虛函式,那麼基類的虛表中就有三項(虛函式位址),派生類也會有虛表,至少有三項,如果重寫了相應的虛函式,那麼虛表中的位址就會改變,指向自身的虛函式實現。如果派生類有自己的虛函式,那麼虛表中就會新增該項。

3、派生類的虛表中虛函式位址的排列順序和基類的虛表中虛函式位址排列順序相同。

C 多型實現機制剖析

物件導向的三大概念 封裝,繼承,多型。封裝突破了c語言函式的概念 繼承實現了 的復用,那麼多型實現了什麼價值呢,簡單理解就是前人寫的 框架 可以呼叫後人寫的 多型性可以簡單的概括為 1個介面,多種方法 在程式執行的過程中才決定呼叫的機制 程式實現上是這樣 通過父類指標呼叫子類的函式,可以讓父類指標有...

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 許多人往往將這...