摘要:“多態(tài)”是面向?qū)ο蟪绦蛟O(shè)計(jì)方法中的重要概念,也是提高程序可擴(kuò)充性的重要手段。然而初學(xué)面向?qū)ο缶幊痰膶W(xué)生往往難以真正體會到其作用。文章介紹一個在教學(xué)中沿用多年,能夠生動而充分地展示多態(tài)的作用,并在教學(xué)比賽中獲獎的游戲編程教學(xué)案例,供大家參考。
關(guān)鍵詞:多態(tài);可擴(kuò)充性;虛函數(shù);抽象類
1問題的提出
面向?qū)ο蟪绦蛟O(shè)計(jì)語言有封裝、繼承和多態(tài)三種機(jī)制,這三種機(jī)制能夠有效提高程序的可讀性、可擴(kuò)充性和可重用性。
而“多態(tài)”(Polymorphism),可以分為編譯時的多態(tài)和運(yùn)行時的多態(tài)。
編譯時的多態(tài),主要指的是運(yùn)算符的重載和函數(shù)的重載。這部分內(nèi)容,比較簡單,易于理解,本文并不打算討論。
運(yùn)行時的多態(tài),指的是以下機(jī)制(本文以后提到的“多態(tài)”,都指的是運(yùn)行時的多態(tài)):
對于通過基類指針,調(diào)用基類和派生類中都有的同名、同參數(shù)表的虛函數(shù)這樣的語句,編譯時并不確定要執(zhí)行的是基類還是派生類的虛函數(shù);而當(dāng)程序運(yùn)行到該語句時,如果該基類指針指向的是一個基類對象,則基類的虛函數(shù)被執(zhí)行,如果該基類指針指向的是一個派生類對象,則派生類的虛函數(shù)被執(zhí)行(將上面表述中的“指針”換成“引用”,同樣成立)。
多態(tài)可以簡單地理解成同一條函數(shù)調(diào)用語句能調(diào)用不同的函數(shù),或者說,對不同對象發(fā)送同一消息,使得不同對象有各自不同的行為。
多態(tài)在面向?qū)ο蟮某绦蛟O(shè)計(jì)語言中是如此重要,以至于有類和對象的概念,但是不支持多態(tài)的語言,只能被稱作“基于對象的程序設(shè)計(jì)語言”,而不能被稱為“面向?qū)ο蟮某绦蛟O(shè)計(jì)語言”。如Visual Basic就是“基于對象的程序設(shè)計(jì)語言”。
讓學(xué)生掌握多態(tài)的語法規(guī)則并不難,難的是讓他們深刻理解多態(tài)到底有什么用處。實(shí)際上,在面向?qū)ο蟮木幊讨惺褂枚鄳B(tài),能夠有效地提高程序的可擴(kuò)充性,這就是多態(tài)最大的作用。
所謂一個程序的可擴(kuò)性好,指的就是當(dāng)該程序的功能需要增加或修改時,只需改動或增加比較少的代碼就能實(shí)現(xiàn)。往往,一個程序員只有在編寫了一定規(guī)模的程序,并且等到其程序真正需要添加新功能的時候,才能切身體會到程序的可擴(kuò)充性是多么重要。那么,怎樣才能讓沒有多少編程經(jīng)歷的低年級學(xué)生,不需要編寫大規(guī)模的程序就能體會到多態(tài)在提高程序可擴(kuò)充性方面的作用呢?這就是本文要探討的問題。
2問題的現(xiàn)狀
筆者查閱多本流行的C++教材,這些教材和講義大多對多態(tài)提高程序的可擴(kuò)充性這個作用未能充分展示。這些教材在闡述多態(tài)時,所舉的例子一般都是這樣的:
開設(shè)一個基類指針數(shù)組,該數(shù)組里的指針,有的指向基類對象,有的指向派生類對象。在此種情況下,遍歷該數(shù)組,對每個數(shù)組元素,均通過它去調(diào)用基類和派生類里都有的同名虛函數(shù),這就達(dá)到了在每個對象上都執(zhí)行它自己的虛函數(shù)的目的[2]。例如,一個幾何形體演示程序,有基類Shape,還有Rectangle,Triangle和Circle等Shape的派生類,這些類都有虛函數(shù)double Area()用以計(jì)算圖形的面積。那么要計(jì)算所有幾何圖形的面積,只需用一個Shape * 類型的數(shù)組,存放所有幾何圖形對象的地址,然后遍歷該數(shù)組,對每個元素(即類型為Shape * 的變量)均通過它去調(diào)用Area()虛函數(shù),那么多態(tài)機(jī)制就能確保每個幾何圖形的面積都是用正確的Area()函數(shù)計(jì)算出來的[3]。
這樣的例子,說明使用多態(tài)能夠某種程度上精簡程序的代碼,但不能很好地說明多態(tài)在增強(qiáng)可擴(kuò)充性方面的作用。比較好的例子應(yīng)該是用多態(tài)和非多態(tài)的方法各寫一段程序,然后要求對該程序進(jìn)行功能上的擴(kuò)充,此時再來看這兩段程序各要做多大的改動——這才能夠充分體現(xiàn)多態(tài)的優(yōu)勢。
筆者看到的教材里,只有一部采用了這樣的寫法[1]。該書舉了一個異質(zhì)鏈表(同一鏈表里存放不同類型的對象)的例子。該例子能夠充分說明多態(tài)的優(yōu)點(diǎn),但是略顯冗長,不夠生動有趣,也不像實(shí)踐中的例子。
那么軟件開發(fā)的實(shí)踐中,能否找到生動有趣而又不冗長的例子,來充分說明多態(tài)在程序可擴(kuò)充性方面的作用呢?答案是肯定的,那就是到游戲開發(fā)中去尋找案例。
3問題的解決
游戲軟件的開發(fā),是最能體現(xiàn)面向?qū)ο笤O(shè)計(jì)方法的優(yōu)勢的。游戲中的人物、道具、建筑物、場景,都是很直觀的對象,游戲運(yùn)行的過程,就是這些對象相互作用的過程。每個對象都有自己的屬性和方法,不同對象又可能有共同的屬性和方法,特別適合使用繼承、多態(tài)等面向?qū)ο蟮臋C(jī)制。而且,游戲本來就是學(xué)生所津津樂道的,在課堂的PPT里放幾張游戲的截圖,學(xué)生精神就會為之一振,興趣大增。因此,筆者在講述“多態(tài)”這一概念的時候,以“魔法門之英雄無敵”游戲的開發(fā)為例,充分論述了多態(tài)在提高程序可擴(kuò)充性方面的作用,讓同學(xué)們不但能學(xué)得明白,還能學(xué)得有趣。
“魔法門”游戲中有各種各樣的怪物,如騎士、天使、狼,鬼,等等。每個怪物都有生命力、攻擊力這兩種屬性。怪物能夠互相攻擊,一個怪物攻擊另一個怪物時,會使被攻擊者受傷;同時被攻擊者會反擊,使得攻擊者也受傷。但是一個怪物反擊的力量較弱,只是其自身攻擊力的1/2。
怪物主動攻擊、被敵人攻擊和實(shí)施反擊時都有相應(yīng)的動作。比如騎士攻擊時的動作就是揮舞寶劍,而火龍的攻擊動作就是噴火;怪物受到攻擊會嚎叫和受傷流血,如果受傷過重,生命力被減為0,則怪物就會倒地死去…….
針對這個游戲,教師提出的問題是:該如何編寫程序,才能使得游戲版本升級,要增加新的怪物時,原有的程序改動盡可能少 。換句話說,就是怎樣才能使程序的可擴(kuò)充性更好。
顯然,不論是否使用多態(tài),均應(yīng)使每種怪物都有一個類與之對應(yīng),每個怪物就是一個對象。而且,怪物的攻擊、反擊和受傷等動作,都是通過對象的成員函數(shù)實(shí)現(xiàn)的,因此為每個類都需要編寫Attack、FightBack和 Hurted成員函數(shù)
Attact函數(shù)表現(xiàn)攻擊動作,攻擊某個怪物,并調(diào)用被攻擊怪物的 Hurted函數(shù),以減少被攻擊怪物的生命值,同時也調(diào)用被攻擊怪物的 FightBack成員函數(shù),遭受被攻擊怪物反擊。
Hurted函數(shù)減少自身生命值,并表現(xiàn)受傷動作。
FightBack成員函數(shù)表現(xiàn)反擊動作,并調(diào)用被反擊對象的Hurted成員函數(shù),使被反擊對象受傷。
接下來就是對比使用多態(tài)和不使用多態(tài)兩種寫法,來體現(xiàn)多態(tài)在提高程序可擴(kuò)充性方面的作用。
先看不用多態(tài)的寫法。假定用“CDragon”類表示火龍,用“CWolf”類表示狼,用“CGhost”類表示鬼,則“CDragon”類寫法大致如下(其他類的寫法也類似):
class CDragon
{
private:
int m_nPower ; //攻擊力
int m_nLifeValue ; //生命值
public:
//攻擊“狼”的成員函數(shù)
void Attack(CWolf * p);
//攻擊“鬼”的成員函數(shù)
void Attack(CGhost * p);
//......其他Attack重載函數(shù)
//表現(xiàn)受傷的成員函數(shù)
void Hurted( int nPower);
//反擊“狼”的成員函數(shù)
void FightBack(CWolf * p);
//反擊“鬼”的成員函數(shù)
void FightBack(CGhost * p);
//......其他FightBack重載函數(shù)
};
接下來再看各成員函數(shù)的寫法:
1.void CDragon::Attack(CWolf * p)
2.{
3.p->Hurted(m_nPower);
4. p->FightBack(this);
5.}
6.void CDragon::Attack(CGhost * p)
7.{
8. p->Hurted(m_nPower);
9. p->FightBack(this);
10.}
11.void CDragon::Hurted(int nPower)
12.{
13.m_nLifeValue -= nPower;
14.}
15.void CDragon::FightBack(CWolf * p)
16.{
17. p->Hurted(m_nPower/2);
18.}
19.void CDragon::FightBack(CGhost * p)
20.{
21.p->Hurted(m_nPower/2);
22.}
在上面帶行號的程序中:
第1行,Attack函數(shù)的參數(shù)p,指向被攻擊的CWolf對象。
第3行,在p所指向的對象上面執(zhí)行Hurted成員函數(shù),使被攻擊的“狼”對象受傷。調(diào)用Hurted時,參數(shù)是攻擊者“龍”對象的攻擊力。
第4行,以指向攻擊者自身的this指針為參數(shù),調(diào)用被攻擊者的FightBack成員函數(shù),接受被攻擊者的反擊。
顯然,在真實(shí)的游戲程序中,CDragon類的Attack成員函數(shù)中還應(yīng)包含表現(xiàn)火龍?jiān)诠魰r的動作和聲音的代碼。
第13行,一個對象的Hurted成員函數(shù)被調(diào)用會導(dǎo)致該對象的生命值減少,減少的量等于攻擊者的攻擊力。當(dāng)然,真實(shí)的程序中,Hurted函數(shù)還應(yīng)包含表現(xiàn)受傷時動作的代碼,以及生命力如果減至小于等于零,則倒地死去的代碼。
第17行,p指向的是實(shí)施攻擊者,對攻擊者進(jìn)行反擊,實(shí)際上就是調(diào)用攻擊者的Hurted成員函數(shù)使其受傷。其受到的傷害的大小,等于實(shí)施反擊者的攻擊力的一半(反擊的力量不如主動攻擊大)。當(dāng)然,F(xiàn)ightBack函數(shù)中其實(shí)也應(yīng)包含表現(xiàn)反擊動作的代碼。
實(shí)際上,如果游戲中有n種怪物,CDragon 類中就會有n個Attack成員函數(shù),用于攻擊n種怪物。當(dāng)然,也會有n個FightBack成員函數(shù)(這里我們假設(shè)兩條龍也能互相攻擊)。對于其他類,比如CWolf等,也是這樣
以上為非多態(tài)的實(shí)現(xiàn)方法。如果游戲版本升級,增加了新的怪物雷鳥,假設(shè)其類名為CThunderBird, 則程序需要做哪些改動呢?
顯然,除了新寫一個CThunderBird類外,所有的類都需要增加以下兩個成員函數(shù),用以對雷鳥實(shí)施攻擊,以及在被雷鳥攻擊時對其進(jìn)行反擊:
void Attack( CThunderBird * p) ;
void FightBack( CThunderBird * p) ;
這樣,在怪物種類多的時候,工作量就較大。
實(shí)際上,非多態(tài)實(shí)現(xiàn)中,代碼更精簡的做法是將CDragon,CWolf等類的共同特點(diǎn)抽取出來,形成一個CCreature類,然后再從CCreature類派生出CDragon、CWolf等類。但是由于每種怪物進(jìn)行攻擊、反擊和受傷時的表現(xiàn)動作不同,CDragon、CWolf這些類還是要實(shí)現(xiàn)各自的Hurted成員函數(shù),以及一系列Attack、FightBack成員函數(shù)。所以只要沒有利用多態(tài)機(jī)制,那么即便引入基類CCreature,對程序的可擴(kuò)充性也無幫助。
下面再來看看,如果使用多態(tài)機(jī)制來編寫這個程序,在要新增CThunderBird類的時候,程序改動有多大。
多態(tài)的寫法如下:
設(shè)置一個抽象類CCreature,概括了所有怪物的共同特點(diǎn)。然后,所有具體的怪物類,比如CDragon,CWolf,CGhost等,均從CCreature類派生而來。
下面是CCreature類的寫法:
class CCreature{
protected :
int m_nLifeVa