Effective C++筆記

zhhfan發表於2021-03-03

改善程式與設計的55個具體做法

1. 視C++為一個語言聯邦

  • C++並不是一個帶有一組守則的一體語言:它是從四個次語言(C, Object-Oriented C++, Template C++ 以及 STL)組成的聯邦政府,每個語言都有自己的規約。
  • C++的高效程式設計守則視狀況而變化,取決於你使用C++的哪一部分

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

  • 即寧可以編譯器替換前處理器
  • 對於單純常量,最好以const物件或enums替換#define
  • 對於形似函式的巨集,最好改用inline函式替換#define

3. 儘可能使用const

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

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

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

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

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

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

  • 為駁回編譯器自動提供的機能,可將相應的成員函式宣告為private並且不予實現

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

  • 帶有多型性質的基類應該宣告一個virtual解構函式。如果class帶有任何virtual函式,它就應該擁有一個virtual解構函式
  • 類的設計如果不是作為基類使用,或不是為了具備多型性,就不該宣告virtual解構函式

8. 別讓異常逃離解構函式

  • 只要解構函式突出異常,程式就可能過早結束或出現不明確行為
  • 解構函式絕對不要吐出異常。如果一個被解構函式呼叫的函式可能丟擲異常,解構函式應該捕獲任何異常,然後吞下它們(不傳播)或者結束程式
  • 如果客戶需要對某個操作函式執行期間丟擲的異常做出反應,那麼class應該提供一個普通函式(而非在解構函式中)執行該操作

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

  • 在構造和析構期間不要呼叫virtual函式,因為這類呼叫從不下降至派生類

10. 令operator=返回一個reference to *this

  • 為了實現連鎖賦值

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

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

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

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

13. 以物件管理資源

  • 獲得資源後立刻放進管理物件
  • 管理物件運用解構函式確保資源被釋放
  • 為防止資源洩露,請使用RAII物件,它們在建構函式中獲得資源並在解構函式中釋放資源
  • 兩個常被使用的RAII classes分別是tr1::shared_ptr和auto_ptr。前者通常是較佳選擇,因為其copy行為比較直觀。若選擇auto_ptr,複製動作會使它(被複制物)指向null

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

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

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

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

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

  • 如果你在new表示式中使用[],必須在相應的delete表示式中也使用[]。如果在new表示式中不使用,在delete中也不要使用

17. 以獨立語句將new物件置入智慧指標

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

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

  • 好的介面很容易被正確使用,不容易被誤用。你應該在你的所有介面中努力達成在這些性質
  • “促進正確使用”的辦法包括介面的一致性,以及與內建型別的行為相容
  • “阻止誤用”的辦法包括建立新型別、限制型別上的操作,束縛物件值,以及消除客戶的資源管理責任
  • tr1::shared_ptr支援定製型刪除器。這可防範DLL問題,可被用來自動解除互斥鎖等等

19. 設計class猶如設計type

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

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

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

22. 將成員變數宣告為private

  • 這可賦予客戶訪問資料的一致性、可細微劃分訪問控制,允諾約束條件獲得保證,並提供class作者以充分的實現彈性
  • protected並不比public更具有封裝性

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

  • 這樣做可以增加封裝性、包裹彈性和機能擴充性

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

  • 即,如果你需要為某個函式(包括被this指標所指的那個隱喻引數)進行型別轉換,那麼這個函式必須是個non-member

25. 考慮寫出一個不丟擲異常的swap函式

  • 當std:swap對你的型別效率不高時,提供一個swap成員函式,並確定這個函式不丟擲異常
  • 如果你提供一個member swap,也該提供一個non-member swap用來呼叫前者。對於classes,也請特化std:swap
  • 呼叫swap時應針對std::swap使用using宣告式,然後呼叫swap並且不帶任何“名稱空間資格修飾”
  • 為“使用者定義型別”進行std templates全特化是好的,但千萬不要嘗試在std內加入某些對std而言全新的東西

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

  • 這樣可增加程式的清晰度並改善程式效率

27. 儘量少做轉型動作

  • const_cast通常被用來將物件的常量性剔除
  • dynamic_cast主要用來執行“安全向下轉型”,也就是用來判斷某物件是否歸屬繼承體系中的某個型別
  • reinterpret_cast意圖執行低階轉型,實際動作及結果可能取決於編譯器,這也就表示它不可移植
  • static_cast用來強迫隱式轉換,但它無法將const轉為non-const
  • 如果可以,儘量避免轉型,特別是在注重效率的程式碼中避免dynamic_casts。如果有個設計需要轉型動作,試著發展無需轉型的替代設計
  • 如果轉型是必要的,試著將它隱藏於某個函式背後。客戶可以呼叫該函式,而不需要將轉型放進他們自己的程式碼內
  • 寧可以C++-style轉型,不要使用舊式轉型

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

  • 該條款可增加封裝性,幫助const成員函式的行為像個const,並將發生 dangling handles的可能性降至最低

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

  • 異常安全函式提供一下三個保證之一
  1. 基本承諾:如果異常被丟擲,程式內的任何事物仍然保持在有效狀態下
  2. 強烈保證:如果異常被丟擲,程式狀態不改變
  3. 不拋擲保證:承諾絕不丟擲異常,因為它們總是能夠完成它們原先承諾的功能

30. 徹底瞭解inlining的裡裡外外

  • 將inlining限制在小型、被頻繁呼叫的函式身上
  • 不要只因為function templates出現在標頭檔案,就將它們宣告為inline

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

  • 如果使用object references或object pointers可以完成任務,就不要使用objects
  • 如果能夠,儘量以class宣告式替換class定義式
  • 為宣告式和定義式提供不同的標頭檔案
  • 程式庫標頭檔案應該以“完全且僅有宣告式”的形式存在

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

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

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

34. 區分介面繼承和實現繼承

  • 介面繼承和實現繼承不同。在public繼承之下,派生類總是繼承基類的介面
  • pure virtual函式只具體指定介面繼承
  • impure virtual函式具體指定介面繼承及預設實現繼承
  • non-virtual函式具體指定介面繼承以及強制性實現繼承

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

  • virtual函式的替代方案包括NVI手法以及Strategy設計模式的多種形式。NVI手法自身是一個特殊形式的Template Method設計模式
  • 將機能從成員函式轉移到class外部函式,帶來的一個缺點是,非成員函式無法訪問class的non-public成員
  • tr1::function物件的行為就像一般函式指標。這樣的物件可接納“與給定的目標標籤格式相容”的所有可呼叫物

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

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

  • 因為預設數值都是靜態繫結,而virtual函式確實動態繫結

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

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

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

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

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

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

  • class和templates都支援介面和多型
  • 對classes而言介面是顯示的,以函式簽名為中心,多型則是通過virtual函式發生於執行期
  • 對template引數而言,介面是隱式的,奠基於有效表示式。多型則是通過template具現化和函式過載解析發生於編譯期

42. 瞭解typename的雙重意義

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

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

44. 將與引數無關的程式碼抽離templates

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

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

  • 如果你宣告member templates用於泛化copy構造或泛化assignment操作,你還需要宣告正常的copy建構函式和copy assignment操作符

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

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

47. 請使用traits classes表現型別資訊

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

48. 認識template超程式設計

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

49.瞭解new-handler的行為

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

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

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

51. 編寫new和delete時需固守常規

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

52. 寫了placement new也要寫placement delete

  • 如果沒有這樣做,你的程式可能會發生隱微而時斷時續的記憶體洩露
  • 當宣告placement new和placement delete時,不要無意識地遮掩它們的正常版本

53. 不要輕忽編譯器的警告資訊

54.讓自己熟悉包括TR1在內的標準程式庫(Boost)

55. 讓自己熟悉Boost

相關文章