面向對象的三大特徵:
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修飾符!
·························································································································································································
··································································
水煮多態——對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本身的多態有一個感性的認識,感謝您們的閱讀。