子類別繼承父類別,可用來避免重複的行為,不過並非為了避免重複定義行為就使用繼承,濫用繼承而導致程式維護上的問題時有所聞,如何正確判斷使用繼承的時機,以及繼承之後如何活用多型,才是學習繼承時的重點。
無論如何,先來看看行為重複是怎麼一回事,假設你在正開發一款 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
,符合最低限度的關係。
就這邊的範例說,建構子類實例時,會先執行父類建構式,接著是子類建構式,而解構的時候相反,會先執行子類解構式,接著才是父類解構式。