■ 如果你期望衍生類別重新定義一個成員函數,那麼你應該在基礎類別中把此函數設爲virtual。
■ 以單一指令喚起不同函數,這種性質稱爲Polymorphism,意思是"the ability toassume many forms",也就是多態。
■ 虛擬函數是C++ 語言的Polymorphism 性質以及動態綁定的關鍵。
■ 既然抽象類別中的虛擬函數不打算被調用,我們就不應該定義它,應該把它設爲純虛擬函數(在函數聲明之後加上"=0" 即可)。
■ 我們可以說,擁有純虛擬函數者爲抽象類別(abstract Class),以別於所謂的具象類別(concrete class)。
■ 抽象類別不能產生出對象實體,但是我們可以擁有指向抽象類別之指針,以便於操作抽象類別的各個衍生類別。
■ 虛擬函數衍生下去仍爲虛擬函數,而且可以省略virtual 關鍵詞。
虛函數聯繫到多態,多態聯繫到繼承.所以本文中都是在繼承層次上做文章.沒了繼承,什麼都沒得談. 下面是對C++的虛函數這玩意兒的理解. 一.什麼是虛函數(如果不知道虛函數爲何物,但有急切的想知道,那你就應該從這裏開始) 簡單地說,那些被virtual關鍵字修飾的成員函數,就是虛函數.虛函數的作用,用專業術語來解釋就是實現多態性(Polymorphism),多態性是將接口與實現進行分離;用形象的語言來解釋就是實現以共同的方法,但因個體差異而採用不同的策略.下面來看一段簡單的代碼: 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(); } 通過class A和class B的print()這個接口,可以看出這兩個class因個體的差異而採用了不同的策略,輸出的結果也是我們預料中的,分別是This is A和This is B.但這是否真正做到了多態性呢? No,多態還有個關鍵之處就是一切用指向基類的指針或引用來操作對象.那現在就把main()處的代碼改一改. int main() //main2 { A a; B b; A* p1=&a; A* p2=&b; p1->print(); p2->print(); } 運行一下看看結果,喲呵,驀然回首,結果卻是兩個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;} //這裏需要在前面加上關鍵字virtual嗎? }; 毫無疑問,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裏的值,這個值就是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"); } 用VC或Dev-C++編譯運行一下,看看結果是不是輸出3,如果不是,那麼太陽明天肯定是從西邊出來.現在一步一步開始分析: void (*fun)(A*);這段定義了一個函數指針名字叫做fun,而且有一個A*類型的參數,這個函數指針待會兒用來保存從vtbl裏取出的函數地址; A* p=new B;這個我不太瞭解,算了,不解釋這個了; 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;和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虛函數示例代碼2 int main() { void (A::*fun)(); //定義一個函數指針 A *p=new B; fun=&A::fun; (p->*fun)(); fun = &A::fun2; (p->*fun)(); delete p; system("pause"); } 你能估算出輸出結果嗎?如果你估算出的結果是A::fun和A::fun2,呵呵,恭喜恭喜,你中圈套了.其實真正的結果是B::fun和B::fun2,如果你想不通就接着往下看.給個提示,&A::fun和&A::fun2是真正獲得了虛函數的地址嗎? 首先我們回到第二部分,通過段實作代碼,得到一個"通用"的獲得虛函數地址的方法. #include<iostream> using namespace std; //將上面"虛函數示例代碼2"添加在這裏 void CallVirtualFun(void* pThis,int index=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"); } 現在我們擁有一個"通用"的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中.所以這些類比方法不能當作具體實現
|