存在父子類繼承關系時,若有同名成員函數同時存在,會發生隱藏、覆蓋和重載這幾種情況。對於初學者也比較容易混淆,為此,我整理了一下我的個人看法,僅供參考。希望對大家理解有幫助,也歡迎指正。
1.父子類繼承關系: 子類復制父類全部成員
首先,理解父子類的繼承關系是怎樣發生的。在此基礎上就很容易理解它們之間的關系和區別。
每一個類有它自己的成員變量和成員函數,是一個獨立的空間整體。當子類繼承父類時,會將父類的全部成員全部復制一份,作為子類的成員,但是,同時也會標記這些成員是從父類中繼承的,與子類本身的成員,還是有區別的。這里認為將子類本身的成員存在子類域,從父類復制過來的存在父類域。
如下圖,Childer類中存在兩個域,子類域和父類域,相互之間互不干擾。
1 class Father 2 { 3 int f_a; 4 int f_b; 5 }; 6 7 class Childer:public Father 8 { 9 int c_a; 10 int f_b; 11 }; 12 13 int main() 14 { 15 cout<<"sizeof childer:"<<sizeof(Childer)<<endl; //-> 16 16 cout<<"sizeof father:"<<sizeof(Father)<<endl; //-> 8 17 }
運行結果顯示,子類大小為16,父類大小為8,也就是說子類的確有4個成員變量,就算是同名成員,也同樣復制。
2.隱藏:子類對象優先考慮子類域自身成員(成員變量和成員函數)
隱藏發生的主要原因,就是當子類有父類的同名成員時,子類對象訪問該成員時,會發生沖突。所以編譯器的處理方式是,優先考慮子類域中的自身成員。
即,子類對象訪問某成員時,如ch.m_m 或者ch.f(),成員變量和成員函數都一樣。編譯器首先在子類域中檢索,如果在子類域中找到該成員,則檢索結束,返回該成員進行訪問。如果在子類域中找不到該成員,則去父類域中檢索。如果父類域中存在,則返回該成員進行訪問,如果父類域中也不存在,則編譯錯誤,該成員無效。
當父子類域都存在同一成員時,編譯器優先在子類中檢索,就算父類域中也存在該同名成員,也不會被檢索到。因此,父類域中的該成員被子類域中的該同名成員隱藏,即訪問時完全以為該成員不存在,如果想訪問父類域中的該成員,只能通過顯示調用的方式,即:ch.Father::m_m;
下面用代碼說明,為了對問題有針對性說明,此處成員都采用public,也不涉及構造析構等問題。
1 class Father 2 { 3 public: 4 int f_a; 5 int f_b; 6 7 void ff1() {cout<<"father ff1"<<endl;} 8 }; 9 10 class Childer:public Father 11 { 12 public: 13 int c_a; 14 int f_b; 15 16 void cf1() {cout<<"childer cf1"<<endl;} 17 void ff1() {cout<<"childer ff1"<<endl;} 18 }; 19 20 int main() 21 { 22 Childer ch; 23 24 cout<<ch.c_a<<endl; //只在子類域中的成員變量 25 cout<<ch.f_b<<endl; //子類域和父類域都存在,優先訪問子類域中的 26 cout<<ch.Father::f_b<<endl; //顯示訪問被隱藏的成員變量 27 28 cout<<"====================\n"; 29 30 ch.cf1(); 31 ch.ff1(); 32 ch.Father::ff1(); 33 }
運行結果可以看出,ch.f_b; 和 ch.Father::f_b; 兩個同名成員同時存在。但訪問時,子類成員將父類成員隱藏,想訪問父類成員只能顯示調用。
通過成員函數的訪問,這一效果更明顯,ch.ff1();調用時,調用了子類域中的該同名成員函數。
且此時編譯器檢索時,只根據名字,與函數的參數和返回類型無關。
1 int ff1(int a ) {cout<<"childer ff1"<<endl;return 0;}
若將Childer中的函數,改為上述類型。主函數中調用時,ch.ff1();編譯錯誤。因為子類的int ff1(int a);會將父類的void ff1();隱藏。所以它們之間不存在重載。
應該改為 ch.ff1(10); 這樣會匹配子類域中的該成員。或者ch.Father::ff1();顯示調用父類域中的成員。
3.覆蓋:虛函數,成員函數類型一摸一樣,父類指針調用子類對象成員
覆蓋只發生在有虛函數的情況下,且父子類成員函數類型必須一摸一樣,即參數和返回類型都必須一致。子類對象調用時,會直接調用子類域中的成員函數,父類域中的該同名成員就像不存在一樣,(可以顯示調用)即父類該成員被子類成員覆蓋。這里很多人會感覺疑惑,認為是隱藏,因為父類的成員函數依然存在,依然可以調用,只是優先調用子類的,也就是“隱藏”了。而“覆蓋”兩個字的意思,應該是一個將另一個替代了,也就是另一個不存在了。
舉個小例子可以很明顯的看出,覆蓋的情況下,父子類的成員函數也是同時存在的。
virtual void ff1() {cout<<"father ff1"<<endl; }
將上面的例子Father類中的ff1函數加上virtual,其他不進行改變,運行結果也不變。
下面解釋一下,“覆蓋”二字的由來。
首先需明白一點,虛函數的提出,是為了實現多態。也就是說,虛函數的目的是為了,在用父類指針指向不同的子類對象時,調用虛函數,調用的是對應子類對象的成員函數,即可以自動識別具體子類對象。所以,上述例子中,直接用子類對象調用虛函數是沒有意義的,一般情況也不會這樣使用。
1 class Father 2 { 3 public: 4 virtual void ff1() {cout<<"father ff1"<<endl;} 5 }; 6 7 class Childer_1:public Father 8 { 9 public: 10 void ff1() {cout<<"childer_1 ff1 "<<endl;} 11 }; 12 class Childer_2:public Father 13 { 14 public: 15 void ff1() {cout<<"childer_2 ff1"<<endl; } 16 }; 17 18 int main() 19 { 20 Father* fp; 21 22 Childer_1 ch1; 23 fp = &ch1; 24 fp->ff1(); 25 26 Childer_2 ch2; 27 fp = &ch2; 28 fp->ff1(); 29 30 return 0; 31 }
使用虛函數,都是父類指針的形式,pf->f11() 。例子中的24行和28行,相同的代碼,因為fp的指向不同對象,所以調用不同對象的虛函數。但從代碼上看,fp是一個Father類的指針,但調用的是子類成員函數,就好像父類的成員被覆蓋了一樣。這就是覆蓋一詞的來源。
覆蓋的情況下,子類虛函數必須與父類虛函數有相同的參數列表,否則認為是一個新的函數,與父類的該同名函數沒有關系。但不可以認為兩個函數構成重載。因為兩個函數在不同的域中。
舉例:
1 class Father 2 { 3 public: 4 virtual void ff1() {cout<<"father ff1"<<endl;} 5 }; 6 7 class Childer_1:public Father 8 { 9 public: 10 void ff1(int a) {cout<<"childer_1 ff1 "<<endl; } 11 }; 12 13 int main() 14 { 15 Father* fp; 16 17 Childer_1 ch1; 18 fp = &ch1; 19 fp->ff1(); 20 //ch1.ff1(); //沒有匹配的成員 21 ch1.ff1(2); 22 23 return 0; 24 }
運行結果為:
father ff1
childer_1 ff1
從19行 fp->ff1();的運行結果可以看出,fp雖然指向子類對象,並且調用的是虛函數。但是該虛函數,在子類中沒有對應的實現,只好使用父類的該成員。
即第10行的帶參ff1 並沒有覆蓋從父類中繼承的無參ff1. 而是認為是一個新函數。
4.重載:相同域的同名不同參函數
重載必須是發生在同一個域中的兩個同名不同形參之間的。如果一個在父類域一個在子類域,是不會存在重載的,屬於隱藏的情況。調用時,只會在子類域中搜索,如果形參不符合,會認為沒有該函數,而不會去父類域中搜索。
5.總結
重載是在同一域下的函數關系,在父子類情況下時,一般不予考慮。
隱藏,是子類改寫、重寫了父類的代碼。而覆蓋認為,子類實現了父類的虛函數。父類的虛函數可以沒有實現體,成為純虛函數,等着子類去實現。而隱藏時,父類的函數也必須有實現體的。隱藏還是覆蓋,只是說法不同,只要明白編譯器在調用時,如果檢索、匹配相應的函數即可。
綜上所述,總結為以下幾點:
1.子類是將父類的所有成員都復制一份,並且保存在不同的域中。如果同名,子類中會有兩份,分別在子類域和父類域。
2.調用時,是從調用對象(或指針)的類型開始檢索的,先從自己域中檢索,如果找到,判斷是否為虛函數,不為虛函數直接調用,若為虛函數,通過運行時類型識別,調用真正對象的函數。如果沒找到,去其父類域中檢索,重復剛剛的判斷。直到調用函數或者沒有匹配的成員。而不會去子類中檢索,所以如果是父類指針,即使指向子類對象,但調用的函數也只能是父類中的函數,除非是虛函數,才會根據子類對象去檢索函數。
明白調用過程:
2.1 一般情況下,哪種類型的,就調哪種類型對於自己域中的成員。
Father f; f.a; f.ff1(); 由於f是Father類型的,所以調用的都是Father自己域中的成員。
Childer c; c.a; c.ff1(); 由於c是Chiler類型的,所以調用的都是Childer自己域中的成員。
指針也一樣。Father*fp; fp->a; fp->ff1(); 由於fp是Father類型的指針,所以調用的都是Father自己域中的成員。
就算fp = new Childer. fp->ff1(); 指向的是子類對象,依然調用父類自己的成員。因為fp是Father類型的。
Childer *cp; cp->a; cp->ff1(); 由於cp是Childer類型的指針,所以調用的都是Childer自己域中的成員。
2.2 .而有一種情況特殊,則是,當成員函數為虛函數時,雖然是父類類型的指針,但會根據指針指向的具體對象,調用該函數。
即,如果ff1為虛函數,Father*fp; fp = new Childer; fp->ff1(); 雖然fp是Father類型的指針,但由於ff1是虛函數,所以調用的是具體對象,Childer類的成員。
對比2中的相同語句,這就是虛函數和多態的意義。