Effective C++筆記 -- 改善程式與設計的55個具體做法
改善程式與設計的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. 為“異常安全”而努力是值得的
- 異常安全函數提供一下三個保證之一
- 基本承諾:如果異常被抛出,程式内的任何事物仍然保持在有效狀态下
- 強烈保證:如果異常被抛出,程式狀态不改變
- 不抛擲保證:承諾絕不抛出異常,因為它們總是能夠完成它們原先承諾的功能
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