子類別繼承父類別,可用來避免重複的行為,不過並非為了避免重複定義行為就使用繼承,濫用繼承而導致程式維護上的問題時有所聞,如何正確判斷使用繼承的時機,以及繼承之後如何活用多型,才是學習繼承時的重點。
無論如何,先來看看行為重複是怎麼一回事,假設你在正開發一款 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) +
")";
};
};
你注意到什麼呢?因為只要是遊戲中的角色,都會具有角色名稱、等級與血量,類別中也都為名稱、等級與血量定義了取值方法與設值方法,Magician 與 SwordsMan 有許多程式碼重複了。
重複在程式設計上,就是不好的訊號。舉個例子來說,如果要將 name、level、blood 改為其他名稱,那就要修改 SwordsMan 與 Magician 兩個類別,如果有更多類別具有重複的程式碼,那就要修改更多類別,造成維護上的不便。
如果要改進,可以把相同的程式碼提昇(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) +
")";
};
};
這個類別在定義上沒什麼特別的新語法,只不過是將 SwordsMan 與 Magician 中重複的程式碼複製過來。接著 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 繼承而來的相關成員維持既有的權限控制。
在繼承類別時,還可以在 : 右邊指定 protected 或 private,表示繼承而來的 Role 成員權限控制最大是 protected 或 private,例如若 : 右邊指定 private,Role 的 protected 或 public 成員在子類中,權限就會被限縮為 private。
繼承時設定的權限預設會套用至各個成員,然而,可以使用 using 指出哪些成員要維持父類中設定之權限。例如,若父類 P 中有 public 的 publicMember 及 protected 的 protectedMember:
class D : private P {
public:
using P::publicMember; // 維持 public
protected:
using P::protectedMember; // 維持 protected
};
如果繼承時沒有指定 public、protected、private,若子類別定義時使用 struct,那預設就是 public 繼承,若子類別定義時使用 class,那預設就是 private 繼承。
定義類別時,protected 成員,是表示只能被子類存取。
在方才的程式碼中,SwordsMan 定義了建構式,建構時指定的 name、level、blood 指定給 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;
}
雖然 SwordsMan 與 Magician 並沒有定義 to_string 方法,但從 Role 繼承了,所以可以直接使用,執行的結果如下:
揮劍攻擊
魔法攻擊
魔法治療
SwordsMan(Justin, 1, 1000)
Magician(Magician, 1, 800)
繼承的好處之一,就是若要將 name、level、blood 等值域改名為其他名稱,那就只要修改 Role 就可以了,繼承 Role 的子類別無需修改。
在 SwordsMan、Magician 中定義了建構式,並呼叫了父類 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 是一種 Role,Magician 是一種 Role,符合最低限度的關係。
就這邊的範例說,建構子類實例時,會先執行父類建構式,接著是子類建構式,而解構的時候相反,會先執行子類解構式,接著才是父類解構式。

