《Effective C++》筆記

吳尼瑪發表於2017-12-19

這本書屬於“想提高必看之書”,相見恨晚,建議所有C++程式設計師都看看,沒事也可以拿出來翻翻。大家也可以瀏覽下面的筆記看看是不是所有條款都瞭解了。

我已經將這個筆記的思維導圖和有用的程式碼片段上傳到我的GitHub上了,歡迎大家下載。

讓自己習慣C++

  • 視C++為一個語言聯邦
    • C(C++的基礎C語言的部分)
    • Object-Oriented C++(物件導向)
    • Template C++(泛型程式設計)
    • STL(標準庫)
  • C++高效程式設計守則視狀況而變化,取決於你使用C++的哪一部分。

儘量以const,enum,inline替換#define

  • 對於單純常量,最好以const物件或enums替換#define。
  • 對於形似函式的巨集,最好改用inline函式替換#define。

儘可能使用const

  • 將某些東西宣告為const可幫助編譯器偵測出錯誤用法。const可被施加於任何作用域內的物件、函式引數、函式返回型別、成員函式本體。
  • 編譯器強制實施bitwise constness,但你編寫程式時應該使用“概念上的常量性”(conceptual constness)。
  • 當const和non-const成員函式有著實質等價的實現時,令non-const版本呼叫const版本可避免程式碼重複。

確定物件被使用前已被初始化

  • 為內建型物件進行手工初始化,因為C++不保證初始化它們。
  • 建構函式最好使用成員初值列,而不要在建構函式本體內使用賦值操作。初值列列出的成員變數,其排列次序應該和它們在class中的宣告次序相同。
  • 為免除“跨編譯單元之初始化次序”問題,請以local static物件替換non-local static物件

瞭解C++默默編寫並呼叫哪些函式

  • 編譯器可以暗自為class建立default建構函式,copy建構函式,copy assignment操作符,以及解構函式。

若不想使用編譯器自動生成的函式,就該明確拒絕

  • 為駁回編譯器自動提供的機能,可將相應的成員函式宣告為private並且不予實現。使用想Uncopyable這樣的base class也是一種做法。

為多型基類宣告virtual解構函式

  • 只有當class內含至少一個virtual函式,才為它宣告virtual解構函式。
  • 為你希望它成為抽象的那個class宣告一個pure virtual解構函式,且必須定義。

別讓異常逃離解構函式

  • 解構函式絕對不要吐出異常。如果一個被解構函式呼叫的函式可能丟擲異常,解構函式應該捕捉任何異常,然後吞下它們或者結束程式。
  • 如果客戶需要對某個操作函式執行期間丟擲的異常做出反應,那麼class應該提供一個普通函式執行該操作。

絕不在構造和析構過程中呼叫virtual函式

  • 因為在base class構造期間,virtual函式不是virtual函式。

令operator=返回一個reference to *this

在operator=中處理“自我賦值”

  • 確保當物件自我賦值時operator=有良好行為。其中技術包括比較“來源物件”和“目標物件”的地址、精心周到的語句順序、以及copy-and-swap。
  • 確定任何函式如果操作一個以上的物件,而其中多個物件是同一個物件時,其行為仍然正確。

複製物件時勿忘其每一個成分

  • Copying函式應該確保複製“物件內的所有成員變數”及“所有base class成分”。
  • 不要嘗試以某個copying函式實現另一個copying函式。應該將共同機能放進第三個函式中,並由兩個copying函式共同呼叫。

以物件管理資源

  • shared_ptr
  • unique_ptr

在資源管理類中小心copying行為

  • 複製RAII物件必須一併複製它所管理的資源,所以資源的copying行為決定RAII物件的copying行為。
  • 普遍而常見的RAII class copying行為是:抑制copying,施行引用計數法。不過其他行為也都可能被實現。

在資源管理類中提供對原始資源的訪問

  • APIs往往要求訪問原始資源,所以每一個RAII class應該提供一個“取得其所管理之資源”的辦法。
  • 對原始資源的訪問可能經由顯式轉換或隱式轉換。一般而言顯式轉換比較安全,但隱式轉換對客戶比較方便。

成對使用new和delete時要採取相同形式

以獨立語句將newed物件置於智慧指標

  • 如果不這樣做,一旦異常被丟擲,有可能導致難以察覺的資源洩露。

讓介面容易被正確使用,不易被誤用

  • 好的介面很容易被正確使用,不容易被誤用。你應該在你的所有介面中努力達成這些性質。
  • “促進正確使用”的辦法包括介面的一致性,以及與內建型別的行為相容。
  • “阻止誤用”的辦法包括建立新型別、限制型別上的操作,束縛物件值,以及消除客戶的資源管理責任。

設計class猶如設計type

寧以pass-by-reference-to-const替換pass-by-value

  • 儘量以pass-by-reference-to-const替換pass-by-value。前者通常比較高效,並可避免切割問題。
  • 以上規則並不適用於內建型別,以及STL的迭代器和函式物件。對它們而言,pass-by-value往往比較適當。

必須返回物件時,別妄想返回其reference

  • 絕不要返回pointer或reference指向一個local stack物件,或返回reference指向一個heap-allocated物件,或返回pointer或reference指向一個local static物件而又可能同時需要對個這樣的物件(單例除外)。

將成員變數宣告為private

  • 切記將成員變數宣告為private。這可賦予客戶訪問資料的一致性、可細微劃分訪問控制、允許約束條件獲得保證,並提供class作者以充分的實現彈性。
  • protect並不比public更具封裝性。

寧以non-member、non-friend替換member函式

  • 將所有便利函式放在多個標頭檔案內但隸屬同一個名稱空間,意味著客戶可以輕鬆擴充套件這一組便利函式。他們需要做的就是新增更多non-member non-friend函式到此名稱空間內。
  • 這樣做可以增加封裝性、包裹彈性和技能擴充性。

若所有引數皆需型別轉換,請為此採用non-member函式

考慮寫出一個不拋異常的swap函式

  • 當std::swap對你的型別效率不高時,提供一個swap成員函式,並確定這個函式不丟擲異常。
  • 如果你提供一個member swap,也該提供一個non-member swap用來呼叫前者。對於classes(而非templates),也請特化std::swap。

儘可能延後變數定義式的出現時間

  • 你不只應該延後變數的定義,直到非得使用該變數的前一刻為止,甚至應該嘗試延後這份定義直到能夠給它初值實參為止。

儘量少做轉型動作

  • 如果可以,儘量避免轉型,特別是在注重效率的程式碼中避免dynamic_casts。如果有個設計需要轉型動作,試著發展無需轉型的替代設計。
  • 如果轉型是必要的,試著將它隱藏於某個函式背後。客戶隨後可以呼叫該函式,而不需將轉型放進他們自己的程式碼內。
  • 寧可使用C++_style(新式)轉型,不要使用舊式轉型。前者很容易辨識出來,而且也比較有著分門別類的職掌。

避免返回handles指向物件內部成分

  • 避免返回handles(包括references、指標、迭代器)指向物件內部。遵守這個條款可增加封裝性,幫助const成員函式的行為像個const,並將發生“虛吊號碼牌”(dangling handles)的可能性降至最低。

為“異常安全”而努力是值得的

  • 異常安全函式即使發生異常也不會洩露資源或允許任何資料結構敗壞。這樣的函式區分為三種可能的保證:基本型、強烈型、不拋異常型。
  • “強烈保證”往往能夠以copy-and-swap實現出來,但“強烈保證”並非對所有函式都可實現或具備現實意義。
  • 函式提供的“異常安全保證”通常最高只等於其所呼叫之各個函式的“異常安全保證”中最弱者。

透徹瞭解inlining的裡裡外外

  • 平均而言一個程式往往將80%的執行時間花費在20%的程式碼上頭。作為一個軟體開發者,你的目標是找出這可以有效增程式序整體銷量的20%程式碼,然後將它inlining或竭盡所能地將它瘦身。
  • 將大多數inlining限制在小型、被頻繁呼叫的函式身上。這可使日後的除錯過程和二進位制升級更容易,也可使潛在的程式碼膨脹問題最小化,使程式的速度提升機會最大化。
  • 不要只是因為function templates出現在標頭檔案,就將它們宣告為inline。

將檔案間的編譯依存關係降至最低

  • 支援“編譯依存性最小化”的一半構想是:相依與宣告式,不要相依與定義式。基於此構想的兩個手段是Handle classes和Interface classes。
  • 程式庫標頭檔案應該以”完全且僅有宣告式“的形式存在。這種做法不論是否涉及templates都適用。

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

  • “public繼承”以為is-a。適用於base classes 身上的每一件事情一定也適用於derived classes身上,因為每一個derived class物件也都是一個base class物件。

避免遮掩繼承而來的名稱

  • derived classes內的名稱會遮掩base classes內的名稱。在public繼承下從來沒有人希望如此。
  • 為了讓被遮掩的名稱再見天日,可使用using宣告式或轉角函式。

區分介面繼承和實現繼承

  • pure virtual函式只具體指定介面繼承。
  • 簡樸的(非純)impure virtual函式具體指定介面繼承及預設實現繼承。
  • non-virtual函式具體指定介面繼承以及強制性實現繼承。

考慮virtual函式以外的其他選擇

  • 使用non-virtual interface(NVI)手法
  • 將virtual函式替換為“函式指標成員變數”
  • 以tr1::function成員變數替換virtual函式
  • 將繼承體系內的virtual函式替換為另一個繼承體系內的virtual函式

絕不重新定義繼承而來的non—virtual函式

絕不重新定義繼承而來的預設引數值

  • 因為預設引數值都是靜態繫結,而virtual函式——你唯一應該覆寫的東西——卻是動態繫結。

通過複合塑模出has-a或“根據某物實現出”

  • 複合(composition)的意義和public繼承完全不同。
  • 在應用域,複合意味has-a(有一個)。在實現域。複合意味is-implemented-in-terms-of(根據某物實現出)。

明智而審慎地使用private繼承

  • Private繼承意味is-implemented-in-terms of(根據某物實現出)。它通常比複合的級別低。但是當derived class需要訪問protected base class的成員,或需要重新定義繼承而來的virtual函式時,這麼設計師合理的。
  • 和複合不同,private繼承可以造成empty base最優化。這對致力於“物件尺寸最小化”的程式庫開發者而言,可能很重要。

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

  • 多重繼承比單一繼承複雜,它可能導致新的歧義性,以及對virtual繼承的需要。
  • virtual繼承會增加大小、速度、初始化(及賦值)複雜度等等成本。如果virtual base classes不帶任何資料,殭屍最具有實用價值的情況。
  • 多重繼承的確有正當用途。其中一個情節設計“public繼承某個Interface class”和“private繼承某個協助實現的class”的兩相組合。

瞭解隱式介面和編譯器多型

  • 對template引數而言,介面是隱式的,奠基於有效表示式。多型則是通過template據具現化和函式過載解析發生於編譯器。

瞭解typename的雙重意義

  • 宣告template引數時,字首關鍵字class和typename可互換。
  • 請使用關鍵字typename標識巢狀從屬型別名稱:但不得在base class lists(基類列)或member initialization list(成員初值列)內以它作為base class修飾符。

學習處理模板化基類內的名稱

  • 可在derived class templates內通過“this->”指涉base class template內的成員名稱,或藉由一個明白寫出的“base class資格修飾符”完成。

將於引數無關的程式碼抽離templates

  • templates生成多個classes和多個函式,所以任何template程式碼都以應該與某個造成膨脹的template引數產生相依關係。
  • 因費型別模板引數而造成的程式碼膨脹,往往可消除,做法是以函式引數或class成員變數替換template引數。
  • 因型別引數而造成的程式碼膨脹,往往可降低,做法是讓帶有完全相同二進位制表述的具現型別共享實現碼。

運用成員函式模板接受所有相容型別

  • 請使用member function template生成“可接受所有相容型別”的函式。
  • 如果你宣告member templates用於“泛化copy構造”或“泛化assignment操作”,你還是需要宣告正常的copy建構函式和copy assignment操作符。

需要型別轉換時請為模板 定義非成員函式

  • 當我們編寫一個class template,而它所提供之“與此template相關的”函式支援“所有引數之隱身型別轉換”時,請將那些函式定義為“class template內部的friend函式”。

請使用trait classes表現型別資訊

  • Traits classes使得“型別相關資訊”在編譯期可用。它們以templates和“templates特化”完成實現。
  • 整合過載技術後,trait classes有可能在編譯期對型別執行if...else測試。

認識template超程式設計

  • Template metaprogramming(TMP,模板超程式設計)可將工作由執行期移往編譯期,因而得以實現早起錯誤偵測和更高的執行效率。
  • TMP可被用來生成“基於政策選擇組合”的客戶定製程式碼,也可用來避免生成對某些特殊型別並不適合的程式碼。

瞭解new-handler的行為

  • set_new_handler允許客戶指定一個函式,在記憶體分配無法獲得滿足時被呼叫。
  • Nothrow new是一個頗為侷限的工具,因為它只適合於記憶體分配;後繼的建構函式呼叫還是可能丟擲異常。

瞭解new和delete的合理替換時機

  • 有許多理由需要寫個自定的new和delete,包括改善效能、對heap運用錯誤進行除錯、收集heap使用資訊。

編寫new和delete時需固守常規

  • operator new應該內含一個無窮迴圈,並在其中嘗試分配記憶體,如果它無法滿足記憶體需求,就該呼叫new-handler。它也應該有能力處理0bytes申請。Class專屬版本則應該處理“比正確大小更大的(錯誤)申請”。
  • operator delete應該在收到null指標時不做任何事。Class專屬版本則還應該處理“比正確大小更大的(錯誤)申請”。

寫了placement new也要寫placement delete

  • 當你寫一個placement operator new,請確定也寫出了對應的placement operator delete。如果沒有這樣做,你的程式可能會發生隱微而時斷時續的記憶體洩露。
  • 當你宣告placement new和placement delete,請確定不要無意識(非故意)地遮掩了它們的正常版本。

不要輕忽編譯器的警告

讓自己熟悉包括TR1在內的標準程式庫

讓自己熟悉Boost

相關文章