1. 程式人生 > >C++中 多型 過載 覆蓋

C++中 多型 過載 覆蓋

面向物件的三大特徵:

1.封裝:保證物件自身資料的完整性、安全性

2.繼承:建立類之間的關係,實現程式碼複用、方便系統的擴充套件

3.多型:相同的方法呼叫可實現不同的實現方式。多型是指兩個或多個屬於不同類的物件,對於同一個訊息(方法呼叫)作出不同響應的方式。(同樣的操作,不同的物件執行,就會出現不同的結果。)

、、、、、、、、、

實現多型的方式:

函式過載;運算子過載;虛擬函式

、、、、、、、、、

C++有兩種多型:

1.編譯時的多型:函式過載和運算子過載,在編譯時就決定呼叫哪個函式

2.執行時的多型:通過類繼承和虛擬函式實現的。

、、、、、、、、

函式過載包括:普通函式過載和成員函式過載。函式的引數個數、型別、順序不同

運算子過載:實現兩個物件的相加。

、、、、、、、、

當基類中的某個函式被定義為虛擬函式之後,該函式可以在一個或多個派生類中重新定義,但其函式原型,包括返回值型別,引數個數,引數型別和引數順序都必須和基類中的一致。

多型性是面向物件程式設計的重要特徵之一。它與封裝性和繼承性共同構成了面向物件程式設計的三大特徵。封裝性是基礎 ,繼承性是關鍵 ,多型性是補充 ,多型性又存在於繼承的環境之中 ,所以這三大特徵是相互關聯的 ,相互補充的。C++ 語言中有兩種過載 :函式過載和運算子過載。運算子過載很重要 ,它的實質就是函式過載。我們接觸的還有另一種是指同樣的訊息被不同類的物件接受時產生完全不同的實現 ,該情況大多產生在多類繼承中不同類中的相同說明的成員函式的多型行為。

··································································

多型是基於對抽象方法的覆蓋來實現的,用統一的對外介面來完成不同的功能。過載也是用統一的對外介面
來完成不同的功能。那麼兩者有什麼區別呢?
過載,是指允許存在多個同名方法,而這些方法的引數不同。過載的實現是:編譯器根據方法不同的引數表
,對同名方法的名稱做修飾。對於編譯器而言,這些同名方法就成了不同的方法。它們的呼叫地址在編譯期
就綁定了。
多型:是指子類重新定義父類的虛方法(virtual,abstract)。當子類重新定義了父類的虛方法後,父類根據
賦給它的不同的子類,動態呼叫屬於子類的該方法,這樣的方法呼叫在編譯期間是無法確定的。


不難看出,兩者的區別在於編譯器何時去尋找所要呼叫的具體方法,對於過載而言,在方法呼叫之前,編譯
器就已經確定了所要呼叫的方法,這稱為“早繫結”或“靜態繫結”;而對於多型,只有等到方法呼叫的那一刻
,編譯器才會確定所要呼叫的具體方法,這稱為“晚繫結”或“動態繫結”。 

·························································································································································································

一.多型
(連結機制)
多型(Polymorphism)按字面的意思就是“多種形狀”。引用Charlie Calverts對多型的描述——多型性是允許你將父物件設定成為和一個或更多的他的子物件相等的技術,賦值之後,父物件就可以根據當前賦值給它的子物件的特性以不同的方式運作(摘自“Delphi4 程式設計技術內幕”)。簡單的說,就是一句話:允許將子類型別的指標賦值給父類型別的指標。多型性在Object Pascal和C++中都是通過虛擬函式(Virtual Function) 實現的。

  多型性是允許將父物件設定成為和一個和多個它的子物件相等的技術,比如Parent:=Child; 多型性使得能夠利用同一類(基類)型別的指標來引用不同類的物件,以及根據所引用物件的不同,以不同的方式執行相同的操作.
  多型的作用:把不同的子類物件都當作父類來看,可以遮蔽不同子類物件之間的差異,寫出通用的程式碼,做出通用的程式設計,以適應需求的不斷變化。
  賦值之後,父物件就可以根據當前賦值給它的子物件的特性以不同的方式運作。也就是說,父親的行為像兒子,而不是兒子的行為像父親。
  舉個例子:從一個基類中派生,響應一個虛命令,產生不同的結果。
  比如從某個基類繼承出多個物件,其基類有一個虛方法Tdoit,然後其子類也有這個方法,但行為不同,然後這些子物件中的任何一個可以附給其基類的物件,這樣其基類的物件就可以執行不同的操作了。實際上你是在通過其基類來訪問其子物件的,你要做的就是一個賦值操作。
  使用繼承性的結果就是可以建立一個類的家族,在認識這個類的家族時,就是把匯出類的物件 當作基類的的物件,這種認識又叫作upcasting。這樣認識的重要性在於:我們可以只針對基類寫出一段程式,但它可以適 應於這個類的家族,因為編譯器會自動就找出合適的物件來執行操作。這種現象又稱為多型性。而實現多型性的手段又叫稱動態繫結(dynamic binding)。
  簡單的說,建立一個父類的變數,它的內容可以是這個父類的,也可以是它的子類的,當子類擁有和父類同樣的函式,當使用這個變數呼叫這個函式的時候,定義這個變數的類,也就是父類,裡的同名函式將被呼叫,當在父類裡的這個函式前加virtual關鍵字,那麼子類的同名函式將被呼叫
  class A {
  public:
  A() {}
  virtual void foo() {
  cout << "This is A." << endl;
  }
  };
  class B : public A {
  public:
  B() {}
  void foo() {
  cout << "This is B." << endl;
  }
  };
  int main(int argc, char* argv[]) {
  A *a = new B();
  a->foo();
  return 0;
  }
  這將顯示:
  This is B.
  如果把virtual去掉,將顯示:
  This is A.
  前面的多型實現使用抽象類,並定義了虛方法.

二.過載
(編譯機制)
過載決策是一種編譯時機制,用於在給定了引數列表和一組候選函式成員的情況下,選擇一個最佳函式成員來實施呼叫。函式過載就是一個類中有幾個同名函式但引數表不同:

過載分為普通方法過載和基類(也就是父類)虛方法的過載!

普通方法的過載指的是:類中兩個以上的方法(包括隱藏的繼承而來的方法),取的名字相同,但使用的引數型別或者引數個數不同!

對基類方法的過載是函式過載的另一種特殊形式。在派生類中重新定義此虛擬函式!方法名稱,返回值型別,引數表中的引數個數,型別,順序都必須和基類中的虛擬函式完全一致!在派生類中宣告對虛方法的過載,要求在宣告中加上override關鍵字,而且不能有new,static或virtual修飾符!

························································································································································································· 覆蓋 在基類中定義了一個非虛擬函式,然後在派生類中又定義了一個同名同參數同返回型別的函式,這就是覆蓋了。 在派生類物件上直接呼叫這個函式名,只會呼叫派生類中的那個。 過載 在基類中定義了一個非虛擬函式,然後在派生類中定義一個同名,但是具有不同的引數表的函式,這就是過載。 在派生類物件上呼叫這幾個函式時,用不同的引數會呼叫到不同的函式,有可能會直接呼叫到基類中的那個。 多型 在基類中定義了一個虛擬函式,然後在派生類中又定義一個同名同參數表的函式,這就是多型。 多型是這3種情況中唯一採用動態繫結技術的一種情況。也就是說,通過一個基類指標來操作物件,如果物件是基類物件,就會呼叫基類中的那個函式,如果物件實際是派生類物件,就會呼叫派聲類中的那個函式,呼叫哪個函式並不由函式的引數表決定,而是由函式的實際型別決定。 在這之所以以不停強調基類和派生類,是因為在面向物件的思想中不存在一個單獨的不隸屬於類的方法。 通俗點說,比如吃東西是一個方法,但這個方法可以使人類的,也可以是動物的。面向物件的思想中這個方法必須屬於一個類

··································································

                水煮多型——對C++多型性的形象解釋

水是什麼形狀的?

乍一看這個問題似乎問得很沒有道理,其實仔細想想,水正是自然界中“多型”的完美體現。不是麼?用圓柱形容器裝水,那麼水就是圓柱形的;換用圓錐形 容器盛之,水則又會成為圓錐形的了。在這個過程中,我們並不需要關心水是如何改變形狀的,亦無需關心水在改變形狀的過程中具體做了哪些事情;我們所要關心 的,只是提供給它一個什麼形狀的容器,這就足夠了。

OO(面向物件)中所謂的多型性,也正是這個道理。對於一個同名的方法(Water),我們在不同的情況(Container)下對其進行呼叫,那麼它所完成的行為(Where_Am_I)也是不一樣的。以下我將解說的,便是C++之中對於“多型”幾種不同的實現形式。

函式的過載(Overload)

這兒是一個非常簡單的函式max,它返回兩個傳入引數中較大的那一個。

int max( int a, int b )

{

    if ( a > b )

        return a;

    else

        return b;

}

相信這段程式碼的具體內容不用我解釋了,是的,這是一段非常基本的程式碼。你可能會發現,這個max函式只適用於int型別的引數。那麼,如果我同時還需要一個針對double型別的max,又該怎麼辦呢?

所幸C++語言本身提供了這一功能,它允許我們在定義函式的時候使用相同的名稱——是為函式的過載。也就是說,我們可以繼續定義一個double版本的max:

double max( double a, double b )

{

    if ( a > b )

        return a;

    else

        return b;

}

然後,在我們的程式碼中對這兩個函式分別進行呼叫:

void f( void )

{

    int a = max( 1, 2 );

    double b = max( 1.5, 2.5 );

}

這樣一來,我們無需關心呼叫的是哪個版本的max,編譯器會自動根據我們給定的引數型別(int或double)挑選出最適當的max函式來進行呼叫。

模板(Template)

函式的過載的確為我們提供了很大的方便,我們不需要關心呼叫哪個函式,編譯器會根據我們給定的引數型別挑選出最適當的函式進行呼叫。但是對於下面的情況,函式的過載就不是很適用了:

函式體程式碼內容基本相同。

需要為多個型別編寫同樣功能的函式。

也就是說,我們也許需要更多版本(int、double,甚至更多自定義型別,如複數complex之類)的max,但是它們的程式碼卻無一例外的都是:

if ( a > b )

    return a;

else

    return b;

這樣一來,我們需要做的事情就更傾向於一種體力勞動,而且,如是過多重複的工作也必然存在著錯誤的隱患。C++在這一方面,又為我們提供了一個解決方法,那就是模板。對於上面這眾多版本且內容基本相同的max函式,我們只需要提供一個像下面這樣函式模板即可:

template < typename T >

T max( const T& a, const T& b )

{

    if ( a > b )

        return a;

    else

        return b;

}

template是C++的關鍵字,表示它以下的程式碼塊將使用模板。尖括號裡面的內容稱為模板引數,表示其中的T將在下面的程式碼模板中作為一種確定 的型別使用。引數之所以使用const引用的形式,是為了避免遇到類物件的時候不必要的傳值開銷。在這個模板定義完畢之後,我們就可以像這樣使用了:

void f( void )

{

    int a = max< int >( 1, 2 );

    double b = max< double >( 1.5, 2.5 );

}

對於這段程式碼,編譯器會分別將int與double填充到函式模板中T所在的位置,也就是分別為max< int >和max< double >各自產生一份max函式的實體程式碼。這樣一來,就達到了與函式過載一樣的效果,但是程式設計師的工作量卻是不可同日而語的。

虛擬函式(Virtual Function)

下面來以水為例,說說虛擬函式提供的多型表現形式。首先我們建立一個Water類,用來表示水。

class Water

{

public:

    virtual void Where_Am_I() = 0;

};

正如單獨討論水的形狀沒有意義一樣,我們在這裡當然也不能允許Water類的例項化,所以成員函式Where_Am_I被定義為了純虛擬函式。下面,我們來分別定義水(Water)在瓶子(Bottle)、玻璃杯(Glass)以及湖泊(Lake)中的三種不同情況:

class Water_In_Bottle : public Water

{

public:

    virtual void Where_Am_I()

    {

        cout << "Now I'm in a bottle." << endl;

    }

};

class Water_In_Glass : public Water

{

public:

    virtual void Where_Am_I()

    {

        cout << "Now I'm in a glass." << endl;

    }

};

class Water_In_Lake : public Water

{

public:

    virtual void Where_Am_I()

    {

        cout << "Now I'm in a lake." << endl;

    }

};

這三者分別實現了成員函式Where_Am_I。然後,多型性的實現就可以通過一個指向Water的指標來完成:

void f( void )

{

    Water_In_Bottle a;

    Water_In_Glass b;

    Water_In_Lake c;

    Water *pWater[3];

    pWater[0] = &a;

    pWater[1] = &b;

    pWater[2] = &c;

    for ( int i = 0; i < 3; i++ )

    {

        pWater[i]->Where_Am_I();

    }

}

這樣,程式的執行結果是:

Now I'm in a bottle.

Now I'm in a glass.

Now I'm in a lake.

好了,如你所見,我們並不需要關心pWater指向的是哪一種水,而只需要通過這個指標進行相同的呼叫工作,水本身就可以根據自身的所在來選擇相應 的行為。虛擬函式的多型性是非常有用的,尤其是在使用C++進行Windows程式設計的時候。考慮那些不同的視窗針對使用者的相同行為而能夠做出不同反應, 也正是由於相應的訊息響應虛擬函式的具體實現不同,方能達到這樣的效果。

水煮多型,暫且煮到這裡。這裡所談及的僅僅是C++對於多型的表現形式,而並未對文中三種技術(過載、模板、虛擬函式)的具體內容進行過多的解說—— 畢竟稍微一深入就難免會對某些技術細節進行大篇幅追究,譬如說到過載難免就會說到引數的匹配,說到模板又難免與泛型進行掛鉤,到了虛擬函式又不能不提一下VTable的東西……在這裡我一概全免,因為我的目的也就是希望通過上面幾個簡單的例子讓諸位看官能對OO本身的多型有一個感性的認識,感謝您們的閱讀