1. 程式人生 > >Effective C++筆記

Effective C++筆記

改善程式與設計的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. 讓自己熟