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