C 中 虛函式及包含多型的實現

2021-07-31 08:24:46 字數 4291 閱讀 6166

我們分三個方面來說明虛函式以及用虛函式實現的包含多型。

第乙個:什麼是虛函式?

從語法上來說虛函式就是用

virtual

宣告的函式。所以定義乙個虛函式很簡單。重點是你需要知道我們如何用虛函式解決實際的問題。

第二個:編譯器是如何解析函式呼叫語句的?

通常我們是用乙個型別定義乙個物件,或者

new乙個物件,然後用這個型別的指標指向它,然後用物件或者指標來呼叫它所擁有的函式。某些時候(其實是經常)我們會遇到在子類中覆蓋父類方法的情況,根據我們前面所說的指標賦值相容性規則,我們用下面的例子詳細說明一下這種語法情況,然後和後面的多型進行對比:

class a

};class b : public a

};int _tmain(int argc,_tchar* argv)

仔細觀察乙個,編譯器在編譯乙個方法呼叫語句時,總是根據呼叫方法的物件或者指標的型別來呼叫對應的方法。那麼就引出了我們的第三個問題。

第三:我們如何根據父類的指標來呼叫子類的方法呢?

答案就是我們前面所說的虛函式。

下面我們用實際的**來看一下虛函式的作用,以及其記憶體模型。

class a

};class b : public a

};int _tmain(int argc,_tchar* argv)

第一種情況,用

b的物件給

a的物件賦值,實際上是呼叫了

a的拷貝建構函式,也就是用b1

中a的部分去給a1

賦值,所以我們可以認為

b的物件中包含乙個

a物件(可能這就是為什麼叫做包含多型吧)。此時用

a1呼叫

fun()就確實是用

a的物件來呼叫

fun()方法

其他三種情況a2

,a3,a4

實際上表示或者指向的都是乙個

b型別的物件,所有後面輸出的就都是「b的

hello」

為了弄清楚程式究竟是如何工作的,我們從反彙編和記憶體的角度來看乙個它的記憶體模型。

在前面所說的物件的記憶體模型中,我們知道程式在執行時會先把

方法**載入到**區,然後

把方法的入口位址以jmp位址的方式給出方法入口表。而我們在**中對方法的呼叫會被編譯器自動轉換為

call入口表中對應入口位址

的方式。

現在再來看一下含有虛函式的情況:

pb4->fun(); // b

的hello

00fe6a91  mov         eax,dword ptr [pb4] 

//1-

把物件的首位址複製到eax

00fe6a94  mov         edx,dword ptr [eax] 

// 2-

把以eax的內容為位址的4個位元組的記憶體空間的內容複製到edx,實際上它就是虛指標。

00fe6a96  mov        esi,esp 

00fe6a98  mov         ecx,dword ptr [pb4] 

// ecx

存放對應物件的首位址

00fe6a9b  mov         eax,dword ptr [edx] 

// 3-

然後把以edx的內容為位址的記憶體空間的內容複製到eax,現在eax就是函式入口表中對應的函式入口位址了。

00fe6a9d  call        eax 

//4-

呼叫,跳轉

00fe6a9f  cmp        esi,esp 

00fe6aa1  call       __rtc_checkesp (0fe136bh) 

a *pa4 = pb4;

00fe6aa6  mov        eax,dword ptr [pb4] 

00fe6aa9  mov        dword ptr [pa4],eax 

pa4->fun();// b

的hello

00fe6aac  mov        eax,dword ptr [pa4] 

00fe6aaf  mov        edx,dword ptr [eax] 

00fe6ab1  mov        esi,esp 

00fe6ab3  mov        ecx,dword ptr [pa4] 

00fe6ab6  mov        eax,dword ptr [edx] 

00fe6ab8  call       eax 

如果你看完上面加粗的說明之後還沒有頭暈,那麼恭喜你,你不是正常人^^

作為正常人,我需要去看看記憶體中的具體資料。

第一步:

mov  eax, dword ptr [pb4]      [pb4] == pb

4 =

0x0061d7e0

,執行之後

eax=

0x0061d7e0

它所對應的前四個位元組的內容為

0x00fed998

第二步:

movedx,dword ptr [eax] ; [eax] =

以0x0061d7e0

為位址的記憶體單元的內容,也就是

0x00fed998

也就是虛指標,此時

edx= 0x

00fed998

我們看一下

0x00fed998

可以看到,此處的內容都是

00fe

開頭的一些記憶體空間的位址。

第三步:

moveax,dword ptr [edx]; [edx] = 以0x

00fed998

為首位址的記憶體單元的內容,也就是

0x00fe151e

此時,eax = 00fe151e

第四步:

call

eax;

在call

後面,說明

eax儲存的是可執行**了,所以我們檢視一下此處的反彙編

a::a:

00fe1519  jmp        a::a (0fe68f0h) 

b::fun:

00fe151e jmp         b::fun (0fe2dd0h) 

a::fun:

00fe1523  jmp        a::fun (0fe2d70h) 

b::b:

00fe1528  jmp        b::b (0fe98f0h) 

a::a:

00fe152d  jmp        a::a (0fe6950h) 

可以看到,從

call

開始,進入到普通函式呼叫的函式入口表中的函式入口位址。

(ps:

虛表和函式入口表是在一段記憶體空間中存放)

總結一下,

對於含有虛函式的類,在生成物件時,會在物件的前四個位元組儲存乙個

虛指標,我們稱虛指標指向的記憶體空間是乙個

虛表,它是由一系列虛函式的入口的位址組成的,然後用

call

進行程式的跳轉,回到普通函式的呼叫方法上。

對應的我們可以和虛基類對比一下,虛繼承之後的類在生成物件時,同樣會在物件開始儲存乙個虛指標,只不過虛指標指向的虛表中儲存的不是函式的入口位址,而是物件中,基類的屬性成員的偏移位址。

現在我們已經說明了虛函式用途和使用方式,以及包含多型的意義,需要明確以下幾點:

1、虛函式會造成額外的記憶體開支,所以只應該在類層次結構中並且需要使用多型時才使用 2

、虛函式應該是公有的,並且類層次之間的繼承也應該是公有的,否則就沒什麼意義了,

或者有其他的特殊情況,再特殊處理。

多型實現及虛函式

多型是c 物件導向三大特性之一 多型分為兩類 1.靜態多型 函式過載和運算子過載屬於靜態多型 2.動態多型 派生類與虛函式實現執行時多型 區別 靜態多型的函式位址早繫結 編譯階段確定函式位址 動態多型的函式位址晚繫結 執行階段確定函式位址 class animal class cat public ...

C 中的多型及虛函式大總結

多型是c 中很關鍵的一部分,在物件導向程式設計中的作用尤為突出,其含義是具有多種形式或形態的情形,簡單來說,多型 向不同物件傳送同乙個訊息,不同的物件在接收時會產生不同的行為。即用乙個函式名可以呼叫不同內容的函式。多型可分為靜態多型與動態多型,靜態多型的實現在於靜態聯編,關聯出現在編譯階段而非執行期...

C 多型,虛函式作用及底層實現原理

簡述c 虛函式作用及底層實現原理 c 是物件導向程式設計,其包括3項特點 1 資料抽象 介面和實現分離 2 繼承 父類和子類 3 多型 動態繫結 本文討論多型。當父類希望子類重新定義某些函式時,用virtual關鍵字宣告為虛函式。當我們使用乙個基類型別的引用或者指標,呼叫乙個虛函式時就引發動態繫結 ...