概要:
C++中經常出現函數名字一樣,但參數列表或返回值不同的函數,要搞清楚函數的正確調用關系,需理清三個概念:重寫(override)、重載(overload)、重定義(redefine)。
一、三個基本概念
1、重定義(redefine):派生類對基類的成員函數重新定義,即派生類定義了某個函數,該函數的名字與基類中的函數名字一樣。
特點:(1)不在同一個作用域(分別位於基類、派生類) (2)函數的名字必須相同 (3)對函數的返回值、形參列表無要求
特殊情況:若派生類定義的該函數與基類的成員函數完全一樣(返回值、形參列表均相同),且基類的該函數為virtual,則屬於派生類重寫基類的虛函數。
作用效果:若重新定義了基類中的一個重載函數,則在派生類中,基類中該名字的函數(即其他所有重載版本)都被自動隱藏,包括同名的虛函數。
2、重載(overload):函數名字相同,但它的形參個數或者順序,或者類型不同,但是不能靠返回類型來判斷。
特點:(1)位於同一個類中 (2)函數的名字必須相同 (3)形參列表不同(可能是參數個數 or 類型 or 順序 不同),返回值無要求
特殊情況:若某一個重載版本的函數前面有virtual修飾,則表示它是虛函數。但它也是屬於重載的一個版本
不同的構造函數(無參構造、有參構造、拷貝構造)是重載的應用
作用效果和原理:編譯器根據函數不同的參數表,將函數體與函數調用進行早綁定。重載與多態無關,只是一種語言特性,與面向對象無關。
3、重寫(override):派生類重定義基類的虛函數,即會覆蓋基類的虛函數 (多態性)
特點:(1)不在同一個作用域(分別位於基類、派生類) (2)函數名、形參列表、返回值相同 (3)基類的函數是virtual
特殊情況:若派生類重寫的虛函數屬於一個重載版本,則該重寫的函數會隱藏基類中與虛函數同名的其他函數。
作用效果:父類的指針或引用根據傳遞給它的子類地址或引用,動態地調用屬於子類的該函數。這個晚綁定過程只對virtual函數起作用
具體原理是由虛函數表(VTABLE)決定的,在第三節介紹。
二、程序實例
1、兩個類:基類( 取名Test)和派生類( 取名XX) 名字不規范,哈哈隨便取得!
基類和派生類的結構
//Base class class Test { public: int a; Test() { cout<<"Test() 無參構造函數!"<<endl; } Test(int data) { a = data; cout<<"Test(int data) 有參構造函數!"<<endl; } Test(const Test &tmp) { a = tmp.a; cout<<"Test 拷貝構造函數!!"<<endl; } //基類中對函數名f,進行了重載。其中最后一個重載函數為虛函數 void f()const { cout<<"調用 void Test::f()"<<endl; } //overload int f(int data) const { cout<<"調用 Test f(int data)"<<endl; return 1; } //overload 虛函數 virtual double f(int dataA,int dataB) { cout<<"調用 Test f(int a,int b)"<<endl; return dataA*dataB/2.0; } }; class XX: public Test { public: Test atest;//先調用基類的構造函數,然后對象成員的構造函數,最后才是派生類的構造函數 XX() { cout<<"XX() 無參構造函數被調用!"<<endl; } //對基類的函數名f,進行了重定義。則會隱藏基類中的其他f函數 //redefine int f() const { cout<<" 調用 XX f()函數"<<endl; return 1; } //重寫基類的虛函數 //redefine override double f(int dataA,int dataB) { cout<<"調用 XX f(int dataA,int dataB)函數"<<endl; return (dataA+dataB)/2.0; } };
分析:基類class Test中定義了名為f的3個重載函數,其中最后一個是虛函數
派生類class XX中對f進行了重定義,所以會隱藏基類中名為f的版本。其中派生類的double f(int dataA,int dataB)屬於對虛函數的重寫
測試---主程序
int main() { //-----test 1------------------------ cout<<"-------test 1------------"<<endl; //Base class Test aaTest; aaTest.f(); aaTest.f(12); aaTest.f(10,20); //derived class XX d; d.f(); // d.f(2); //error C2661: 'f' : no overloaded function takes 1 parameters d.f(10,20); //--------test 2---------------------------------- cout<<"-------test 2------------"<<endl; Test b = d; b.f(); b.f(10,20);//調用的是基類的函數,不發生多態 //--------test 3---------------------------------------- cout<<"-------test 3------------"<<endl; Test &bR = d;//引用 b.f();//f()不是虛函數,調用基類的函數 bR.f(10,20);//調用的是派生類的函數,發生多態 //--------test 4-------------------------------------- cout<<"-------test 4------------"<<endl; Test* pB = &d; b.f(); pB->f(10,20);//調用的是派生類的函數,發生多態 return 1; }
分析:(1)test 1中進行了重載測試,根據傳遞參數的不一樣,調用不同的函數 (早綁定,與多態無關)
(2)test 2中Test b = d;定義了一個基類對象,用派生類對象來進行初始化。這會調用基類的拷貝構造函數,生成基類的對象b,基類的拷貝構造函數初始化b的VPTR,指向b的VTABLE。因此所有的函數調用都只發生在基類,不會產生多態。
這是一個對象切片過程(參見《C++編程思想.第二版》P370),對象切片是當它拷貝到一個新的對象時,會去掉原來對象的一部分,而不是像使用指針或引用那樣簡單地改變地址的內容。
(3)test 3和test 4中,定義的基類指針和引用,故會發生多態。
三、晚綁定原理:虛函數表
編譯器會對每一個包含虛函數的類(或者從包含虛函數的基類派生的類)創建一個表(VTABLE),里面存放特定類的虛函數的地址。然后編譯器秘密地放置一指針vpointer(VPTR),指向這個對象的vtable。當通過基類指針做虛函數調用時(即多態調用時),編譯器靜態地插入能取得這個VPTR並在VTABLE表中查找函數地址的代碼,這樣就能調用正確的函數並引起晚綁定的發生。
這個具體過程, 可以參見本人的另一篇博客《C模擬實現C++的多態》http://www.cnblogs.com/wly603/archive/2012/04/11/2441912.html
(完)
