我們分三個方面來說明虛函式以及用虛函式實現的包含多型。
第乙個:什麼是虛函式?
從語法上來說虛函式就是用
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關鍵字宣告為虛函式。當我們使用乙個基類型別的引用或者指標,呼叫乙個虛函式時就引發動態繫結 ...