C++父子類繼承時的隱藏、覆蓋、重載

存在父子類繼承關係時,若有同名成員函數同時存在,會發生隱藏、覆蓋和重載這幾種情況。對於初學者也比較容易混淆,爲此,我整理了一下我的個人看法,僅供參考。希望對大家理解有幫助,也歡迎指正。

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中的相同語句,這就是虛函數和多態的意義。



轉載地址:

http://www.cnblogs.com/Lalafengchui/p/3994340.html