c++
的虛函式
下面是對c++的虛函式這玩意兒的理解。
一, 什麼是虛函式
(如果不知道虛函式為何物,但又急切的想知道,那你就應該從這裡開始)
簡單地說,那些被virtual關鍵字修飾的成員函式,就是虛函式。虛函式的作用,用專業術語來解釋就是實現多型性(polymorphism),多型性是將介面與實現進行分離;用形象的語言來解釋就是實現以共同的方法,但因個體差異而採用不同的策略。下面來看一段簡單的**
class a;
class b:public a;
int main()
通過class a和class b的print()這個介面,可以看出這兩個class因個體的差異而採用了不同的策略,輸出的結果也是我們預料中的,分別是this is a和this is b。但這是否真正做到了多型性呢?no,多型還有個關鍵之處就是一切用指向基類的指標或引用來操作物件。那現在就把main()處的**改一改。
int main()
執行一下看看結果,喲呵,驀然回首,結果卻是兩個this is a。問題來了,p2明明指向的是class b的物件但卻是呼叫的class a的print()函式,這不是我們所期望的結果,那麼解決這個問題就需要用到虛函式
class a;
class b:public a;
毫無疑問,class a的成員函式print()已經成了虛函式,那麼class b的print()成了虛函式了嗎?回答是yes,我們只需在把基類的成員函式設為virtual,其派生類的相應的函式也會自動變為虛函式。所以,class b的print()也成了虛函式。那麼對於在派生類的相應函式前是否需要用virtual關鍵字修飾,那就是你自己的問題了。
現在重新執行main2的**,這樣輸出的結果就是this is a和this is b了。
現在來消化一下,我作個簡單的總結,指向基類的指標在操作它的多型類物件時,會根據不同的類物件,呼叫其相應的函式,這個函式就是虛函式。
二, 虛函式是如何做到的
(如果你沒有看過《inside the c++ object model》這本書,但又急切想知道,那你就應該從這裡開始)
虛函式是如何做到因物件的不同而呼叫其相應的函式的呢?現在我們就來剖析虛函式。我們先定義兩個類
class a;
class b:public a;
由於這兩個類中有虛函式存在,所以
編譯器就會為他們兩個分別插入一段你不知道的資料,並為他們分別建立乙個表。那段資料叫做vptr指標,指向那個表。那個表叫做vtbl,每個類都有自己的vtbl,vtbl的作用就是儲存自己類中虛函式的位址,我們可以把vtbl形象地看成乙個陣列,這個陣列的每個元素存放的就是虛函式的位址,請看圖
通過左圖,可以看到這兩個vtbl分別為class a和class b服務。現在有了這個模型之後,我們來分析下面的**
a *p=new a;
p->fun();
毫無疑問,呼叫了a::fun(),但是a::fun()是如何被呼叫的呢?它像普通函式那樣直接跳轉到函式的**處嗎?no,其實是這樣的,首先是取出vptr的值,這個值就是vtbl的位址,再根據這個值來到vtbl這裡,由於呼叫的函式a::fun()是第乙個虛函式,所以取出vtbl第乙個slot裡的值,這個值就是a::fun()的位址了,最後呼叫這個函式。現在我們可以看出來了,只要vptr不同,指向的vtbl就不同,而不同的vtbl裡裝著對應類的虛函式位址,所以這樣虛函式就可以完成它的任務。
而對於class a和class b來說,他們的vptr指標存放在何處呢?其實這個指標就放在他們各自的例項物件裡。由於class a和class b都沒有資料成員,所以他們的例項物件裡就只有乙個vptr指標。通過上面的分析,現在我們來實作一段**,來描述這個帶有虛函式的類的簡單模型。
#include
using namespace std;
//將上面「虛函式示例**」新增在這裡
int main()
用vc或dev-c++編譯執行一下,看看結果是不是輸出3,如果不是,那麼太陽明天肯定是從西邊出來。現在一步一步開始分析
void (*fun)(a*); 這段定義了乙個函式指標名字叫做fun,而且有乙個a*型別的引數,這個函式指標待會兒用來儲存從vtbl裡取出的函式位址
a* p=new b; new b是向記憶體(記憶體分5個區:全域性名字空間,自由儲存區,暫存器,**空間,棧)自由儲存區申請乙個記憶體單元的位址然後隱式地儲存在乙個指標中.然後把這個位址賦值給a型別的指標p.
. long lvptraddr; 這個long型別的變數待會兒用來儲存vptr的值
memcpy(&lvptraddr,p,4); 前面說了,他們的例項物件裡只有vptr指標,所以我們就放心大膽地把p所指的4bytes記憶體裡的東西複製到lvptraddr中,所以複製出來的4bytes內容就是vptr的值,即vtbl的位址
現在有了vtbl的位址了,那麼我們現在就取出vtbl第乙個slot裡的內容
memcpy(&fun,reinterpret_cast(lvptraddr),4); 取出vtbl第乙個slot裡的內容,並存放在函式指標fun裡。需要注意的是lvptraddr裡面是vtbl的位址,但lvptraddr不是指標,所以我們要把它先轉變成指標型別
fun(p); 這裡就呼叫了剛才取出的函式位址裡的函式,也就是呼叫了b::fun()這個函式,也許你發現了為什麼會有引數p,其實類成員函式呼叫時,會有個this指標,這個p就是那個this指標,只是在一般的呼叫中編譯器自動幫你處理了而已,而在這裡則需要自己處理。
delete p; 釋放由p指向的自由空間;
system("pause"); 螢幕暫停;
如果呼叫b::fun2()怎麼辦?那就取出vtbl的第二個slot裡的值就行了
memcpy(&fun,reinterpret_cast(lvptraddr+4),4); 為什麼是加4呢?因為乙個指標的長度是4bytes,所以加4。或者memcpy(&fun,reinterpret_cast(lvptraddr)+1,4); 這更符合陣列的用法,因為lvptraddr被轉成了long*型別,所以+1就是往後移sizeof(long)的長度
三, 以一段**開始
#include
using namespace std;
class a;
class b:public a; //end//虛函式示例**2
int main()
你能估算出輸出結果嗎?如果你估算出的結果是a::fun和a::fun2,呵呵,恭喜恭喜,你中圈套了。其實真正的結果是b::fun和b::fun2,如果你想不通就接著往下看。給個提示,&a::fun和&a::fun2是真正獲得了虛函式的位址嗎?
首先我們回到第二部分,通過段實作**,得到乙個「通用」的獲得虛函式位址的方法
#include
using namespace std;
//將上面「虛函式示例**2」新增在這裡
void callvirtualfun(void* pthis,int index=0)
int main()
callvirtualfun方法
現在我們擁有乙個「通用」的callvirtualfun方法。
這個通用方法和第三部分開始處的**有何聯絡呢?聯絡很大。由於a::fun()和a::fun2()是虛函式,所以&a::fun和&a::fun2獲得的不是函式的位址,而是一段間接獲得虛函式位址的一段**的位址,我們形象地把這段**看作那段callvirtualfun。編譯器在編譯時,會提供類似於callvirtualfun這樣的**,當你呼叫虛函式時,其實就是先呼叫的那段類似callvirtualfun的**,通過這段**,獲得虛函式位址後,最後呼叫虛函式,這樣就真正保證了多型性。同時大家都說虛函式的效率低,其原因就是,在呼叫虛函式之前,還呼叫了獲得虛函式位址的**。
最後的說明
本文的**可
以用vc6
和dev-c++4.9.8.0
通過編譯,且執行無問題。其他的編譯器小弟不敢保證。其中,裡面的模擬方法只能看成模型,因為不同的編譯器的低層實現是不同的。例如
this
指標,dev-c++
的gcc
就是通過壓棧,當作引數傳遞,而
vc的編譯器則通過取出位址儲存在
ecx中。所以這些模擬方法不能當作具體實現。
C 虛函式 純虛函式
1 基本概念 虛函式是在基類中使用關鍵字virtual宣告的函式。在派生類中重新定義基類中定義的虛函式時,會告訴編譯器不要靜態鏈結到該函式。我們想要的是在程式中任意點可以根據所呼叫的物件型別來選擇呼叫的函式,這種操作被稱為動態鏈結,或後期繫結。您可能想要在基類中定義虛函式,以便在派生類中重新定義該函...
C 虛函式 純虛函式
1.析構函式是否應為虛函式問題?2.成員函式的虛函式問題?3.析構函式是否可以為純虛函式問題?說明 僅在使用父類指標指向子類物件時有區別 當析構函式非虛函式時,使用父類指標指向子類物件,在析構時將不會呼叫子類析構函式 當析構函式是虛函式時,使用分類指標指向子類物件,在析構時會呼叫子類析構函式,且呼叫...
C 虛函式 純虛函式
include qdebug class animal void animal animal 即要實現基類animal的animal函式 假如在dog子類中沒有實現animal 函式,則會呼叫基類的animal 函式。即列印 what is the animal 假如在dog子類中實現了animal...