C++多型的好處和作用(用範例說話)

2020-07-16 10:04:21
在物件導向的程式設計中,使用多型能夠增強程式的可擴充性,即程式需要修改或增加功能時,只需改動或增加較少的程式碼。此外,使用多型也能起到精簡程式碼的作用。本節通過兩個範例來說明多型的作用。

遊戲程式範例

遊戲軟體的開發最能體現物件導向設計方法的優勢。遊戲中的人物、道具、建築物、場景等都是很直觀的物件,遊戲執行的過程就是這些物件相互作用的過程。每個物件都有自己的屬性和方法,不同物件也可能有共同的屬性和方法,特別適合使用繼承、多型等物件導向的機制。下面就以“魔法門”遊戲為例來說明多型在增加程式可擴充性方面的作用。

“魔法門”遊戲中有各種各樣的怪物,如騎士、天使、狼、鬼,等等。每個怪物都有生命力、攻擊力這兩種屬性。怪物能夠互相攻擊。一個怪物攻擊另一個怪物時,被攻擊者會受傷;同時,被攻擊者會反擊,使得攻擊者也受傷。但是一個怪物反擊的力量較弱,只是其自身攻擊力的 1/2。

怪物主動攻擊、被敵人攻擊和實施反擊時都有相應的動作。例如,騎士的攻擊動作是揮舞寶劍,而火龍的攻擊動作是噴火;怪物受到攻擊會嚎叫和受傷流血,如果受傷過重,生命力被減為 0,則怪物就會倒地死去。

針對:這個遊戲,該如何編寫程式,才能使得遊戲版本升級、要增加新的怪物時,原有的程式改動盡可能少呢?換句話說,如何才能使程式的可擴充性更好呢?

然而,無論是否使用多型,均應使每種怪物都有一個類與之對應,每個怪物就是一個物件。而且,怪物的攻擊、反擊和受傷等動作都是通過物件的成員函數實現的,因此需要為每個類編寫 Attack、FightBack 和 Hurted 成員函數。

Attack 成員函數表現攻擊動作,攻擊某個怪物並呼叫被攻擊怪物的 Hurted 成員函數,以減少被攻擊怪物的生命值,同時也呼叫被攻擊怪物的 FightBack 成員函數,遭受被攻擊怪物的反擊。

Hurted 成員函數減少自身生命值,並表現受傷動作。

FightBack 成員函數表現反擊動作,並呼叫被反擊物件的 Hurted 成員函數,使被反擊物件受傷。

下面對比使用多型和不使用多型兩種寫法,來看看多型在提高程式可擴充性方面的作用。

先介紹不用多型的寫法。假定用 CDmgon 類表示龍,用 CWolf 類表示狼,用 CGhost 類表示鬼,則 CDragon 類的寫法大致如下(其他類的寫法與之類似):
class CDragon
{
private:
    int power;  //攻擊力
    int lifeValue;  //生命值
public:
    void Attack(CWolf * p);  //攻擊“狼”的成員函數
    void Attack(CGhost* p);  //攻擊“鬼”的成員函數
                             //……其他 Attack 過載函數
                             //表現受傷的成員函數
    void Hurted(int nPower);
    void FightBack(CWolf * p);  //反擊“狼”的成員函數
    void FightBack(CGhost* p);  //反擊“鬼”的成員函數
                                //......其他FightBack過載函數
};
各成員函數的寫法如下:
void CDragon::Attack(CWolf* p)
{
    p->Hurted(power);
    p->FightBack(this);
}
void CDragon::Attack(CGhost* p)
{
    p->Hurted(power);
    p->FightBack(this);
}
void CDragon::Hurted(int nPower)
{
    lifeValue -= nPower;
}
void CDragon::FightBack(CWolf* p)
{
    p->Hurted(power / 2);
}
void CDragon::FightBack(CGhost* p)
{
    p->Hurted(power / 2);
}
第 1 行,Attack 函數的引數 p 指向被攻擊的 CWolf 物件。

第 3 行,在 p 所指向的物件上面執行 Hurted 成員函數,使被攻擊的“狼”物件受傷。呼叫 Hurted 成員函數時,引數是攻擊者“龍”物件的攻擊力。

第 4 行,以指向攻擊者自身的 this 指標為引數,呼叫被攻擊者的 FightBack 成員函數,接受被攻擊者的反擊。

在真實的遊戲程式中,CDragon 類的 Attack 成員函數中還應包含表現“龍”在攻擊時的動作和聲音的程式碼。

第 13 行,一個物件的 Hurted 成員函數被呼叫會導致該物件的生命值減少,減少的量等於攻擊者的攻擊力。當然,在真實的程式中,Hurted 成員函數還應包含表現受傷時動作的程式碼,以及生命值如果減至小於或等於零,則倒地死去的程式碼。

第 17 行,p 指向的是實施攻擊者。對攻擊者進行反擊,實際上就是呼叫攻擊者的 Hurted 成員函數使其受傷。其受到的傷害的大小等於實施反擊者的攻擊力的一半(反擊的力量不如主動攻擊大)。當然,FightBack 成員函數中其實也應包含表現反擊動作的程式碼。

實際上,如果遊戲中有 n 種怪物,CDragon 類中就會有 n 個 Attack 成員函數,用於攻擊 n 種怪物。當然,也會有 71 個 FightBack 成員函數(這裡假設兩條龍也能互相攻擊)。對於其他類,如 CWolf 類等,也是這樣。

以上為非多型的實現方法。如果遊戲版本升級,增加了新的怪物“雷鳥”,假設其類名為 CThunderBird,則程式需要做哪些改動呢?

除了編寫一個 CThiinderBird 類外,所有的類都需要增加以下兩個成員函數,用以對“雷鳥”實施攻擊,以及在被“雷鳥”攻擊時對其進行反擊:
void Attack(CThunderBird* p);
void FightBack(CThunderBird* p);
這樣,在怪物種類多的時候,工作量會比較大。

實際上,在非多型的實現中,使程式碼更精簡的做法是將 CDragon、CWolf 等類的共同特點 抽取出來,形成一個 CCreature 類,然後再從 CCreature 類派生出 CDragon、CWolf 等類。但是由於每種怪物進行攻擊、反擊和受傷時的表現動作不同,CDmgon、CWdf 這些類還要實現各自的 Hurted 成員函數,以及一系列 Attack、FightBack 成員函數。因此,如果沒有利用多型機制,那麼即便引人基礎類別 CCreature,對程式的可擴充性也沒有太大幫助。

下面再來看看,如果使用多型機制編寫這個程式,在要新增 CThunderBird 類時,程式改動的情況。使用多型的寫法如下:設定一個基礎類別 CCreature,概括所有怪物的共同特點。所有具體的怪物類,如 CDragon、CWolf、CGhost 等,均從 CCreature 類派生而來。下面是 CCreature 類的寫法:
class CCreature {  //“怪物”類
protected:
    int lifeValue, power;
public:
    virtual void Attack(CCreature* p) {};
    virtual void Hurted(int nPower) {};
    virtual void FightBack(CCreature* p) {};
};
可以看到,基礎類別 CCreature 中只有一個 Attack 成員函數,也只有一個 FightBack 成員函數。

實際上,所有 CCreature 類的派生類也都只有一個 Attack 成員函數和一個 FightBack 成員函數。例如,CDragon 類的寫法如下:
class CDragon : public CCreature
{
public:
    virtual void Attack(CCreature* p) {
        p->Hurted(power);
        p->FightBack(this);
    }
    virtual int Hurted(int nPower) {
        lifeValue -= nPower;
    }
    virtual int FightBack(CCreature* p) {
        p->Hurted(power / 2);
    }
};
CDragon 類的成員函數中省略了表現動作和聲音的那部分程式碼。其他類的寫法和 CDragon 類類似,只是實現動作和聲音的程式碼不同。如何實現動畫的動作和聲音不是本書要講述的內容。

在上述多型的寫法中,當需要增加新怪物“雷鳥”時,只需要編寫新類 CThunderBird 即可,不需要在已有的類中專門為新怪物增加 void Attack(CThunderBird * p) 和 void FightBack(CThunderBird* p) 這兩個成員函數。也就是說,其他類根本不用修改。這樣一來,和前面非多型的實現方法相比,程式的可擴充性當然大大提高了。實際上,即便不考慮可擴充性的問題,程式本身也比非多型的寫法大大精簡了。

為什麼 CDragon 等類只需要一個 Attack 函數,就能夠實現對所有怪物的攻擊呢?

假定有以下程式碼片段:
CDragon dragon;
CWolf wolf;
CGhost ghost;
CThunderBird bird;
Dragon.Attack(&wolf);
Dragon.Attack(&ghost);
Dragon.Attack(&bird);
根據賦值相容規則,上面第 5、6、7 行中的引數都與基礎類別指標型別 CCreature* 相匹配,所以編譯沒有問題。從 5、6、7 三行進入 CDragon::Attack 函數後,執行 p-> Hurted(power) 語句時,p 分別指向的是 wolf、ghost 和 bird,根據多型的規則,分別呼叫的就是 CWolf::Hurted、CGhost::Hurted 和 CBird: Hurted 函數。

FightBack 函數的情況和 Attack 函數類似,不再贅述。

幾何形體程式範例

例題:編寫一個幾何形體處理程式,輸入幾何形體的個數以及每個幾何形體的形狀和引數,要求按面積從小到大依次輸出每個幾何形體的種類及面積。假設幾何形體的總藪不超過 100 個。

例如,輸入
4
R 3 5
C 9
T 3 4 5
R 2 2

表示一共有 4 個幾何形體,第一個是矩形(R 代表矩形),寬度和高度分別是 3 和 5;第二個是圓形(C 代表圓形),半徑是 9;第三個是三角形(T代表三角形),三條邊的長度分別是 3,4,5;第四個是矩形,寬度和高度都是 2。

應當輸出:
Rectangle:4
Triangle:6
Rectangle:15
Circle:254.34

該程式可以運用多型機制編寫,不但便於擴充(新增新的幾何形體),還能夠節省程式碼量。程式如下:
#include <iostream>
#include <cmath>
using namespace std;
class CShape  //基礎類別:形體類
{
    public:
        virtual double Area() { };  //求面積
        virtual void PrintInfo() { }; //顯示資訊
};
class CRectangle:public CShape  //派生類:矩形類
{
    public:
        int w,h;     //寬和高
        virtual double Area();
        virtual void PrintInfo();
};
class CCircle:public CShape  //派生類:圓類
{
    public:
        int r;      //半徑
        virtual double Area();
        virtual void PrintInfo();
};
class CTriangle:public CShape //派生類:三角形類
{
    public:
        int a,b,c;      //三邊長
        virtual double Area();
        virtual void PrintInfo();
};
double CRectangle::Area()  {
    return w * h;
}
void CRectangle::PrintInfo()  {
    cout << "Rectangle:" << Area() << endl;
}
double CCircle::Area()  {
    return 3.14 * r * r ;
}
void CCircle::PrintInfo()  {
    cout << "Circle:" << Area() << endl;
}
double CTriangle::Area()  {   //根據海倫公式計算三角形面積
    double p = ( a + b + c) / 2.0;
    return sqrt(p * ( p - a)*(p- b)*(p - c));
}
void CTriangle::PrintInfo()  {
    cout << "Triangle:" << Area() << endl;
}
CShape *pShapes[100]; //用來存放各種幾何形體,假設不超過100個
int MyCompare(const void *s1, const void *s2)  //定義排序規則的函數
{
    CShape **p1 = (CShape **)s1; //s1是指向指標的指標,其指向的指標為CShape* 型別
    CShape **p2 = ( CShape **)s2;
    double a1 = (*p1)->Area(); //p1指向幾何形體物件的指標, *p1才指向幾何形體物件
    double a2 = (*p2)->Area();
    if( a1 < a2 )
        return -1;   //面積小的排前面
    else if (a2 < a1)
        return 1;
    else
        return 0;
}
int main()
{
    int i; int n;
    CRectangle *pr; CCircle *pc; CTriangle *pt;
    cin >> n;
    for( i = 0;i < n;++i ) {
        char c;
        cin >> c;
        switch(c) {
            case 'R': //矩形
            pr = new CRectangle();
            cin >> pr->w >> pr->h;
            pShapes[i] = pr;
            break;
             case 'C': //圓
            pc  = new CCircle();
            cin >> pc->r;
            pShapes[i] = pc;
            break;
            case 'T': //三角形
            pt = new CTriangle();
            cin >> pt->a >> pt->b >> pt->c;
            pShapes[i] = pt;
            break;
        }
    }
    qsort(pShapes,n,sizeof(Cshape *),MyCompare);
    for(i = 0;i <n;++i) {
        pShapes[i]->PrintInfo();
        delete pShapes[i]; //釋放空間
    }
    return 0;
}
程式涉及三種幾何形體。如果不使用多型,就需要用三個陣列分別存放三種幾何形體,不但編碼麻煩,而且如果以後要增加新的幾何形體,就要增加新的陣列,擴充性不好。

本程式將所有幾何形體的共同點抽象出來,形成一個基礎類別 CShape,CRectangle、CCircle 等各種幾何形體類都由 CShape 類派生而來。每個類都有各自的計算面積函數 Area 和顯示資訊函數 PrintInfo,這兩個函數在所有類中都有,而且都是虛擬函式。

第 50 行定義了一個 CShape * pShapes[100] 陣列。由於基礎類別指標也能指向派生類物件,因此,每輸入一個幾何形體,就動態分配一個與該形體對應的類的物件(第 74、79、84 行), 然後將該物件的指標存入 pShapes 陣列(第 76、81、86 行)。總之,pShapes 陣列中的元素可能指向 CRectangle 物件,也可能指向 CCircle 物件,還可能指向 CTriangle 物件。

第 90 行對 pShapes 陣列進行排序。排序的規則是按陣列元素所指向的物件的面積從小到大排序。注意,待排序的陣列元素是指標而不是物件,因此呼叫 qsort 時的第三個引數是 sizeof (CShape *),而不是 sizeof(CShape)。

在定義排序規則的 MyCompare 函數中,形參 s1(s2 與 s1 類似)指向的是待排序的陣列元素,陣列元素是指標,因而 s1 是指向指標的指標,其指向的指標是 CShape* 型別。*s1是陣列元素,即指向物件的指標,**s1才是幾何形體物件。

由於 s1 是 void* 型別的,*s1 無定義,因此要先將 s1 轉換為 CShape** 型別的指標 p1(第 53 行),此後,*p1 即是一個 CShape* 型別的指標,*p1 指向一個幾何形體物件,通過(*p1)->Area()就能求該物件的面積了(第 55 行)。(* p1) -> Area();這條語句是多型的,因為 *p1 是基礎類別指標,Area 是虛擬函式。程式執行到此時,*p1 指向哪種物件,就會呼叫相應類的計算面積函數 Area,正確求得其面積。

如果不使用多型,就需要將不同形體的面積一一求出來,存到另外一個陣列中,然後再排序,排序後還要維持面積值和其所屬的幾何形體的對應關係——這顯然是比較麻煩的。

多型的作用還體現在第 91、92 行。只要用一個迴圈遍歷排好序的 pShapes 陣列,並通過陣列元素呼叫 PrintInfo 虛擬函式,就能正確執行不同形體物件的 PrintInfo 成員函數,輸出形體物件的資訊。

上面這個使用了多型機制的程式,不但編碼比較簡單,可擴充性也較好。如果程式需要增加新的幾何形體類,所要做的事情也只是從 CShape 類派生出新類,然後在第 72 行的 switch 語句中加入一個分支即可。

第 93 行釋放動態分配的物件。按照本程式的寫法,這條語句是有一些問題的。具體是什麼問題,如何解決,將在《虛解構函式》一節中解釋。