在C++語言中,虛函數是非常重要的概念,虛函數是實現C++面向對象中多態性和繼承性的基石。而多態性和繼承性則是面向對象語言的精髓。掌握虛函數才算是真正掌握C++語言,而C++語言中虛函數的繼承覆蓋與函數重載有些類似,很多初學者搞不清他們之間的關系。
首先要明確覆蓋(override)與重載(overload)的定義,區別出什么是覆蓋和重載:
覆蓋就是派生類中虛成員函數覆蓋基類中同名且參數相同的成員函數。
如下例:
上例中,CDerived和CBase之間就是函數覆蓋關系,Walk和Jump函數被派生類覆蓋,輸出肯定是派生類函數的輸出。下面就這幾者關系着重學習。再看看下例:
class CBase { public: CBase(); virtual void Walk(){cout<<"CBase:Walk"<<endl;} virtual void Jump(){cout<<"CBase:Jump"<<endl;} void Run(int speed){cout<<"CBase:Run:"<<"Speed="<<speed<<endl;} }; class CDerivedA : public CBase { public: CDerivedA(); void Walk(){cout<<"CDerivedA:Walk"<<endl;} void Jump(){cout<<"CDerivedA:Jump"<<endl;} void Run(int speed) {cout<<"CDerivedA:Run"<<"Speed="<<speed<<endl;} }; int main() { CBase *pTmp1=new CDerivedA ; pTmp1->Walk(); pTmp1->Run(20); return 0; }
這里基類中Run與派生類中Run函數就是重載(overload)關系,當使用基類來調用派生類對象中重載函數,會根據其作用域來判斷,如基類CBase,其作用域就是CBase這個類的大括號包含的內容,派生類CDeriverdA其作用域就是CDerivedA大括號中包含的內容。因此在使用基類調用派生類對象中的Run方法時,會執行基類的Run方法。
關於作用域可見性規則(摘於網上:http://www.51laifu.cn/archives/2012/610/article-31092.html)如下:
如果存在兩個或多個具有包含關系的作用域,外層聲明了一個標識符,而內層沒有再次聲明同名標識符,那么外層標識符在內層依然可見,如果在內層聲明了同名標識符,則外層標識符在內層不可見,這時稱內層標識符隱藏了外層同名標識符,這種現象稱為隱藏規則。
在類的派生層次結構中,基類的成員和派生類新增的成員都具有類作用域。二者的作用范圍不同,是相互包含的兩個層,派生類在內層。這時,如果派生類聲明了一個和某個基類成員同名的新成員,派生的新成員就隱藏了外層同名成員,直接使用成員名只能訪問到派生類的成員。如果派生類中聲明了與基類同名的新函數,即使函數的參數表不同,從基類繼承的同名函數的所有重載形式也都被隱藏。如果要訪問被隱藏的成員,就需要使用類作用域分辨符和基類名來限定。
根據上面的解釋,CBase類處於作用域的外層,因此派生類的方法對於其將是不可見的,即隱藏的。而在派生類中,如果有重載函數時,基類函數將會被隱藏,否則基類函數就不被隱藏。
函數重載和虛函數都能實現對象的多態特性,但是函數重載只能實現靜態的多態,也就是在編譯時多態,即編譯器會給重載的函數在編譯時加上不同的前綴,是函數名在運行時不同。而虛函數可以實現動態多態,即運行時多態,但是虛函數實現需要增加額外的虛函數指針等,會增加相應空間。
再看下例:
以上代碼就可以看出,派生類中的重載方法,在基類是不能調用的,編譯的時候就會出錯。