effective C++ 讀書筆記(中)

條款33:避免遮掩繼承而來的名稱

      看下面的例子:

 

      以作用域爲基礎的“名稱遮掩規則”並沒有改變,因此base class內所有名爲mf1和mf3的函數都被derived class內的mf1和mf3函數遮掩掉了。從名稱查找觀點來看,Base::mf1和Base::mf3不再被Derived繼承!只有在當前域找不到名稱的成員纔會再到基類去找。。。

 

      如果想繼承基類的重載函數,可在Base類的聲明裏加上:

 

 

條款34:區分接口繼承和實現繼承

      有時候,你希望繼承類只繼承成員函數的接口;有時候你又會希望繼承類同時繼承函數的接口和實現,但又希望能夠覆寫它們所繼承的實現;有時候你希望繼承類同時繼承函數的接口和實現,並且不允許覆寫任何東西。

      Scott提到:

      1.成員函數的接口總是會被繼承。

      2.聲明一個pure virtual函數的目的是爲了讓derived classes只繼承函數接口。

      3.聲明簡樸的impure virtual函數的目的,是讓derived classes繼承該函數的接口和缺省實現。

      4.聲明non-virtual函數的目的是爲了令derived classes繼承函數的接口及一份強制性實現。

 

條款35:考慮virtual函數以外的其他選擇

      藉由Function Pointers實現Strategy模式

       

      藉由tr1::function完成Strategy模式

      如果我們不再使用函數指針,而是改用一個類型爲tr1::function的對象,與函數指針相關的一些約束就沒有了。這樣的對象可持有(保存)任何可調用物(也就是函數指針、函數對象、或成員函數指針),只要其簽名式兼容於需求端。tr1::function的定義與函數指針類似,但是基於tr1的其它輔助功能將使該tr1::function有別於函數指針,下面詳細介紹這個東西。。

      先來看基本功能代碼:

       

      初看起來,和函數指針沒有什麼區別,但看看下面的代碼:

 

      注意上面的ebg2,我們希望用GameLevel類的成員函數health來計算ebg2的健康指數。如果我們使用它,就必須先有一個GameLevel類的對象。因此這裏將currentLevel綁定爲GameLevel對象,讓它在“每次GameLevel::health被調用以計算ebg2的健康”時被使用。那正是tr1::bind的作爲:它指出ebg2的健康計算函數應該總是以currentLevel作爲GameLevel對象。bind函數具體細節可以上網搜索。

      古典的Strategy模式

      將繼承體系內的virtual函數替換爲另一個體系內的virtual函數。這是Strategy設計模式的傳統實現手法。

 

條款37:絕不重新定義繼承而來的缺省參數值

      對象的所謂靜態類型,就是它在程序中被聲明時所採用的類型。考慮一下的class繼承體系:

 

      現在考慮這些指針:

 

      本例中ps,pc和pr都被聲明爲pointer-to-Shape類型,所以它們都以爲它爲靜態類型。注意,不論它們真正指向什麼,它們的靜態類型都是Shape*。

      對象的所謂動態類型則是指“目前所指對象的類型”。也就是說,動態類型可以表現出一個對象將會有什麼行爲。以上例而言,pc的動態類型是Circle*,pr的動態類型是Rectangle*。ps沒有動態類型,因爲它尚未指向任何對象。

      動態類型一如其名稱所示,可在程序執行過程中改變(通常是經由賦值動作):

 

      virtual函數是動態綁定,而缺省參數值確實靜態綁定。意思是你可能會在“調用一個定義與derived class內的virtual函數”的同時,卻使用base class爲它所制定的缺省參數值。

 

條款39:明智而審慎地使用private繼承

      如果classes之間的集成關係是private,編譯器不會自動將一個derived class對象轉換爲一個base class對象。這和public繼承的情況不同。如果讓class D以private形式繼承class B,你的用意是爲了採用class B內已經備妥的某些特性,不是因爲B對象和D對象存在有任何觀念上的關係。private繼承意味着只有實現部分被繼承,接口部分應略去。如果D以private形式繼承B,意思是D對象根據B對象實現而得,再沒有其他意涵了。private繼承在軟件“設計”層面上沒有意義,其意義只及於軟件實現層面。

      下面是兩種對於Timer類的onTick函數的使用,一種使用private繼承,一種採用public繼承加複合:

 

      Scott介紹了兩個採用第二種方案的理由,第二個是:可以將Widget的編譯依存性降至最低。如果Widget繼承Timer,當Widget被編譯時Timer的定義必須可見,所以定義Widget的那個文件恐怕必須#include Timer.h。但如果WidgetTimer移出Widget之外而Widget內含指針指向一個WidgetTimer,Widget可以只帶着一個簡單的WidgetTimer聲明式,不再需要#include任何與Timer有關的東西。

      使用private繼承有一種極端情況:當基類爲空,而又要考慮節約空間時,以private繼承將不會增加繼承類的大小,而用複合的方式則不是這樣,空的基類也要佔用一定空間。

 

條款40:明智而審慎地使用多重繼承

      多重繼承的意思是繼承一個以上的基類,但這些基類可能又有相同的基類,這樣就會形成所謂的“磚石型多重繼承”。如果這樣,C++將會在繼承類裏複製兩次最上層的基類。如果不想出現複製多次的情形,那麼就必須採用虛繼承

      虛繼承在本書中是不被鼓勵的行爲,因爲使用虛繼承可能使派生類對象的體積比非虛繼承大,而且虛繼承的基類的初始化工作是由最下層的派生類負責的,這就是說,虛基類不會自己初始化。

      書中還有一個巧妙利用多重繼承實現代碼重用的例子,感興趣的可以看看該書195頁到198頁。這裏就不囉嗦了。

 

條款41:瞭解隱式接口和編譯器多態

      面向對象的世界裏,顯示接口和運行期多態占主導地位,而在模板和泛型編程的世界,隱式接口和編譯器多態反而變成更加重要的概念。模板中的對象必須實現的接口是由模板的行爲來決定的,模板中的一系列表達式便是模板對象所必需實現的隱式接口。而涉及到對象的任何函數調用,例如operator>和operator!=,有可能造成模板具現化,使這些調用得以成功。這樣的具體行爲發生在編譯期。“以不同的模板參數具現化函數模板”會導致調用不同的函數,這便是所謂的編譯期多態。

      PS:所謂顯示接口和隱式接口的區別,其實就是顯示接口是由直接的定義,如成員函數,成員變量等。而隱式接口則有與對象有關的有效表達式推斷出對象所支持的接口,比如若對象調用了size函數,那麼該對象必然支持size函數的接口。(此書把它說的挺玄乎的。)

      加諸於模板參數身上的隱式接口,就像加諸於類對象身上的顯示接口一樣在編譯期完成檢查。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章