Effective cpp 讀書筆記7

繼承與面向對象設計

32.確定你的public繼承塑模出is-a關係

  1. “public繼承”以爲is-a。適用於base classes身上的每件事一定也適用於derived classes身上,因爲每個derived class對象也都是base class對象

    • 這一條意思是,注意你在base class定義的函數,因爲公共繼承是is-a的關係,但是這種關係是具體問題具體分析的,不要用常識去理解

33.避免遮掩繼承而來的名稱

  1. derived classes內的名稱會遮掩base classes的名稱。在public繼承下從來沒有人希望如此

  2. 爲了讓被遮掩的名稱重見天日,可使用using聲明式或轉交函數

    • 繼承體系內存在函數遮掩。這種遮掩很奇怪,只要函數名重複,不看形參是否一致,就必然會存在遮掩。(派生類的f(double)會遮掩基類的f())
    • 編譯器名稱查詢:首先看作用域內是否有對應函數,其次看類覆蓋的作用域有沒有,最後看繼承的上級有沒有
    • 兩種方法讓被遮掩的函數重見天日:在對應的derived class的域內(public域)用using聲明;在派生類的其他函數中,顯式調用基類被遮掩的函數

34.區分接口繼承和實現繼承

  1. 接口繼承和實現繼承不同。在public繼承下,derived classes總是繼承base class的接口

  2. pure virtual函數只具體指定接口繼承

  3. 簡樸的impure virtual函數具體指定接口繼承和缺省實現繼承

  4. non-virtual函數具體指定接口繼承以及強制性實現繼承

    • pure virtual函數:只提供接口
    • impure virtual函數:提供接口和缺省實現。這裏,接口和缺省實現應該分開。這裏有2種方法
      – 將這種函數變成pure virtual的形式,同時提供另一個函數作爲缺省實現,在派生類中,暴露這個缺省實現(protected)
      – 將它聲明爲pure virtual並提供實現,在派生類的對應函數中調用基類的缺省實現
    • non-virtual是爲了另derived classes繼承函數的接口和一份強制實現。它代表的意義是不變形凌駕於特異性
    • 程序員2大錯誤:所有函數聲明爲non-virtual(使得derived classes沒有空餘空間進行特化);所有函數聲明爲virtual(立場不堅定)
  5. virtual函數的成本:80-20法則(程序80%的執行時間花在20%的代碼上),意味着你的函數調用有80%是virtual,而不衝擊程雪的效率。當你擔心virtual的成本,請先將精力放在20%的代碼上


35.考慮virtual函數以外的其他選擇(策略模式)

  1. virtual函數的替代方案包括NVI手法以及Strategy設計模式的多種形式。NVI手法自身是一個特殊形式的Template Method設計模式(模板設計模式就是指普通的模板基類)

  2. 將機能從成員函數移到class外部函數,帶來的一個缺點是,非成員函數無法訪問class的non-public成員

  3. tr1::function對象的行爲就像一般函數指針。這樣的對象可接納“與給定的目標籤名式兼容”的所有可調用物

    • non-Virtual Interface替換virtual:基類的virtual函數設定爲private,設計public的函數定義死上下文,並在其中調用virtual(外覆蓋器),這樣,派生類只需要複寫virtual函數,上下文關係都被基類定死
    • Function Pointer替換virtual:函數指針將動態變換的函數部分從class中移出。但是,任何時候,將class內的技能替換爲class外部的等價機能都會存在爭議
    • tr1::function替換virtual:它能覆蓋的形式更廣,比函數指針要好
    • 用傳統的Strategy模式替代:主要是將本繼承體系的virtual定義好,用另一個繼承體系來設定其他函數去調用這個virtual,可能兩個繼承體系之間存在複用

36.絕不重新定義繼承而來的non-virtual函數

  1. 這一條主要注意動態和靜態綁定,virtual肯定是動態綁定。而我們定義指針的時候,是靜態綁定的關係。所以下面的例子,pB靜態綁定爲B*,由於調用函數沒有virtual,則自動調用了繼承的B的對應函數。Reference也會有這種行爲
class B {
    public: void mf();
}

class D: public B {
    public: void mf()
}

D x;
B* pB = &x;
D* pD = &x;
pB->mf();   // 調用B的mf函數
pD->mf();   // 調用D的mf函數

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

  1. 因爲缺省參數值都是靜態綁定,而virtual函數——你唯一應該覆寫的東西卻是動態綁定

    • 靜態類型:在程序中被聲明時所採用的類型
    • 動態類型:目前所指對象的類型
  2. 小結:如果基類的virtual函數有缺省值,因爲是靜態綁定,當你用指針動態綁定對象的時候,儘管能調用對應類型的virtual函數,但這個函數的缺省參數僅與指針的聲明對象的virtual函數有關,與動態綁定的對象無關


39.明智而審慎地使用private繼承

  1. private繼承意味着is-implemented-in-terms-of。它通常比複合的級別低。但是當derived class需要訪問protected base class的成員,或者需要重新定義繼承而來的virtual,這麼設計是合理的

  2. 和複合不同,private繼承可以造成empty base最優化。這對致力於“對象尺度最小化”的程序庫開發者是重要的

    • 繼承關係是private的時候,不在是is-a的關係,編譯器不會自動完成類型轉化(這意味着動態綁定沒有意義)
    • private繼承:只有實現部分被繼承,接口部分略去。如果D以private形式繼承B,則D對象是根據B對象而得,沒有其他任何意義了
    • private和複合:兩者其實表達的意義一樣。儘量用複合,必要時候採用private。必要時候是指:當protected成員需要被調用或virtual函數需要被覆寫或者極端情況
    • 極端情況:爲了達到空間最優化。因爲絕大多數編譯器對空類(empty class)都會默認安插一個char,使得空類大小爲sizeof爲1,如果有類複用了它,可能因爲內存的齊位需要,增大空間。而如果私有繼承,就不會安插char,這就是空白基類最優化(EBO)
    • 如何阻止derived class重新定義virtual函數:放棄基類的繼承,在原本的derived class內定義新的類繼承基類,那麼,後續的派生類繼承原本的這個derived class的時候,無法暴露virtual函數讓它們覆寫

40.明智而審慎地使用多重繼承

  1. 多重繼承比單一繼承複雜。它可能導致新的歧義,以及對virtual繼承的需要

  2. virtual繼承會增加空間、時間、初始化的複雜度。如果virtual base classes不帶任何數據,將是最具使用價值的情況

    • 問題:當MI進入設計,程序可能從一個以上的base class繼承相同的名稱;當存在磚石型多重繼承,C++缺省做法是每個路徑執行復制,存在重複
    • virtual繼承:
      – 當基類通過多條派生路徑被一個派生類繼承時,該派生類只繼承該基類一次
      – C++編譯系統只執行最後的派生類對虛基類的構造函數的調用,而忽略虛基類的其他派生類對虛基類的構造函數的調用
    • virtual繼承用法:
      – 應當在該基類的所有直接派生類中將基類聲明爲虛基類
      – 在最後的派生類中不僅要負責對其直接基類進行初始化,還要負責對虛基類初始化
      – 非必要不要使用virtual base;如果必須使用,儘可能避免在其中放數據
發佈了33 篇原創文章 · 獲贊 21 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章