C++面向對象程序設計中,最重要的規則便是:public繼承應當是"is-a"的關係。當Derived public繼承自Base時, 相當於你告訴編譯器和所有看到你代碼的人:Base是Derived的抽象,Derived就是一個Base,任何時候Derived都可以代替Base使用。
比如一個Student
繼承自Person
,那麼Person
有什麼屬性Student
也應該有,接受Person
類型參數的函數也應當接受一個Student
:
void eat(const Person& p);
void study(const Person& p);
Person p; Student s;
eat(p); eat(s);
study(p); study(s);
語言的二義性
上述例子也好理解,也很符合直覺。但有時情況卻會不同,比如Penguin
繼承自Bird
,但企鵝不會飛:
class Bird{
public:
vitural void fly();
};
class Penguin: public Bird{
// fly??
};
這時你可能會困惑Penguin
到底是否應該有fly()
方法。但其實這個問題來源於自然語言的二義性: 嚴格地考慮,鳥會飛並不是所有鳥都會飛。我們對會飛的鳥單獨建模便是:
class Bird{...};
class FlyingBird: public Bird{
public:
virtual void fly();
};
class Penguin: public Bird{...};
這樣當你調用penguin.fly()時便會編譯錯。當然另一種辦法是Penguin繼承自擁有fly()方法的Bird, 但Penguin::fly()中拋出異常。這兩種方式在概念是有區別的:前者是說企鵝不能飛;後者是說企鵝可以飛,但飛了會出錯。
哪種實現方式好呢?[Item 18]中提到,接口應當設計得不容易被誤用,最好將錯誤從運行時提前到編譯時。所以前者更好!
錯誤的繼承
生活的經驗給了我們關於對象繼承的直覺,然而並不一定正確。比如我們來實現一個正方形繼承自矩形:
class Rect{...};
void makeBigger(Rect& r){
int oldHeight = r.height();
r.setWidth(r.width()+10);
assert(r.height() == oldHeight);
}
class Square: public Rect{...};
Square s;
assert(s.width() == s.height());
makeBigger(s);
assert(s.width() == s.height());
根據正方形的定義,寬高相等是任何時候都需要成立的。然而makeBigger卻破壞了正方形的屬性, 所以正方形並不是一個矩形(因爲矩形需要有這樣一個性質:增加寬度時高度不會變)。即Square繼承自Rect是錯誤的做法。 C++類的繼承比現實世界中的繼承關係更加嚴格:任何適用於父類的性質都要適用於子類!
本節我們談到的是"is-a"關係,類與類之間還有着其他類型的關係比如"has-a", "is-implemented-in-terms-of"等。這些在Item-38和Item-39中分別介紹。