定義
簡單地說,那些被virtual關鍵字修飾的成員函式,就是虛函式。虛函式的作用,用專業術語來解釋就是實現多態性(Polymorphism),多態性是將接口與實現進行分離;用形象的語言來解釋就是實現以共同的方法,但因個體差異,而採用不同的策略。下面來看一段簡單的代碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | #include<iostream> using namespace std; class A { public : void print() { cout<< "ThisisA" <<endl; } }; class B: public A { public : void print() { cout<< "ThisisB" <<endl; } }; int main() { //為了在以後便於區分,這段main()代碼叫做main1 Aa; Bb; a.print(); b.print(); return 0; } |
輸出結果
分別是“ThisisA”、“ThisisB”。通過class A和class B的print()這個接口,可以看出這兩個class因個體的差異而採用了不同的策略,但這是否真正做到了多態性呢?No,多態還有個關鍵之處就是一切用指向基類的指針或引用來操作對象。那現在就把main()處的代碼改一改。1 2 3 4 5 6 7 8 9 10 11 | int main() { //main2 Aa; Bb; A*p1=&a; A*p2=&b; p1->print(); p2->print(); return 0; } |
1 2 3 4 5 6 7 8 9 10 11 | class A { public : virtual void print(){cout<< "ThisisA" <<endl;} }; class B: public A { public : void print(){cout<< "ThisisB" <<endl;} }; |
實現
虛函式是如何做到因對象的不同而調用其相應的函式的呢?現在就來剖析虛函式。先定義兩個類1 2 3 4 5 6 7 8 9 10 | 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;} }; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #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; } |
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)的長度。
代碼示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | #include<iostream> using namespacestd; 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//虛函式示例代碼2 int main(){ void (A::*fun)(); //定義一個函式指針 A*p= new B; fun=&A::fun; (p->*fun)(); fun=&A::fun2; (p->*fun)(); delete p; system ( "pause" ); return 0; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | #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方法。這個通用方法和第三部分開始處的代碼聯繫很大。由於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中。所以這些類比方法不能當作具體實現。
作用
虛函式的作用是實現動態聯編,也就是在程式的運行階段動態地選擇合適的成員函式,在定義了虛函式後,可以在基類的派生類中對虛函式重新定義,在派生類中重新定義的函式應與虛函式具有相同的形參個數和形參類型。以實現統一的接口,不同定義過程。如果在派生類中沒有對虛函式重新定義,則它繼承其基類的虛函式。
當程式發現虛函式名前的關鍵字virtual後,會自動將其作為動態聯編處理,即在程式運行時動態地選擇合適的成員函式。虛函式是C++多態的一種表現。
例如:子類繼承了父類的一個函式(方法),而把父類的指針指向子類,則必須把父類的該函式(方法)設為virtual(虛函式)。
([2010.10.28]註:下行語義容易使人產生理解上的偏差,實際效果應為:
如存在:Base->Derive1->Derive2及它們所擁有的虛函式func()
則在訪問派生類Derive1的實例時,使用其基類Base及本身類型Derive1,或被靜態轉換的後續派生類Derive2的指針或引用,均可訪問到Derive1所實現的func()。)
動態聯編規定,只能通過指向基類的指針或基類對象的引用來調用虛函式,其格式:
1、指向基類的指針變數名->虛函式名(實參表)
2、基類對象的引用名.虛函式名(實參表)
使用虛函式,可以靈活的進行動態綁定,當然是以一定的開銷為代價。如果父類的函式(方法)根本沒有必要或者無法實現,完全要依賴子類去實現的話,可以把此函式(方法)設為virtual函式名=0 把這樣的函式(方法)稱為純虛函式。
如果一個類包含了純虛函式,稱此類為抽象類。
示例
實例
#include<iostream>
usingnamespacestd;
classCshape
{
public:
voidSetColor(intcolor){m_nColor=color;}
virtualvoidDisplay(void){cout<<"Cshape"<<endl;}
private:
intm_nColor;
};
classCrectangle:publicCshape{
public:
virtualvoidDisplay(void)
{
cout<<"Crectangle"<<endl;
}
};
classCtriangle:publicCshape{
virtualvoidDisplay(void)
{cout<<"Ctriangle"<<endl;}
};
classCellipse:publicCshape{
public:
virtualvoidDisplay(void)
{cout<<"Cellipse"<<endl;}
};
voidmain(){
CshapeobShape;
CellipseobEllipse;
CtriangleobTriangle;
CrectangleobRectangle;
Cshape*pShape[4]={&obShape,&obEllipse,&obTriangle,&obRectangle};
for(inti=0;i<4;i++)
pShape[i]->Display();
本程式運行結果:
Cshape
Cellipse
Ctriangle
Crectangle
如果把Cshape類裡面virtualvoidDisplay(void)中的virtual去掉的話
運行結果就不一樣了:
Cshape
Cshape
Cshape
Cshape
條件
所以,從以上程式分析,實現動態聯編需要三個條件:
1、必須把動態聯編的行為定義為類的虛函式。
2、類之間存在子類型關係,一般表現為一個類從另一個類公有派生而來。
3、必須先使用基類指針指向子類型的對象,然後直接或者間接使用基類指針調用虛函式。
c++擴展
下面是對C++的虛函式的理解。
簡單地說,那些被virtual關鍵字修飾的成員函式,就是虛函式。虛函式的作用,用專業術語來解釋就是實現多態性(Polymorphism),多態性是將接口與實現進行分離;用形象的語言來解釋就是實現以共同的方法,但因個體差異而採用不同的策略。下面來看一段簡單的代碼。
classA
{
public:
voidprint(){cout<<"ThisisA"<<endl;}
};
classB:publicA{
public:
voidprint(){cout<<"ThisisB"<<endl;}
};
intmain()
{
//為了在以後便於區分,這段main()代碼叫做main1
Aa;
Bb;
a.print();
b.print();
}
通過classA和classB的print()這個接口,可以看出這兩個class因個體的差異而採用了不同的策略,但這是否真正做到了多態性呢?No,多態還有個關鍵之處就是一切用指向基類的指針或引用來操作對象。那現在就把main()處的代碼改一改。
intmain(){//main2
Aa;
Bb;
A*p1=&a;
A*p2=&b;
p1->print();
p2->print();
}
運行一下看看結果,結果卻是兩個ThisisA。問題來了,p2明明指向的是classB的對象但卻是調用的classA的print()函式,這不是所期望的結果,那么解決這個問題就需要用到虛函式
classA{
public:
virtualvoidprint(){cout<<"ThisisA"<<endl;}//現在成了虛函式了
};
classB:publicA{
public:
voidprint(){cout<<"ThisisB"<<endl;}//這裡需要在前面加上關鍵字virtual嗎?
};
毫無疑問,classA的成員函式print()已經成了虛函式,那么classB的print()成了虛函式了嗎?回答是Yes,只需在把基類的成員函式設為virtual,其派生類的相應的函式也會自動變為虛函式。所以,classB的print()也成了虛函式。那么對於在派生類的相應函式前是否需要用virtual關鍵字修飾,那就是你自己的問題了。
現在重新運行main2的代碼,這樣輸出的結果就是ThisisA和ThisisB了。指向基類的指針在操作它的多態類對象時,會根據不同的類對象,調用其相應的函式,這個函式就是虛函式。