虛函式

虛函式

在某基類中聲明為 virtual 並在一個或多個派生類中被重新定義的成員函式,用法格式為:virtual 函式返回類型 函式名(參數表) {函式體};實現多態性,通過指向派生類的基類指針或引用,訪問派生類中同名覆蓋成員函式。

基本介紹

  • 中文名:虛函式
  • 外文名:virtual function
  • 定義:被virtual關鍵字修飾的成員函式
  • 作用:實現多態性
  • 形象解釋:求同存異
  • 關鍵:用指向基類指針或引用操作對象
  • 聲明:virtual 
定義,輸出結果,實現,代碼示例,

定義

簡單地說,那些被virtual關鍵字修飾的成員函式,就是虛函式。虛函式的作用,用專業術語來解釋就是實現多態性(Polymorphism),多態性是將接口與實現進行分離;用形象的語言來解釋就是實現以共同的方法,但因個體差異,而採用不同的策略。下面來看一段簡單的代碼。
#include<iostream>using namespace std;class A{    public:        void print()        {            cout<<"This is A"<<endl;        }};class B : public A{    public:        void print()        {            cout<<"This is B"<<endl;        }};int main(){    //為了在以後便於區分,我這段main()代碼叫做main1    A a;    B b;    a.print();    b.print();    return 0;}

輸出結果

分別是“ThisisA”、“ThisisB”。
通過class A和class B的print()這個接口,可以看出這兩個class因個體的差異而採用了不同的策略,但這並不是多態性行為(使用的是不同類型的指針),沒有用到虛函式的功能。現在把main()處的代碼改一改。
int main(){    //main2    A a;    B b;    A *p1 = &a;    A *p2 = &b;    p1->print();    p2->print();    return 0;}
運行一下看看結果,結果卻是兩個This is A(錯)。
問題來了,p2明明指向的是class B的對象但卻是調用的class A的print()函式,這不是我們所期望的結果,那么解決這個問題就需要用到虛函式。
class A{    public:        virtual void print(){cout<<"This is A"<<endl;}};class B : public A{    public:    void print(){cout<<"This is B"<<endl;}};
毫無疑問,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{//虛函式示例代碼    public:        virtual void fun(){cout<<1<<endl;}        virtual void fun2(){cout<<2<<endl;}};class B : public A{    public:        void fun(){cout<<3<<endl;}        void fun2(){cout<<4<<endl;}};
由於這兩個類中有虛函式存在,所以編譯器就會為他們兩個分別插入一段你不知道的數據,並為他們分別創建一個表。那段數據叫做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的值即為第一個虛函式的地址(在不同的編譯器下第一個vtbl布局不完全相同,在VS2008中第一個Slot的值為指向第一個虛函式的指針,而其他編譯器中也可能出現第一個Slot中值為type_info對象的指針),這個值就是A::fun()的地址了,最後調用這個函式。現在我們可以看出來了,只要vptr不同,指向的vtbl就不同,而不同的vtbl里裝著對應類的虛函式地址,所以這樣虛函式就可以完成它的任務。
而對於class A和class B來說,他們的vptr指針存放在何處呢?其實這個指針就放在他們各自的實例對象里。由於class A和class B都沒有數據成員,所以他們的實例對象里就只有一個vptr指針。通過上面的分析,現在我們來實作一段代碼,來描述這個帶有虛函式的類的簡單模型。
#include<iostream>using namespace std;//將上面“虛函式示例代碼”添加在這裡int main(){    void(*fun)(A*);    A *p=new B;    long lVptrAddr;    memcpy(&lVptrAddr,p,4);    memcpy(&fun,reinterpret_cast<long*>(lVptrAddr),4);    fun(p);    delete p;    system("pause");    return 0;}
用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<long*>(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<long*>(lVptrAddr+4),4); 為什麼是加4呢?因為一個指針的長度是4bytes,所以加4。或者memcpy(&fun,reinterpret_cast<long*>(lVptrAddr)+1,4); 這更符合數組的用法,因為lVptrAddr被轉成了long*型別,所以+1就是往後移sizeof(long)的長度。

代碼示例

#include<iostream>using namespace std;class A{//虛函式示例代碼2    public:        virtual void fun(){cout<<"A::fun"<<endl;}        virtual void fun2(){cout<<"A::fun2"<<endl;}};class B : public A{    public:        void fun(){cout<<"B::fun"<<endl;}        void fun2(){cout<<"B::fun2"<<endl;}};//end//虛函式示例代碼2int main(){    void(A::*fun)();//定義一個函式指針    A *p=new B;    fun=&A::fun;    (p->*fun)();    fun=&A::fun2;    (p->*fun)();    delete p;    system("pause");    return 0;}
誤區
你能估算出結果嗎?如果你估算出的結果是A::fun和A::fun2,呵呵,恭喜恭喜,你中圈套了。其實真正的結果是B::fun和B::fun2,如果你想不通就接著往下看。給個提示,&A::fun和&A::fun2是真正獲得了虛函式的地址嗎?
首先我們回到第二部分,通過段實作代碼,得到一個“通用”的獲得虛函式地址的方法。
#include<iostream>using namespace std;//將上面“虛函式示例代碼2”添加在這裡void CallVirtualFun(void*pThis,intindex=0){    void(*funptr)(void*);    long lVptrAddr;    memcpy(&lVptrAddr,pThis,4);    memcpy(&funptr,reinterpret_cast<long*>(lVptrAddr)+index,4);    funptr(pThis);//調用}int main(){    A *p = new B;    CallVirtualFun(p);//調用虛函式p->fun()    CallVirtualFun(p,1);//調用虛函式p->fun2()    system("pause");    return 0;}
CallVirtualFun
現在我們擁有一個“通用”的CallVirtualFun方法。
這個通用方法和第三部分開始處的代碼有何聯繫呢?聯繫很大。由於A::fun()和A::fun2()是虛函式,所以&A::fun和&A::fun2獲得的不是函式的地址,而是一段間接獲得虛函式地址的一段代碼的地址,我們形象地把這段代碼看作那段CallVirtualFun。編譯器在編譯時,會提供類似於CallVirtualFun這樣的代碼,當你調用虛函式時,其實就是先調用的那段類似CallVirtualFun的代碼,通過這段代碼,獲得虛函式地址後,最後調用虛函式,這樣就真正保證了多態性。同時大家都說虛函式的效率低,其原因就是,在調用虛函式之前,還調用了獲得虛函式地址的代碼。
其他信息
定義虛函式的限制:(1)非類的成員函式不能定義為虛函式,類的成員函式中靜態成員函式和構造函式也不能定義為虛函式,但可以將析構函式定義為虛函式。實際上,優秀的程式設計師常常把基類的析構函式定義為虛函式。因為,將基類的析構函式定義為虛函式後,當利用delete刪除一個指向派生類定義的對象指針時,系統會調用相應的類的析構函式。而不將析構函式定義為虛函式時,只調用基類的析構函式。
(2)只需要在聲明函式的類體中使用關鍵字“virtual”將函式聲明為虛函式,而定義函式時不需要使用關鍵字“virtual”。
(3)當將基類中的某一成員函式聲明為虛函式後,派生類中的同名函式(函式名相同、參數列表完全一致、返回值類型相關)自動成為虛函式。
(4)如果聲明了某個成員函式為虛函式,則在該類中不能出現和這個成員函式同名並且返回值、參數個數、類型都相同的非虛函式。在以該類為基類派生類中,也不能出現和這個成員函式同名並且返回值、參數個數、類型都相同的非虛函式。
虛函式聯繫到多態,多態聯繫到繼承。所以本文中都是在繼承層次上做文章。沒了繼承,什麼都沒得談。
最後說明
本文的代碼可以用VC6和Dev-C++4.9.8.0通過編譯,且運行無問題。其他的編譯器不敢保證。其中的類比方法只能看成模型,因為不同的編譯器的底層實現是不同的。例如this指針,Dev-C++的gcc就是通過壓棧,當作參數傳遞,而VC的編譯器則通過取出地址保存在ecx中。所以這些類比方法不能當作具體實現。

相關詞條

熱門詞條

聯絡我們