1. 程式人生 > >C++多型及過載(overload),覆蓋(override),隱藏(hide)的區別

C++多型及過載(overload),覆蓋(override),隱藏(hide)的區別

C++程式語言是一款應用廣泛,支援多種程式設計的計算機程式語言。我們今天就會為大家詳細介紹其中C++多型性的一些基本知識,以方便大家在學習過程中對此能夠有一個充分的掌握。

  多型性可以簡單地概括為“一個介面,多種方法”,程式在執行時才決定呼叫的函式,它是面向物件程式設計領域的核心概念。多型(polymorphisn),字面意思多種形狀。
  C++多型性是通過虛擬函式來實現的,虛擬函式允許子類重新定義成員函式,而子類重新定義父類的做法稱為覆蓋(override),或者稱為重寫。(這裡我覺得要補充,重寫的話可以有兩種,直接重寫成員函式和重寫虛擬函式,只有重寫了虛擬函式的才能算作是體現了C++多型性)而過載則是允許有多個同名的函式,而這些函式的引數列表不同,允許引數個數不同,引數型別不同,或者兩者都不同。編譯器會根據這些函式的不同列表,將同名的函式的名稱做修飾,從而生成一些不同名稱的預處理函式,來實現同名函式呼叫時的過載問題。但這並沒有體現多型性。
  多型與非多型的實質區別就是函式地址是早繫結還是晚繫結。如果函式的呼叫,在編譯器編譯期間就可以確定函式的呼叫地址,並生產程式碼,是靜態的,就是說地址是早繫結的。而如果函式呼叫的地址不能在編譯器期間確定,需要在執行時才確定,這就屬於晚繫結。

  那麼多型的作用是什麼呢,封裝可以使得程式碼模組化,繼承可以擴充套件已存在的程式碼,他們的目的都是為了程式碼重用。而多型的目的則是為了介面重用。也就是說,不論傳遞過來的究竟是那個類的物件,函式都能夠通過同一個介面呼叫到適應各自物件的實現方法。

  最常見的用法就是宣告基類的指標,利用該指標指向任意一個子類物件,呼叫相應的虛擬函式,可以根據指向的子類的不同而實現不同的方法。如果沒有使用虛擬函式的話,即沒有利用C++多型性,則利用基類指標呼叫相應的函式的時候,將總被限制在基類函式本身,而無法呼叫到子類中被重寫過的函式。因為沒有多型性,函式呼叫的地址將是一定的,而固定的地址將始終呼叫到同一個函式,這就無法實現一個介面,多種方法的目的了。

筆試題目:

  1. #include<iostream>
  2. usingnamespace std;  
  3. class A  
  4. {  
  5. public:  
  6.     void foo()  
  7.     {  
  8.         printf("1\n");  
  9.     }  
  10.     virtualvoid fun()  
  11.     {  
  12.         printf("2\n");  
  13.     }  
  14. };  
  15. class B : public A  
  16. {  
  17. public:  
  18.     void foo()  
  19.     {  
  20.         printf("3\n");  
  21.     }  
  22.     void fun()  
  23.     {  
  24.         printf("4\n");  
  25.     }  
  26. };  
  27. int main(void)  
  28. {  
  29.     A a;  
  30.     B b;  
  31.     A *p = &a;  
  32.     p->foo();  
  33.     p->fun();  
  34.     p = &b;  
  35.     p->foo();  
  36.     p->fun();  
  37.     return 0;  
  38. }  
     第一個p->foo()和p->fuu()都很好理解,本身是基類指標,指向的又是基類物件,呼叫的都是基類本身的函式,因此輸出結果就是1、2。
    第二個輸出結果就是1、4。p->foo()和p->fuu()則是基類指標指向子類物件,正式體現多型的用法,p->foo()由於指標是個基類指標,指向是一個固定偏移量的函式,因此此時指向的就只能是基類的foo()函式的程式碼了,因此輸出的結果還是1。而p->fun()指標是基類指標,指向的fun是一個虛擬函式,由於每個虛擬函式都有一個虛擬函式列表,此時p呼叫fun()並不是直接呼叫函式,而是通過虛擬函式列表找到相應的函式的地址,因此根據指向的物件不同,函式地址也將不同,這裡將找到對應的子類的fun()函式的地址,因此輸出的結果也會是子類的結果4。
  筆試的題目中還有一個另類測試方法。即

       B *ptr = (B *)&a;  ptr->foo();  ptr->fun();
  問這兩呼叫的輸出結果。這是一個用子類的指標去指向一個強制轉換為子類地址的基類物件。結果,這兩句呼叫的輸出結果是3,2。
  並不是很理解這種用法,從原理上來解釋,由於B是子類指標,雖然被賦予了基類物件地址,但是ptr->foo()在呼叫的時候,由於地址偏移量固定,偏移量是子類物件的偏移量,於是即使在指向了一個基類物件的情況下,還是呼叫到了子類的函式,雖然可能從始到終都沒有子類物件的例項化出現。
  而ptr->fun()的呼叫,可能還是因為C++多型性的原因,由於指向的是一個基類物件,通過虛擬函式列表的引用,找到了基類中fun()函式的地址,因此呼叫了基類的函式。由此可見多型性的強大,可以適應各種變化,不論指標是基類的還是子類的,都能找到正確的實現方法。
  1. //小結:1、有virtual才可能發生多型現象
  2. // 2、不發生多型(無virtual)呼叫就按原型別呼叫
  3. #include<iostream>
  4. usingnamespace std;  
  5. class Base  
  6. {  
  7. public:  
  8.     virtualvoid f(float x)  
  9.     {  
  10.         cout<<"Base::f(float)"<< x <<endl;  
  11.     }  
  12.     void g(float x)  
  13.     {  
  14.         cout<<"Base::g(float)"<< x <<endl;  
  15.     }  
  16.     void h(float x)  
  17.     {  
  18.         cout<<"Base::h(float)"<< x <<endl;  
  19.     }  
  20. };  
  21. class Derived : public Base  
  22. {  
  23. public:  
  24.     virtualvoid f(float x)  
  25.     {  
  26.         cout<<"Derived::f(float)"<< x <<endl;   //多型、覆蓋
  27.     }  
  28.     void g(int x)  
  29.     {  
  30.         cout<<"Derived::g(int)"<< x <<endl;     //隱藏
  31.     }  
  32.     void h(float x)  
  33.     {  
  34.         cout<<"Derived::h(float)"<< x <<endl;   //隱藏
  35.     }  
  36. };  
  37. int main(void)  
  38. {  
  39.     Derived d;  
  40.     Base *pb = &d;  
  41.     Derived *pd = &d;  
  42.     // Good : behavior depends solely on type of the object
  43.     pb->f(3.14f);   // Derived::f(float) 3.14
  44.     pd->f(3.14f);   // Derived::f(float) 3.14
  45.     // Bad : behavior depends on type of the pointer
  46.     pb->g(3.14f);   // Base::g(float)  3.14
  47.     pd->g(3.14f);   // Derived::g(int) 3 
  48.     // Bad : behavior depends on type of the pointer
  49.     pb->h(3.14f);   // Base::h(float) 3.14
  50.     pd->h(3.14f);   // Derived::h(float) 3.14
  51.     return 0;  
  52. }  
令人迷惑的隱藏規則
本來僅僅區別過載與覆蓋並不算困難,但是C++的隱藏規則使問題複雜性陡然增加。
這裡“隱藏”是指派生類的函式遮蔽了與其同名的基類函式,規則如下:
(1)如果派生類的函式與基類的函式同名,但是引數不同。此時,不論有無virtual
關鍵字,基類的函式將被隱藏(注意別與過載混淆)。
(2)如果派生類的函式與基類的函式同名,並且引數也相同,但是基類函式沒有virtual
關鍵字。此時,基類的函式被隱藏(注意別與覆蓋混淆)。
上面的程式中:
(1)函式Derived::f(float)覆蓋了Base::f(float)。
(2)函式Derived::g(int)隱藏了Base::g(float),而不是過載。
(3)函式Derived::h(float)隱藏了Base::h(float),而不是覆蓋。

成員函式被過載的特徵
(1)相同的範圍(在同一個類中); 
(2)函式名字相同; 
(3)引數不同; 
(4)virtual 關鍵字可有可無。 
覆蓋是指派生類函式覆蓋基類函式,特徵是
(1)不同的範圍(分別位於派生類與基類); 
(2)函式名字相同; 
(3)引數相同; 
(4)基類函式必須有virtual 關鍵字。 
“隱藏”是指派生類的函式遮蔽了與其同名的基類函式,規則如下
(1)如果派生類的函式與基類的函式同名,但是引數不同。此時,不論有無virtual關鍵字,基類的函式將被隱藏(注意別與過載混淆)。 
(2)如果派生類的函式與基類的函式同名,並且引數也相同,但是基類函式沒有virtual 關鍵字。此時,基類的函式被隱藏(注意別與覆蓋混淆) 
3種情況怎麼執行: 1。過載:看引數 2。隱藏:用什麼就呼叫什麼 3。覆蓋:呼叫派生類
寫正題之前,先給出幾個關鍵字的中英文對照,過載(overload),覆蓋(override),隱藏(hide)。在早期的C++書籍中,可能翻譯的人不熟悉專業用語(也不能怪他們,他們不是搞計算機程式設計的,他們是英語專業的),常常把過載(overload)和覆蓋(override)搞錯!

  我們先來看一些程式碼及其編譯結果。

 例項一:

 
  #include "stdafx.h"
  #include <iostream.h>

  class CB
  {
  public:
     void f(int)
     {
        cout << "CB::f(int)" << endl;
     }
  };

  class CD : public CB
  {
  public:
     void f(int,int)
     {
        cout << "CD::f(int,int)" << endl;
     }
     void test()
     {
       f(1);
     }
  };

 int main(int argc, char* argv[])
 {
    return 0;
 }

編譯了一下
error C2660: 'f' : function does not take 1 parameters


結論:在類CD這個域中,沒有f(int)這樣的函式,基類中的void f(int)被隱藏

  如果把派生CD中成員函式void f(int,int)的宣告改成和基類中一樣,即f(int),基類中的void f(int)還是一樣被覆蓋,此時編譯不會出錯,在函式中test呼叫的是CD中的f(int) 

  所以,在基類中的某些函式,如果沒有virtral關鍵字,函式名是f(引數是什麼我們不管),那麼如果在派生類CD中也聲明瞭某個f成員函式,那麼在類CD域中,基類中所有的那些f都被隱藏。
  如果你比較心急,想知道什麼是隱藏,看文章最後的簡單說明,不過我建議你還是一步一步看下去。

  我們剛才說的是沒有virtual的情況,如果有virtual的情況呢??
  例項二:

#include "stdafx.h"
#include <iostream.h>

class CB
{
public:
   virtual void f(int)
   {
      cout << "CB::f(int)" << endl;
   }
};

class CD : public CB
{
public:
   void f(int)
   {
      cout << "CD::f(int)" << endl;
   }
};

int main(int argc, char* argv[])
{
  return 0;
}


  這麼寫當然是沒問題了,在這裡我不多費口舌了,這是很簡單的,多型,虛擬函式,然後什麼指向基類的指標指向派生類物件阿,通過引用呼叫虛擬函式阿什麼的,屬性多的很咯,什麼??你不明白??隨便找本C++的書,對會講多型和虛擬函式機制的哦!!
  這種情況我們叫覆蓋(override)!覆蓋指的是派生類的虛擬函式覆蓋了基類的同名且引數相同的函式!
  在這裡,我要強調的是,這種覆蓋,要滿足兩個條件
 (a)有virtual關鍵字,在基類中函式宣告的時候加上就可以了
 (b)基類CB中的函式和派生類CD中的函式要一模一樣,什麼叫一模一樣,函式名,引數,返回型別三個條件
  有人可能會對(b)中的說法質疑,說返回型別也要一樣??
  是,覆蓋的話必須一樣,我試了試,如果在基類中,把f的宣告改成virtual int f(int),編譯出錯了
  error C2555: 'CD::f' : overriding virtual function differs from 'CB::f' only by return type or calling convention
  所以,覆蓋的話,必須要滿足上述的(a)(b)條件

  那麼如果基類CB中的函式f有關鍵字virtual ,但是引數和派生類CD中的函式f引數不一樣呢,
例項三:
 

 #include "stdafx.h"
#include <iostream.h>

class CB
{
 public:
    virtual  void f(int)
   {
      cout << "CB::f(int)" << endl;
   }
};


class CD : public CB
{
public:
    void f(int,int)
   {
     cout << "CD::f(int,int)" << endl;
   }

   void test()
   {
      f(1);
   }
};

int main(int argc, char* argv[])
{
 return 0;
}

編譯出錯了,
 error C2660: 'f' : function does not take 1 parameters
  咦??好面熟的錯??對,和例項一中的情況一樣哦,結論也是基類中的函式被隱藏了。

  通過上面三個例子,得出一個簡單的結論
如果基類中的函式和派生類中的兩個名字一樣的函式f
滿足下面的兩個條件
(a)在基類中函式宣告的時候有virtual關鍵字
(b)基類CB中的函式和派生類CD中的函式一模一樣,函式名,引數,返回型別都一樣。
那麼這就是叫做覆蓋(override),這也就是虛擬函式,多型的性質

那麼其他的情況呢??只要名字一樣,不滿足上面覆蓋的條件,就是隱藏了。

下面我要講最關鍵的地方了,好多人認為,基類CB中的f(int)會繼承下來和CD中的f(int,int)在派生類CD中構成過載,就像例項一中想像的那樣。
  對嗎?我們先看過載的定義
  
過載(overload):
  必須在一個域中,函式名稱相同但是函式引數不同,過載的作用就是同一個函式有不同的行為,因此不是在一個域中的函式是無法構成過載的,這個是過載的重要特徵
  必須在一個域中,而繼承明顯是在兩個類中了哦,所以上面的想法是不成立的,我們測試的結構也是這樣,派生類中的f(int,int)把基類中的f(int)隱藏了
  所以,相同的函式名的函式,在基類和派生類中的關係只能是覆蓋或者隱藏。

  在文章中,我把過載和覆蓋的定義都給了出來了,但是一直沒有給隱藏的定義,在最後,我把他給出來,這段話是網上google來的,比較長,你可以簡單的理解成,在派生類域中,看不到基類中的那個同名函數了,或者說,是並沒有繼承下來給你用,呵呵,如例項一 那樣。
  

隱藏(hide):
指的是派生類的成員函式隱藏了基類函式的成員函式.隱藏一詞可以這麼理解:在呼叫一個類的成員函式的時候,編譯器會沿著類的繼承鏈逐級的向上查詢函式的定義,如果找到了那麼就停止查找了,所以如果一個派生類和一個基類都有同一個同名(暫且不論引數是否相同)的函式,而編譯器最終選擇了在派生類中的函式,那麼我們就說這個派生類的成員函式"隱藏"了基類的成員函式,也就是說它阻止了編譯器繼續向上查詢函式的定義.


C++純虛擬函式
 一、定義
  純虛擬函式是在基類中宣告的虛擬函式,它在基類中沒有定義,但要求任何派生類都要定義自己的實現方法。在基類中實現純虛擬函式的方法是在函式原型後加“=0” 
  virtual void funtion()=0 
二、引入原因
   1、為了方便使用多型特性,我們常常需要在基類中定義虛擬函式。 
   2、在很多情況下,基類本身生成物件是不合情理的。例如,動物作為一個基類可以派生出老虎、孔雀等子類,但動物本身生成物件明顯不合常理。 
  為了解決上述問題,引入了純虛擬函式的概念,將函式定義為純虛擬函式(方法:virtual ReturnType Function()= 0;),則編譯器要求在派生類中必須予以重寫以實現多型性。同時含有純虛擬函式的類稱為抽象類,它不能生成物件。這樣就很好地解決了上述兩個問題。
三、相似概念
   1、多型性 
  指相同物件收到不同訊息或不同物件收到相同訊息時產生不同的實現動作。C++支援兩種多型性:編譯時多型性,執行時多型性。
  a、編譯時多型性:通過過載函式實現 
  b、執行時多型性:通過虛擬函式實現。 

  2、虛擬函式 
  虛擬函式是在基類中被宣告為virtual,並在派生類中重新定義的成員函式,可實現成員函式的動態覆蓋(Override)
  3、抽象類 
  包含純虛擬函式的類稱為抽象類。由於抽象類包含了沒有定義的純虛擬函式,所以不能定義抽象類的物件