在〈繼承共同行為〉中,Role
的 to_string
被繼承了,然而,你也許會想要 SwordsMan
、Magician
各自的 to_string
,可以有類別名稱作為前置,這個需求可以藉由在各自的類別中定義 to_string
來達成。例如:
#include <iostream>
#include <string>
using namespace std;
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) +
")";
};
};
class SwordsMan : public Role {
public:
using Role::Role;
void fight() {
cout << "揮劍攻擊" << endl;
}
string to_string() {
return "SwordsMan" + Role::to_string();
};
};
class Magician : public Role {
public:
using Role::Role;
void fight() {
cout << "魔法攻擊" << endl;
}
void cure() {
cout << "魔法治療" << endl;
}
string to_string() {
return "Magician" + Role::to_string();
};
};
int main() {
SwordsMan swordsMan("Justin", 1, 1000);
Magician magician("Magician", 1, 800);
swordsMan.fight();
magician.fight();
magician.cure();
cout << swordsMan.to_string() << endl;
cout << magician.to_string() << endl;
return 0;
}
在範例中,to_string
方法取得 Role::to_string
的呼叫結果,並加上各自的前置名稱後傳回,Role::to_string
這樣的呼叫,會隱含地傳入目前的 this
,作為 Role::to_string
中的 this
。
這一次雖然同樣是 swordsMan.to_string()
、magician.to_string()
呼叫,然而使用了子類各自的定義,父類的 to_string
定義被遮蔽(hide),因此執行結果會是:
揮劍攻擊
魔法攻擊
魔法治療
SwordsMan(Justin, 1, 1000)
Magician(Magician, 1, 800)
在子類別中若要呼叫父類建構式或者是父類方法,在其他語言中,會有 super
之類的關鍵字可以用,然而 C++ 必須使用父類名稱,在簡單的情境中,寫死父類名稱或許不是什麼問題,然而,在更複雜的情況,多個方法都得呼叫父類方法時,寫死一大堆父類名稱,可能就是個問題,如果父類名稱在撰寫時又比較複雜,問題可能就更大。
一個緩解的方式是以 using
定義別名。例如:
class SwordsMan : public Role {
using super = Role;
public:
using Role::Role;
void fight() {
cout << "揮劍攻擊" << endl;
}
string to_string() {
return "SwordsMan" + super::to_string();
};
};
這麼一來,未來若真的要修改父類名稱,可以只在一個地方修改。
實際上就以上的需求,你也可以在 SwordsMan
或 Magician
中定義一個 desc
來完成相同的任務,那麼以相同名稱遮敝父類方法的意義何在呢?
就這邊來說,在還沒遮敝同名方法方法前,swordsMan.to_string()
、magician.to_string()
在編譯時期,就綁定了呼叫的方法會是 Role
中定義的 to_string
方法,在遮敝同名方法之後,編譯時綁定的版本,就是各自類別中定義的 to_string
方法。
也就是就這邊的範例來說,遮敝同名方法之目的,是要在編譯時期,視實例的型態來綁定對應的方法版本。
如果遮敝了 to_string
,然後你這麼呼叫呢?
SwordsMan swordsMan("Justin", 1, 1000);
Role &role = swordsMan;
cout << role.to_string() << endl; // 顯示 (Justin, 1, 1000)
首先,因為繼承會有 is-a 的關係,也就是 SwordsMan
是一種 Role
,當 =
左邊型態是一種右邊型態時,編譯器允許隱含的型態轉換,因此 Role role = swordsMan
可以通過編譯。
接下來 role.to_string()
呼叫時,由於編譯器在編譯時期只知道 role
的型態是 Role
,雖然 role
實際上參考了 swordsMan
,然而編譯時期能綁定的就是 Role
的 to_string
定義,因此執行的結果會是來自 Role
的 to_string
定義,而不是 SwordsMan
的 to_string
定義。
如果想在編譯時期,不管實例實際上是哪種型態,一律視呼叫方法時的變數型態來決定呼叫的版本,這個行為就會是你要的,在這種情況下,若有個函式或方法,想要操作實例繼承而來,或者是本身定義的方法,會透過模版來達成。例如:
template <typename T>
void printInfo(T &t) {
cout << t.to_string() << endl;
}
因為是模版,實際上會依呼叫時指定的實例型態,重載出對應型態的版本,該呼叫哪個版本,是編譯時期就決定的事,就程式碼本身而言,是以父類定義的行為來看待實例的操作,是一種多型(polymorphism)的實現,由於這種多型實現是編譯時期就可以達成方法綁定,亦被稱為編譯時期多型(compile-time polymorphism)。
當然,printInfo
模版可以適用任何具有 to_string
方法的實例,不用是 Role
或其子類別的實例,因而可用來實現結構型態系統(structural type system)。
如果想在執行時期,看看實際上實例是何種型態,並採用各自型態的 to_string
定義,該方法必須設定為 virtual
,這之後再來說明了。