這本書屬于“想提高必看之書”,相見恨晚,建議所有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,請確定不要無意識(非故意)地遮掩了它們的正常版本。