博客地址:http://www.cnblogs.com/ronny 轉載請注明出處!
1,繼承可以是單一繼承或多重繼承,每一個繼承連接可以是public、protected或private,也可以是virtual或non-virtual。
2,成員函數的各個選項:virtual或non-virtual或pure-virtual。
3,成員函數和其他語言特性的交互影響:缺省參數值與virtual函數有什麼交互影響?繼承如何影響C++的名稱查找規則?設計選項有如些?如果class的行為需要修改,virtual函數是最佳選擇嗎?
4,public繼承意味著“is-a”。
5,virtual函數意味著“接口必須被繼承”,non-virtual意味著“接口和實現都必須被繼承”。
“public繼承”意味著is-a。適用於base classes身上的每一件事情一定也適合於derived class身上,因為每個derived classes對象也都是一個base classes對象。
上面這種關系聽起來頗為簡單,但有時候你的直覺可能會誤導你,比如,企鵝是一種鳥,這是事實。鳥可以飛,這也是事實,但是如果在用類描述時把企鵝定義為鳥的public繼承則會出問題,比如給基類定義fly函數。
derived classes內的名稱會遮掩base classes內的名稱。在public繼承下從來沒有人希望如些。
// 博客地址:http://www.cnblogs.com/ronny/ 轉載請注明出處! class Base { private: int x; public: virtual void mf1() = 0; virtual void mf1(int); virtual void mf2(); void mf3(); void mf3(double); }; class Derived :public Base { public: virtual void mf1(); //屏蔽了Base裡的mf1(int) void mf3(); // 屏蔽了Base裡的mf3 void mf4(); };
Derived d; int x; d.mf1(); //ok,調用Derived::mf1 d.mf1(x); //error,因為Derived::mf1遮掩Base=::mf1 d.mf2(); //ok,調用Base::mf2 d.mf3(); //ok,調用Derived::mf3 d.mf3(x); // error,因為Derived::mf3遮掩了Base::mf3
Derived內的函數遮掩了Base內所有的同名函數。當然實現中如果是一種public繼承,則是一定需要同理繼承重載函數的,因為必須符合is-a關系。
我們可以用using聲明式達成目標:
class Derived :public Base { public: using Base::mf1; using Base::mf3; virtual void mf1(); void mf3(); void mf4(); };
Derived d; int x; d.mf1(); //ok,仍然調用Derived::mf1 d.mf1(x); //現在沒有問題了 d.mf2(); //ok,調用Base::mf2 d.mf3(); //ok,仍然調用Derived::mf3 d.mf3(x); //沒有問題了
我們還可以使用交換函數來完成這個功能:有時候我們並不想繼承base classes的所有函數。
class Derived :private Base { public: virtual void mf1() // 轉交函數 { Base::mf1(); } };
接口繼承和實現繼承不同。在public繼承之下,derived classes總是繼承base class的接口。
pure virtual函數只具體指定接口繼承
簡樸的(非純)impure virtual函數具體指定接口繼承及缺省實現繼承。
non-virtual函數具體指定接口繼承以及強制性實現繼承
這條款談了兩種設計模式,鼓勵我們多思考的。
以游戲中的人物設計繼承體系為例子,不同的人物有不同的計算健康指數的方法,就叫healthValue函數吧,很自然的就會想到設計一個基類,把healthValue函數設計為virtual的用於繼承。
此條款提供了兩種不同的思路,用於virtual替代方案:
1. Template Method模式,由Non-Virtual Interface手法實現。
具體到以上的例子就是大概如下:
class GameCharacter { public: int healthValue() const { // ... int retVal = doHealthValue(); // ... return retVal; } private: virtual int doHealthValue() const { } };
其實最先提的把healthValue設計為virtual的方法也是Template Method模式,而這裡就是保留healthValue為public,而實現為private virtual(當然這裡是可以protected的),這樣的好處在於其中的前後“...”(省略號),這部分可以進行一些類似檢查、調整的操作,保證doHealthValue()在一個適當的場景下調用。而且子類也可以繼承實現private virtual成員函數。
2. Strategy模式,這一模式令實現方法是個變量,就算是同一個對象在不同的時段也可以有不同的實現方法。但這裡都有個約束,就是對私有成員變量的訪問限制。
a) Function Pointers實現,此種實現手法的約束是只能是函數,而且形式受函數的簽名(參數數量,參數類型,返回類型)的約束。
b) tr1::function實現,擺脫了a)的約束,支持隱式類型轉換,還支持函數對象或者是成員函數(通過std::tr1::bind實現)
c) 古典實現,其實就是對象實體是一類,而實現方法是另一類。
當派生類中重新定義了基類中的non-virtual函數時,如果用指向基類類型的指針(實際指向派生類對象)來訪問該non-virtual函數時,訪問的是基類對象的non-virtual函數。
造成上面行為的原因是,non-virtual是靜態綁定的,區別於virtual函數的動態綁定。
virtual函數系動態綁定的,而缺省參數值是靜態綁定的。
所謂對象的靜態類型:在程序中被聲明時被采用的類型。而對象的動態類型是指“目前所指對象的類型”,也就是說,動態類型可以表現出一個對象將會有什麼行為。
class Shape { public: enum ShapeColor{ Red, Green, Blue }; virtual void draw(ShapeColor color = Red)const = 0; }; class Rectangle :public Shape { public: virtual void draw(ShapeColor color = Green)const = 0; };
上面的代碼中,如果我們定義了一個類型為Shape*的指針指向Rectangle。
Shape* ps=new Rectangle;
ps->draw();
我們的實際願望可能是希望讓draw的默認參數為Green,但是實際上確是Red,因為默認參數是靜態綁定的。
也許你可能會想讓派生類的中virtual函數和base類中保持一致。但是下面的代碼顯然也是不可行的。
class Shape { public: enum ShapeColor{ Red, Green, Blue }; virtual void draw(ShapeColor color = Red)const = 0; }; class Rectangle :public Shape { public: virtual void draw(ShapeColor color = Red)const = 0; };
上面不僅會產生代碼重復,而且帶來了相依性(如果Shape內的缺省參數變了,所有派生類的virtual函數缺省值都要修改)。
聰明的做法是考慮替代設計,如條款35中的一些virutal函數的替代設計,其中之一是NVI手法,令base class內的一個public non-virtual函數調用private virtual函數。
class Shape { public: enum ShapeColor{ Red, Green, Blue }; virtual void draw(ShapeColor color = Red)const { doDraw(color); } private: virtual void doDraw(ShapeColor color)const = 0; }; class Rectangle :public Shape { private: virtual void doDraw(ShapeColor color)const; };
復合或包含意味著has-a。如果我們想設計一個自己的set,我們思考後覺得可以用list來實現它,但是如果我把它設計出list的一個派生類,就會有問題,因為父類的所有行為在派生類都是被允許的,而list允許元素重復,而set則顯然不行,所以set與list之間不符合is-a關系,我們可以把list設計為set的一個成員,即包含關系(has-a)。
private繼承意味著is-implemented-in-terms of(根據某物實現)。它通常比復合的級別低。但是當derived class需要訪問protected base class的成員,或需要重新定義繼承而來的virtual函數時,這麼設計是合理的。
和復合不同,private繼承可以造成empty base最優化。這對致力於“對象尺寸最小化”的程序庫開發者而言,可能很重要。
class Empty{}; class HoldsAnInt { private: int x; Empty e; };
這時候你會發現sizeof(HoldsAnInt)>sizeof(int),因為一些內存對齊的要求。
而如果是:
class HoldsAnInt :private Empty{ private: int x; };
這時候幾乎可以肯定:sizeof(HoldsAnInt)==sizeof(int)。
使用多重繼承就要考慮歧義的問題(成員變量或者成員函數的重名)。
最簡單的情況的解決方案是顯式的調用(諸如item.Base::f()的形式)。
復雜一點的,就可能會出現“鑽石型多重繼承”,以File為例:
class File { ... } class InputFile : public File { ... } class OutputFile : public File { ... } class IOFile : public InputFile, public OutputFile { ... }
這裡的問題是,當File有個filename時,InputFile與OutputFile都是有的,那麼IOFile繼承後就會復制兩次,就有兩個filename,這在邏輯上是不合適的。解決方案就是用virtual繼承:
class File { ... } class InputFile : virtual public File { ... } class OutputFile : virtual public File { ... } class IOFile : public InputFile, public OutputFile { ... }
這樣InputFile與OutputFile共享的數據就會在IOFile中只保留一份了。
但是virtual繼承並不常用,因為:
1. virtual繼承會增加空間與時間的成本。
2. virtual繼承會非常復雜(編寫成本),因為無論是間接還是直接地繼承到的virtual base class都必須承擔這些bases的初始化工作,無論是多少層的繼承都是。
請記住
多重繼承比單一繼承復雜。它可能導致新的歧義性,以及對virtual繼承的需要。
virtual繼承會增加大小、速度、初始化(賦值)復雜度等等成本。如果virtual base classes不帶任何數據,將是最具實用價值的情況。
多重繼承的確有正當用途。其中一個情節涉及“public繼承某個Interface class”和“private繼承某個協助實現的class”的兩相組合。