繼承共同行為


子類別繼承父類別,可用來避免重複的行為,不過並非為了避免重複定義行為就使用繼承,濫用繼承而導致程式維護上的問題時有所聞,如何正確判斷使用繼承的時機,以及繼承之後如何活用多型,才是學習繼承時的重點。

無論如何,先來看看行為重複是怎麼一回事,假設你在正開發一款 RPG(Role-playing game)遊戲,一開始設定的角色有劍士與魔法師。首先你定義了劍士類別:

class SwordsMan {
    string name;   // 角色名稱
    int level;     // 角色等級
    int blood;     // 角色血量

public:
    SwordsMan(string name, int level, int blood)
     : name(name), level(level), blood(blood) {}

    void fight() {
        cout << "揮劍攻擊" << endl;
    }

    string to_string() {
        return "(" + 
            this->name + ", " + 
            std::to_string(this->level) + ", " + 
            std::to_string(this->blood) + 
        ")";
    };
};

接著你為魔法師定義類別:

class Magician {
    string name;   // 角色名稱
    int level;     // 角色等級
    int blood;     // 角色血量

public:
    Magician(string name, int level, int blood)
     : name(name), level(level), blood(blood) {}

    void fight() {
        cout << "魔法攻擊" << endl;
    }

    void cure() {
        cout << "魔法治療" << endl;
    }

    string to_string() {
        return "(" + 
            this->name + ", " + 
            std::to_string(this->level) + ", " + 
            std::to_string(this->blood) + 
        ")";
    };
};

你注意到什麼呢?因為只要是遊戲中的角色,都會具有角色名稱、等級與血量,類別中也都為名稱、等級與血量定義了取值方法與設值方法,MagicianSwordsMan 有許多程式碼重複了。

重複在程式設計上,就是不好的訊號。舉個例子來說,如果要將 namelevelblood 改為其他名稱,那就要修改 SwordsManMagician 兩個類別,如果有更多類別具有重複的程式碼,那就要修改更多類別,造成維護上的不便。

如果要改進,可以把相同的程式碼提昇(Pull up)為父類別:

class Role {
    string name;   // 角色名稱
    int level;     // 角色等級
    int blood;     // 角色血量

public:
    Role(string name, int level, int blood)
     : name(name), level(level), blood(blood) {}

    string to_string() {
        return "(" + 
            this->name + ", " + 
            std::to_string(this->level) + ", " + 
            std::to_string(this->blood) + 
        ")";
    };
};

這個類別在定義上沒什麼特別的新語法,只不過是將 SwordsManMagician 中重複的程式碼複製過來。接著 SwordsMan 可以如下繼承 Role

class SwordsMan : public Role {
public:
    SwordsMan(string name, int level, int blood) : Role(name, level, blood) {}

    void fight() {
        cout << "揮劍攻擊" << endl;
    }
};

在定義 SwordsMan 類別時,: 指出會 SwordsMan 會擴充 Role 的行為,: 右邊的 public 表示,會以公開的方式繼承 Role,這表示繼承而來的 Role 成員,權限控制最大是 public,也就是 Role 繼承而來的相關成員維持既有的權限控制。

在繼承類別時,還可以在 : 右邊指定 protectedprivate,表示繼承而來的 Role 成員權限控制最大是 protectedprivate,例如若 : 右邊指定 privateRoleprotectedpublic 成員在子類中,權限就會被限縮為 private

繼承時設定的權限預設會套用至各個成員,然而,可以使用 using 指出哪些成員要維持父類中設定之權限。例如,若父類 P 中有 publicpublicMemberprotectedprotectedMember

class D : private P {
public:
    using P::publicMember;    // 維持 public

protected:
    using P::protectedMember; // 維持 protected
};

如果繼承時沒有指定 publicprotectedprivate,若子類別定義時使用 struct,那預設就是 public 繼承,若子類別定義時使用 class,那預設就是 private 繼承。

定義類別時,protected 成員,是表示只能被子類存取。

在方才的程式碼中,SwordsMan 定義了建構式,建構時指定的 namelevelblood 指定給 Role 的建構式,SwordsMan 也定義了自己的 fight 方法。

類似地,Magician 可以如下繼承 Role 類別:

class Magician : public Role {
public:
    Magician(string name, int level, int blood) : Role(name, level, blood) {}

    void fight() {
        cout << "魔法攻擊" << endl;
    }

    void cure() {
        cout << "魔法治療" << endl;
    }
};

如何看出確實有繼承了呢?以下簡單的程式可以看出:

#include <iostream>
#include <string> 
using namespace std;

... 方才的 Role、SwordsMan、Magician 程式碼

int main() { 
    SwordsMan swordsMan("Justin", 1, 1000);
    Magician magician("Magician", 1, 800);

    swordsMan.fight();
    magician.fight();
    magician.cure();

    cout << "SwordsMan" << swordsMan.to_string() << endl;
    cout << "Magician" << magician.to_string() << endl;

    return 0;
}

雖然 SwordsManMagician 並沒有定義 to_string 方法,但從 Role 繼承了,所以可以直接使用,執行的結果如下:

揮劍攻擊
魔法攻擊
魔法治療
SwordsMan(Justin, 1, 1000)
Magician(Magician, 1, 800)

繼承的好處之一,就是若要將 namelevelblood 等值域改名為其他名稱,那就只要修改 Role 就可以了,繼承 Role 的子類別無需修改。

SwordsManMagician 中定義了建構式,並呼叫了父類 Role 建構式,實際上建構式本體沒寫什麼,在這種情況下,你可能會想直接繼承 Role 定義的建構流程,這可以透過 using 指定父類名稱來達到,例如:

class SwordsMan : public Role {
public:
    using Role::Role;

    void fight() {
        cout << "揮劍攻擊" << endl;
    }
};

class Magician : public Role {
public:
    using Role::Role;

    void fight() {
        cout << "魔法攻擊" << endl;
    }

    void cure() {
        cout << "魔法治療" << endl;
    }
};

這麼一來,SwordsMan("Justin", 1, 1000)Magician("Magician", 1, 800) 的建構流程,就直接走 Role 中相同簽署的建構流程了,不過,就繼承意義而言,這才是實質地繼承了建構式,不過這種方式,不能繼承預設、複製與移動建構式,若需要這些建構式,子類必須自行定義。

在物件導向中,繼承是個雙面刃,想判斷繼承的運用是否正確,有許多角度可以探討,最基本的,就是看看父子類別是否為「是一種(is-a)」的關係,就上例來說,SwordsMan 是一種 RoleMagician 是一種 Role,符合最低限度的關係。

就這邊的範例說,建構子類實例時,會先執行父類建構式,接著是子類建構式,而解構的時候相反,會先執行子類解構式,接著才是父類解構式。