有網友學生針對《白話C++》問到一個小事, 有關虛函數和多態:
“ 如果說虛函數是為了讓衍生類別的對象擁有特別、與人不同的個性的話。那麼定義一個基類的堆變數:卻用衍生類別對堆變數進行執行個體化:
Code:
- Person *someone;
- someone=new Beauty;
這有什麼用?有什麼好處嗎?與其這樣,為什麼不乾脆把基類、衍生類別對象分別執行個體化? ”
簡單回一下:
基類,衍生類別分別執行個體化,也很常見.這通常叫做OB編程("基於對象"),在這種情況下,衍生類別和基類都是具體的東西,只不過衍生類別對基類在功能有所擴 展......但在OO方法中,抽象是很重要的. 所謂抽象 和具體,通常就是:基類代表"抽象",衍生類別代表"具體". 比如:基類是:飛行物(抽象)...而衍生類別:飛機,老鷹....(具體,或叫具象). 如果你要寫一個打飛行物的程式,我們當然希望射擊邏輯只對"飛行物"寫一次,而不是針對各種各樣的衍生類別.
基類作為一種“抽象”,它只是提出要求:“我要求我的衍生類別必須提供哪些功能” ——這正是“類別”所代表的含義。我們偶爾會罵別人:“你簡直就不是人”。為什麼這麼罵? 那是因為,某些社會的,公眾的道德規定了“人類”應該具備的“功能”,如果一個人被社會唾棄,那麼他肯定是:
1)、做了一些非人類功能範圍內的事。 (比如他擁有一些只有禽獸才具備的功能)。
2)、缺失一些人類必須擁有的功能。(比如,善良。作為一個對象,他發現自己居沒有這個功能)
回到飛行物的例子,再常見不過了。在飛行物射擊類遊戲中,它通常要具備:
1、能飛(不然怎麼叫飛物)?
2、能判斷是否被擊中?
3、被擊種以後,要有一些反應。
4、被擊,但未中的情況下,也來點反應……
Code:
- class Flyer //不是蒼蠅
- {
- public:
- virtual void Flying() = 0; //我會飛
- virtual bool HitTest(int x, int y, Bullet const& bullet) = 0; //被子彈擊中了嗎?
- virtual void OnHitted() = 0; //被擊中怎麼辦?
- virtual void OnEscape() = 0; //躲過時怎麼辦?
- };
好,承認這個抽象可能寫得不好,比如HitTest函數,也許不需要x,y,而由bullet提供自己的位置……另外,飛行物如何畫到螢幕的函數沒提供……但這些都不是重點。重點是,這是一個不錯的抽象,所以它全都是“=0”的純虛函數——代表Flyer只是提供要求,所有具體動作或功能,都需要衍生類別來實現。
但衍生類別會有哪些呢?理論是無邊無盡的!該遊戲的創造者,假設就你,可能唯寫以下一個:
Code:
- class Airplane : public Flyer
-
- {
-
- //具體實現略
-
- };
只有飛機一種射擊目標,所以這個遊戲或許只叫“打飛機”遊戲更合適吧……但你們的老闆有雄心壯志!果然這個遊戲大賣特賣。不過,由於你的出色才華,你光榮地被M$給收買,於是又來了一個程式員補你的缺,假設是我,我呢,覺得光有飛機不太好玩,最好加下難度係數小一點的,於是我寫這樣一個衍生類別:
Code:
- class Duck : public Flyer //別不把鴨子當飛行物
- {
- //... 其餘略
- virtual void OnHitted() //被擊中
- {
- cout << "Ga~~ Ga~~~" << endl; //純屬示意,真正的代碼是往音效卡輸出
- //....
- }
-
- };
現在問題來了!我的前任(也就是你),當時根本不知道會有人寫一個“鴨子”衍生類別,而鐵打的營盤流水的兵,有一天,我也會離職(我好想去google啊~~555),我的續任者寫什麼什麼衍生類別呢? 我現在不知道,反正是一種不明飛行物,噢對了,是UFO。
這裡的典型問題就是:難道後任程式員,都必須對前任程式的代碼深翻三尺地修改嗎?對於BO,這是很有可能的。但對於OO,這個可以得到有效緩解,因為,OO程式員,他懂得(爭取)只對“抽象”對象寫代碼中的主要邏輯,比如一顆子彈飛出去,而此時滿屏的飛行物,那麼我的偉大的前任,偉大的OO程式員,再強調一次,就是你,是這樣寫這一段邏輯的:
Code:
- ...
-
- std::list<Flyer *> flyerLst; //一個列表存放當前螢幕上所有飛行物
-
- Bullet bullet; //一顆飛出的子彈.嗖嗖...
-
- ...
-
- std::list<Flyer*>::iterator it; //列表的迭代器,請看《白話C++》的感受篇:Hello STL
-
- for (it=flyerLst.begin(); it != flyerLst.end(); ++it)
-
- {
-
- //重點在這裡:
-
- Flyer* flyer = *it; //為了清楚,我囉嗦一點
-
- if (flyer->HitTest(bullet)) //判斷是否擊中?
-
- {
-
- flyer->OnHittest(); //如果flyer是duck對象,那它就會GaGa地叫~~~
-
- //....
-
- }
-
- }
看,上面的這段代碼就是你當時寫的,它一直都在,並且只處理Flyer對象!沒有duck,ufo的單詞吧?
繼任者,我,只要往list裡塞我鴨子就對了……
Code:
- Flyer* flyer = new Duck; //這裡無所謂,也可以寫 Duck* duck = new Duck;
- //但可能的話……
-
- flyerLst.push_back(flyer);
作為前任,你不需要知道任何有關鴨子的具體的事……還有UFO……
--------------------
補充一點正題——我以為我在《白話 C++》裡已經講過了。new的時候,也就是上面代碼中“但可能的話”,為什麼也要這樣寫呢;
Flyer* flyer = new Duck;
這要看情況,當我們直接就可以知道當前要建立的是哪個對象,並且需要針對衍生類別作具體的設定時,那麼寫得直接明了一點,是必要的。想像一下,我們希望鴨子在成功躲閃子彈時,可能會因為緊張而下出蛋來,這時遊戲者可以射中鴨蛋以獲得積分。但鴨子區分公母,而且也並不是所有雌性鴨子能下蛋(發育問題)。所以我們在建立一隻鴨子之後,需要調用一個函數,讓它去嘗試準備一下蛋的庫存。
Code:
- Duck* duck = new Duck;
-
- duck->PrepareEggs(); //或許只有鴨子需要準備蛋。
-
- flyerLst.push_back(duck);
這種情況下,就叫做”具體事情具體分析“。交給衍生類別吧,如果用代表抽象的基類指標,代碼編譯不過去,不是嗎?因為下蛋並不是所有飛行物都有的功能。
Code:
- Flyer* flyer = new Duck;
-
- flyer->PrepareEggs(); //編譯出錯! Flyer類沒有這個函數
但不能這樣就放過問題! 真的只有鴨子會下東西嗎? 飛機在逃避過子彈時,(敵軍飛行員)會不會一生氣,扔下一些炸彈呢?嗯,而UFO難道不會扔下幾個外星人? 要讓遊戲有擴充性,這樣的預先設計上的彈性是必要的。所以,我們再改一改,將“下蛋”這樣一件具體的事,抽象為“扔東西”!
Code:
- class Flyer
-
- {
-
- // ... 原有的略
-
- virtual void ThrowObject() = 0; //扔東西
-
- virtual void PreareObject() = 0; //準備東西庫存
-
- };
多麼美妙的設計 (一個正常的程式員的自我YY)!現在,我們可以實現的,再也不是僅僅建立時,也不僅僅是鴨子,更不僅僅是可以準備鴨蛋。 我們可以滿足這樣的需求:當飛行物在天空中堅持飛行了達到一定時間長度,就可以自動調用PrepareObject以準備一下庫存,然後時機合適時,就通過ThrowObject()把各種各樣的東西扔下來!如此,玩家就需要更聰明一些,比如可以讓鴨子多存活一陣,因為我們想要它的蛋,而飛機可就不得了了,一定要快點幹掉它,不然它的炸彈會越來越多……也許你就是遊戲策劃師,此時你是主動去設計這些遊戲邏輯,也許你是程式員,此時你是被迫去實現這些邏輯,但好的程式設計是沒有感情的,不分主動被動,它的目標就是為了讓複雜的,不斷髮展變化的邏輯更容易實現,讓程式員更爽,對應的,壞的設計就是為了讓你,或者你的續任者痛不欲生的。
第三個問題,也是關鍵問題。一個好的OO設計師,當然不會寫出完全是OB的代碼,但也並不是一定要(客觀上也做不到)抽象出所有對象的功能介面。上帝花了心思創造鴨子,所以鴨子總有鴨子的獨立人生——臨時勵志一下:上帝創造了我們每個個體,所以我們總有自己的獨到之處——這些獨到是無法,或者說不適於抽象的——繼續迴歸正題,但就算如此,有時我們也還是需要用基類指標來接受每個獨特的衍生類別對象。
繼續本遊戲。想像一下,螢幕上的飛行物不停地被擊中,落下,程式肯定要負責繼續產生新的飛行物飛上天,請問,我們如何決定要產生哪種飛行物呢? 這是一個策略。策略可以很簡單,完全用隨機數決定是一種(抽獎吧),但最好是根據難度,關數,來決定一下只是什麼東西飛上天。我們可以寫一個函數:
Code:
- Flyer* CreateNewFlyObject(); //一個重要的函數!它採用的策略好壞,是決定這個
- //“好玩度”的重要因素之一。
雖然不是直接在new某個飛行物,但這個函數其實就是一複雜的new。它通過種種判斷,來產生一個Flyer。至於這個Flyer是什嗎?調用處的代碼在良好的設計下,是不應該去關心的,調用者只需用基類指標去承接:
Code:
- Flyer* flyer = CreateNewFlyObject();
這行話,代表的意思就是(虛擬碼): Flyer* flyer = new Dock?? UFO? Airplane?? 管它建立出來的什麼呢!我統統把它們當成飛行物就對了! (一個OO程式員的心聲)。
結論還是一樣:我們總是在努力地讓關鍵代碼盡量只處理基類(抽象),而不是衍生類別(具象)。
有讀者要提問題,但……其實是一段廣告啦,如果你沒空,就別往下拉了。:)
有人問,看完《白話C++》會不會寫得出上面的遊戲?
答:沒問題,因為《白話C++》不是只講文法的書(雖然文法內容是最大一塊),也會以著名的SDL為基礎,至少講解三個小遊戲。前而已經有非編程專業同學,在唯讀了不到四分一內容情況下,通過自己擴充,寫出了其中一個遊戲。
------------歡迎關注《白話 C++》的出版-------------------------
忙著房子的事……很久沒來這裡冒泡了,各位同學們還好吧~~~ 如果您想與我交流,請點擊如下連結成為我的好友:
http://student.csdn.net/invite.php?u=112600&c=f635b3cf130f350c