很多人認為,C++中是不存在接口繼承的,只有Java、C#這類語言才提供了相應的語法支持。
但是,如同魯迅說過的某句名言:世上本沒有接口繼承,用的人多了,才有了接口繼承。C++中依然可以實現接口繼承,只是形式上稍有不同罷了。
C++中的繼承基於一個事實:父類定義的成員函數會一直被子類繼承(包括被子類隱藏的部分)。
而父類中提供的函數可以有三種:1)普通成員函數 2)普通虛函數 3)純虛函數。這三種函數類型代表了三種繼承設計模式。
一個簡單的實例代碼如下:
04 |
virtual void Draw() = 0; |
05 |
virtual int GetError(); |
09 |
class Rectangular : public Shape |
14 |
class Circle : public Shape |
普通成員函數由父類聲明且實現,子類應繼承接口以及強制性的實現。
這幾乎是最常見的一種函數類型,代表了典型的”is-a”繼承設計模式。
ps:所謂的”is-a”設計模式,指的是”everything that applies to base classes must also apply to derived classes”
示例中,函數GetId嚴格遵守”is-a”模式。因為每個子類本質都是一個Shape對象,都有一個唯一的ID
普通虛函數可以在父類中有默認的實現,而這個默認實現可以由子類繼承。
子類也可以選擇重寫虛函數以實現多態性。
所以,普通虛函數在繼承設計中表示派生類必須支持此接口,但是否重寫,由派生類自己決定。
如同每個子類對象都應該有一個報錯函數。但是函數可以使用父類提供的默認實現(提示簡單的出錯信息,然后清理資源),也可以選擇自己實現(每個子類有自己的錯誤語義)
純虛函數會使得父類自動成為不可實例化的抽象類。而且每個繼承的子類必須強制自行重寫。
所以,純虛函數表示子類繼承父類的函數接口,並且必須自己具體實現該函數。
即從這個角度上看,純虛函數代表的就是接口繼承。
實例代碼中,父類將Draw聲明為純虛函數。這表明每個具體的子類都應該有Draw函數,並且需要自己實現(每個具體子類的Draw實現應是不同的)。
對於純虛函數,有一個有意思的特性:純虛函數可以有實現代碼
之所以說這個特性有意思,是因為擁有純虛函數的類不能實例化並且純虛函數指定的是接口繼承,子類仍然需要自己實現函數。
這就引發了一個問題:如何調用這個純虛函數的默認實現版本?解決的方法是顯式調用
1 |
Shape* p = new Rectangular; |
那么這種讓人覺得操蛋的trick有沒有什么應用呢?
假設你有一個父類F,定義了一個普通虛函數,子類A,B都使用默認的虛函數實現。但是某天你需要增加一個新的子類C,但是這個子類不能使用默認的實現,必須重寫。
不幸的是,你忘了重寫這個函數,所以編譯器為你調用了默認實現,於是意外的結果讓你蛋碎了一地。
很明顯,使用帶有實現的純虛函數就可以解決這個問題。純虛函數會強制要求你重寫虛函數,而你也可以在需要默認實現時通過顯式調用完成相應工作。
不過需要這種trick的情況相當罕見,而且多半是設計出了問題或者完全可以人為避免。