類別若繼承兩個以上的抽象類別,而兩個抽象類別都定義了相同方法,那麼子類別會怎樣嗎?程式面上來說,並不會有錯誤,照樣通過編譯:
class Task {
public:
virtual void execute() = 0;
virtual void doSome() = 0;
virtual ~Task() = default;
};
class Command {
public:
virtual void execute() = 0;
virtual void doOther() = 0;
virtual ~Command() = default;
};
class Service : public Task, public Command {
public:
void execute() override {
cout << "foo" << endl;
}
void doSome() override {
cout << "some" << endl;
}
void doOther() override {
cout << "other" << endl;
}
};
但在設計上,你要思考一下:Task
與 Command
定義的 execute
是否表示不同的行為?
如果表示不同的行為,那麼 Service
在實作時,應該會有不同的方法實作,那麼 Task
與 Command
的 execute
方法就得在名稱上有所不同,Service
在實作時才可以有兩個不同的方法實作。
如果表示相同的行為,那可以定義一個父類別,在當中定義純虛擬 execute
方法,而 Task
與 Command
繼承該類別,各自定義純虛擬的 doSome
與 doOther
方法:
#include <iostream>
using namespace std;
class Action {
public:
virtual void execute() = 0;
virtual ~Action() = default;
};
class Task : public Action {
public:
virtual void doSome() = 0;
};
class Command : public Action {
public:
virtual void doOther() = 0;
};
class Service : public Task, public Command {
public:
void execute() override {
cout << "service" << endl;
}
void doSome() override {
cout << "some" << endl;
}
void doOther() override {
cout << "other" << endl;
}
};
int main() {
Service service;
service.execute();
service.doSome();
service.doOther();
Task &task = service;
task.doSome();
Command &command = service;
command.doOther();
return 0;
}
這個程式可以編譯成功也可以執行,不過從〈多重繼承的建構〉可以知道,task
與 command
的位址會是不同,建構 service
的過程中,Task
、Command
的建構式中 this
會是不同位址,而它們又會以各自的 this
來執行 Action
的建構式。
也就是就上例來說,Action
的建構流程會跑兩次,一次是以 task
的位址,一次是以 command
的位址,這意謂著,如果 Action
定義了值域,task
與 command
會各自擁有一份。
另外要知道的是,目前為止的繼承方式,都是編譯時期就決定了子類從父類繼承而來的定義,例如,單看 Task
,編譯時期就決定了從 Action
繼承而來的定義,而單看 Command
,編譯時期就決定了從 Action
繼承而來的定義。
結果就是,由於 Task
、Command
各自有一份編譯時期繼承而來的 Action
定義,如果 Service
同時繼承了 Task
、Command
,那它會有兩份 Action
定義,各來自 Task
、Command
,藉由 this
的實際位址來決定該使用哪個定義。
這就有了個問題,如果是用 Action
型態來參考 service
呢?
Action &action = service; // error: 'Action' is an ambiguous base of 'Service'
由於 Service
有兩份 Action
定義,作為父型態的 Action
要參考 service
時,編譯器不知道你想採用哪份 Action
定義,如果想在編譯時期就決定這件事,就得明確告訴編譯器:
Action &action1 = static_cast<Task&>(service);
Action &action2 = static_cast<Command&>(service);
action1.execute();
action2.execute();
如果不想使用 static_cast
呢?根源在於 Task
、Command
在編譯時期就決定了從 Action
繼承而來的定義,才造成 Service
中有兩份 Action
定義,那能不能在執行時期才決定 Task
、Command
繼承的定義,就類似 virtual
函式,執行時期才決定實際的函式位址?
這可以透過虛繼承,也就是在繼承時加上 virtual
關鍵字來達到:
class Task : public virtual Action {
public:
virtual void doSome() = 0;
};
class Command : public virtual Action {
public:
virtual void doOther() = 0;
};
class Service : public Task, public Command {
public:
void execute() override {
cout << "service" << endl;
}
void doSome() override {
cout << "some" << endl;
}
void doOther() override {
cout << "other" << endl;
}
};
現在 Task
、Command
編譯過後,不會各自包含 Action
的定義了,只會各自有個可用來指向 Action
的指標,在執行時期才指向同一個 Action
類別,因此 Service
繼承而來的 Action
類別也就是 Task
、Command
共享的那一個,因此 Action
型態就可以直接參考 Service
實例了:
Action &action = service;
action.execute();
在虛繼承下,Action
的建構式只會以 Service
實例的位址執行一次。
當然,這些都是編譯器的細節,若要從語義上理解,實際上 Service
才真的實作 execute
,Task
、Command
不用真的包含 Action
定義,virtual
繼承時,Task
、Command
就像是轉接 Action
,Service
發現這兩個類別轉接的對象是同一個 Action
,最後就會像是 Service
直接繼承 Action
,若要做個比喻,就會像 class Service : public Action, public Task, public Command
。
另一種語義上的理解方式是,虛繼承的 Task
、Command
表明,若以 Action
型態參考實例來操作時,Task
、Command
的 this
願意共用相同的位址,而這個位址會是同時繼承了 Task
、Command
的子類位址,也就是 Service
實例的位址。