2010年8月25日 星期三

polymorphic 與 virtual關鍵字

物件導向設計中有一項精華就是 polymorphic(同名異式),以 MFC 為例,每個元件其實最底層都是 CWnd (視窗),CButton 與 CEdit都繼承了 CWnd,兩都也都有 SetWindowText(),但 CButton 會顯示在按鍵的中間,CEdit 會顯示在框框中,這就是 polymorphic 的表現。

同名異式還有一項重點,就是這種情況只會發生在繼承與虛擬函式上面,B、C 類別同樣繼承 A 又符合上面的條件,才能說 B、C 有同名異式,這就要來筆記一下 virtual 關鍵字了。

假設你要寫一個 RPG 遊戲,遊戲中有很多種類的 NPC例如蛇妖、蜘蛛、火龍、強盜等 NPC,這時我們可以先找出共通點建立一個叫 BaseNpc 的基底類別,


class CBaseNpc
{
private:
  int m_nType; // 怪的屬性、攻擊型、魔法型等
  int m_nHP; // 每種 npc 都有 HP 值
  int m_nMP; // 每種 npc 都有 MP 值
  int m_nDefense; // 防禦力
  int m_nAttack; // 攻擊力
public:
  int GetHP() {return m_nHP;};
  void SetHP(int n) {m_nHP = n;};
  int GetMP() {return m_nMP;};
  void SetMP(int n) {m_nMP = n;};
  int GetDefense() {return m_nDefense;};
  void SetDefense(int n) {m_nDefense = n;};
  int GetAttack() {return m_nAttack;};
  void SetAttack(int n) {m_nAttack = n;};
};


這時我要做寫任何怪的類別時,都可以使用公用繼承省下寫這些重覆的 Code 例如


class CSnake : public CBaseNpc
{
  // 使用 public 繼承,讓 Snake 類別已包含 BaseNpc 的公用介面,
  // 但對於 BaseNpc 的私有成員必須透過公用函式來存取。
};


在實作CSnake時發現,這似乎不太夠,NPC 還要會攻擊呀,但蛇妖的攻擊是咬或噴毒、甩尾巴,強盜是砍、踢、丟暗器,同樣都有 Attack() 函式,我們就可以把這個 function 加到基底類別中,但各 NPC 的行為不同,我們在 function 定義前加上 virtual 關鍵字變成


class CBaseNpc
{
private:
  int m_nType // 怪的屬性、攻擊型、魔法型等
  int m_nHP; // 每種 npc 都有 HP 值
  int m_nMP; // 每種 npc 都有 MP 值
  int m_nDefense; // 防禦力
  int m_nAttack; // 攻擊力
public:
  int GetHP() {return m_nHP;};
  void SetHP(int n) {m_nHP = n;};
  int GetMP() {return m_nMP;};
  void SetMP(int n) {m_nMP = n;};
  int GetDefense() {return m_nDefense;};
  void SetDefense(int n) {m_nDefense = n;};
  int GetAttack() {return m_nAttack;};
  void SetAttack(int n) {m_nAttack = n;};
  virtual int Attack(int nHeroDef)
  {
    // 站著不動,僅回傳攻擊力減掉主角的防禦力/2
    // 甚至加入更複雜的屬性相剋判斷或 miss 機率
    return (m_nAttack - (nHeroDef / 2));
  };
};

這時其他繼承 CBaseNpc 的怪就可以自己實作自己的攻擊方式,
例如蛇妖和強盜都實作了不同的攻擊方式


int main()
{
  CSnake snakeA; // 一支蛇妖
  CRobber robberA; // 一個強盜
  CHero hero; // 主角
  snakeA.Attack(hero.GetDefense()); // 對主角咬或噴毒、甩尾
  robberA.Attack(hero.GetDefense()); // 拿兵器砍或丟主角
  return TRUE;
}


整篇的重點來了,其實以上面的例子看來,沒有加 virtual 關鍵字也是可以的,CSnake 的物件會去做 CSnake 的攻擊動作,強盜會做強盜自己的攻擊動作,那我們沒事加個 virtual 做什麼呢?這要牽扯到物件的型態,在沒有 virtual 關鍵字的情況下,物件會根據它自己的型態去呼叫相對應的函式。

但注意,基底類別可以指向任何繼承它的衍生類別,這時它只能使用自己所有的類別成員,不可以使用衍生類別多出來的成員,且不論它指向什麼,一律只做基底類別的動作,因為它是基底類別指標嘛。

有種情況發生在我們常常希望用 "陣列" 來管理畫面中的所有 NPC,所以我們宣告一個可以指向所有NPC的基底類別陣列,若沒有用 virtual 關鍵字,這段程式碼的結果會變這樣。


int main()
{
  CBaseNpc *arrayNPC(2);
  CHero hero; // 主角
  arrayNPC[0] = new CSnake();
  arrayNPC[1] = new CRobber();
  arrayNPC[0]->Attack(hero.GetDefense()); // 執行基底類別的"站著不動"
  arrayNPC[1]->Attack(hero.GetDefense()); // 執行基底類別的"站著不動"
  return TRUE;
}


要解決這種情況的發生,virtual 就派上用場了,若你需要用到基底類別的物件指標指向任何 NPC 時,要記得將這類函式加上 virtual 關鍵字,才能正確執行所指類別型態的 function

沒有留言:

張貼留言