35.使公有繼承體現 “是一個” 的含義。
共有繼承意味著 “是一個”。如 ?class B:public A; 說明類型B的每一個對象都是一個類型A的對象,A比B具有更廣泛的概念,而B表示一個更特定的概念。
在C++中任何一個參數為基類的函數都可以實際取一個派生類的對象,只有共有繼承會如此,對于共有繼承,如AB,若有兩個函數 一個函數為 void fun1(A &a);另一個函數為void fun2(B& b);則對于AB的兩個對象a,和b,對于 fun1(a)和fun2(b和fun1(b))都是正確的,fun2(a)是錯誤的。注意只有共有繼承才有這個特性,對于私有繼承會與此不同。而且這是說 B的對象 "是一個“ A的對象,但是B的數組并不是一個A的數組。
使用公有繼承經常遇到的問題是對基類適用的規則并不適用于派生類,但公有繼承又要求對基類對象適用的任何東西都適用于派生類對象,使用公有繼承會導致一些錯誤的設計。
如對于企鵝與鳥,鳥是基類,其有嘴,翅膀等數據成員,還有一個飛的成員函數virtual void fly();一開始你認為鳥有一些屬性,且鳥會飛,然后你有認為企鵝公有繼承于鳥,即企鵝是一種鳥,但是問題出現了,企鵝不會飛。讓企鵝直接公有繼承于鳥類,這是一個錯誤的設計,所以你想去改進它,在依然使用公有繼承的前提下。
1.世上有很多鳥不會飛,于是你將鳥類分成了兩種,FlyingBird 和NoFlyingBird,分成會飛和不會飛兩種鳥類,這兩種鳥類都公有繼承于鳥類,而企鵝公有繼承于NoFlyingBird。
2.企鵝中依然有fly()這個函數,但是重新定義了這個fly函數,使之產生一個運行時錯誤,使企鵝是鳥,企鵝能飛,但是讓企鵝飛的這個操作是錯誤的。這是一個運行時才能檢測的錯誤。
當利用一些知識和常識設計一些類并使用公有繼承時,但是公有繼承卻沒那么有效,因為最關鍵的問題是基類中的規則要同樣適用于派生類對象,而我們想要用繼承實現的對象卻有兩者不同的規則。而對于這樣的情況,一般要用”有一個“ 和”用。。。來實現“這兩種關系來實現。
36.區分接口繼承和實現繼承。
首先,接口是放在public中給外部調用的,而實現是隱藏在private中的內部邏輯。對于類的繼承,有時希望派生類只繼承成員函數的接口,有時派生類同時繼承函數的接口和實現,且允許派生類改寫實現,有時派生類同時繼承類的接口與實現,但是不允許修改任何東西。
純虛函數必須要在具體實現類中重新聲明,它們在抽象類中往往沒有定義,定義純虛函數的目的在于使派生類僅僅 繼承函數的接口,也就是第一種情況,這種情況很容易理解。但是純虛函數其實是可以提供定義的。
對于第二種和第三種情況,繼承函數的接口和實現,一般使用虛函數來實現。而需要改進的地方是,對于一個基類中的虛函數,其有一定的實現,而派生類可以繼承這樣的接口和實現,既可以直接繼承基類中這個接口,也可以重寫這個接口的實現。這樣很科學,但是又要一個問題,當一個新的派生類繼承這個基類時,由于這個類中使用虛函數做接口,導致新的程序員忘記了重新聲明這個虛函數并給予新的實現邏輯而去錯誤的使用虛函數中的默認邏輯而造成了錯誤。為了提供更安全的基類,使用純虛函數做接口,讓純虛函數有自己缺省實現,在派生類繼承時,直接調用基類純虛函數的實現:
~~~
class A{
public:
virtual void fun() const = 0;
};
void A::fun() const{
cout<<"Class A"<<endl;
}
class B:public A{
public:
virtual void fun() const;
};
void B::fun() const{
A::fun();
}
~~~
如上所示,使用一個純虛函數,但是帶有缺省實現,而派生類繼承時就必須重新聲明這個純虛函數,而對于要調用基類的缺省實現時,除了上面直接調用基類的這個純虛函數外,還可以通過在基類中的protected中設置一個默認的實現函數,如 void defaultFun() const;而派生類會繼承這個默認實現,然后在派生類的重新定義的虛函數中調用這個默認的實現函數即可。
這個情況其實就是第二種情況,繼承函數的接口和實現,且能夠修改實現。一般使用虛函數,但是使用帶默認操作的純虛函數會更加安全。安全是一個很重要的問題,如果不考慮安全性,很多在Effective C++這本書中討論的問題都是沒有意義的,因為如果你明白之前程序的設定,就知道哪些事情該做,哪些事情不該做,就不會去犯一些錯誤,但是對于一個程序的開發,不是有一個人完成的。當你理解自己的設定時,別人卻不知道,維護你代碼的人隨意的做一些他們認為應該可以做到的安全的事,卻由于你之前考慮的不周全而使這些行為極度不安全。所以要認真考慮安全性的問題,寫出盡可能完美安全的代碼。
對于第三種情況,聲明非虛函數,目的在于使派生類繼承函數的接口和強制性實現,又由于不應該在派生類中重新聲明和定義基類的非虛函數,所以不會修改非虛函數的實現的。
所以,要理解純虛函數,簡單虛函數和非虛函數聲明和功能上的區別。不用擔心虛函數的效率問題,因為這真的是小問題,所有基類都應該虛函數。一些函數不應該在派生類中重新定義就要將其定義為非虛函數。
37.決不要重新定義繼承而來的非虛函數。
首先,對于重新定義繼承的非虛函數,稱為對這個函數的隱藏,這是一種不常用的東西,正是因為有這個設定,絕不重新定義繼承而來的非虛函數。
這樣做的原因也是很容易理解的,也是多態的優點:
~~~
class A{
public:
void fun() const{
cout<<"Class A"<<endl;
}
};
class B:public A{
public:
void fun() const{
cout<<"Class B"<<endl;
}
};
int main(){
B* b = new B();
A* a = b;
b->fun();
a->fun();
~~~
對于以上代碼,對同一個對象,也就是b指向的對象,當將其轉換為基類指針后,由于其為靜態綁定的,其所指向的函數不同,獲得了不同的結果,而多態時動態綁定,指向的函數通過虛指針指向相同的地址。
結論是,對于類B的對象,其重新定義的函數fun()被調用時,其行為是不確定的,而決定因素與對象本身沒有關系,而取決于指向它的指針的聲明類型,引用也會和指針表現出這樣的異常行為,這樣的行為是不合理的。
而從理論上來考慮,對于公有繼承意味著 ”是一個“,對于B中重新定義了A中的實現后,B就不”是一個“ A了。