1. 程式人生 > 實用技巧 >Effective C++

Effective C++

讓自己習慣C++

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

  • C:包括區塊 blocks,語句 statements,預處理 preprocessor,內建資料型別 build-in data types,陣列 arrays,指標 pointers 等。
  • C++:包括類 classes,封裝 encapsulation,繼承 inheritance,多型 polymorphism,virtual 函式等。
  • Template C++:泛型程式設計 generic programming。
  • STL:標準模板庫 standard template library。

2. 儘量使用constenuminline
替換#define

  • 對於常量,最好使用 const 或者 enum。(#define直接替換導致名稱沒有被編譯器看到,#define不重視作用域)
  • 對於函式,最好使用 inline 函式替代#define函式巨集(#define巨集函式容易出問題)。

3. 儘可能使用const

  • 令函式返回一個常量值,可以預防無意義的賦值動作
  • const成員函式:
  1. const物件只能訪問const成員函式,而非const物件可以訪問任意的成員函式
  2. const成員函式不能修改物件的資料成員,const物件的成員變數不可以修改(mutable修飾的資料成員除外)

  另外:  

  兩個成員函式如果只是常量性不同,是可以被過載的

  當const和non-const成員函式有著實質等價的實現時,令non-const版本呼叫const版本避免程式碼重複

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

  • 為內建型物件進行手工初始化;內建型別以外,建構函式負責初始化責任
  • 建構函式最好使用成員初值列 ,而不使用賦值操作 ;最好總是以宣告次序為其次序
  • 不同編譯單元的non-local static物件初始化相對次序並無明確定義,以local物件替換得以免除問題

構造、析構、賦值運算

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

  • 如果自己不宣告, 編譯器就會暗自為class建立default建構函式copy建構函式copy assignment操作符
    ,以及解構函式

  注:

  預設copy建構函式只是進行簡單的bits拷貝
  base class如果把copy建構函式或copy assignment操作符設定為private,derived class將拒絕生成copy建構函式或copy assignment操作符

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

  • 將相應的成員函式宣告為private,並不予實現即可
  • 或者使用delete關鍵字

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

  • 為了避免在多型情況下,通過一個基類指標去delete一個子類物件時,由於解構函式不是虛擬函式而發生錯誤
  • 如果一個基類可能有多型子類,那麼就該宣告一個虛解構函式。
  • 如果一個類有任何虛擬函式,那麼它就應該有虛解構函式。
  • 如果一個類不被用來做基類,或者不是為了多型,那麼就不該宣告虛解構函式。

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

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

  注:

  1. 如果解構函式丟擲異常,則異常點之後的程式不會執行,如果解構函式在異常點之後執行了某些必要的動作比如釋放某些資源,則這些動作不會執行,會造成諸如資源洩漏的問題
  2. 通常異常發生時,c++的機制會呼叫已經構造物件的解構函式來釋放資源,此時若解構函式本身也丟擲異常,則前一個異常尚未處理,又有新的異常,會造成程式崩潰的問題

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

  • 由於無法使用virtual函式從base classes向下呼叫,可以令derived classes將必要的構造資訊向上傳遞至base class建構函式替換 

  注:base class構造期間virtual函式絕不會下降到derived classes階層,原因有二

  1. base class構造或解構函式執行時derived class的成員變數尚未初始化,如果呼叫的virtual函式下降到derived class階層,必定導致使用的成員變數未初始化
  2. 在derived class的base class構造期間,物件的型別是base class,不會成為一個derived class物件

10.令operator=(包括+=,-=等) 返回一個reference to *this

  • 為了實現“連鎖賦值”等

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

  • 由於變數有別名的存在(多個指標或引用只想一個物件),所以可能出現自我賦值的情況。應對方法有三個:
  1. if(this == &rhs) return *this; 新增判斷
  2. 先建立一個temp物件指向本物件,然後令本物件複製目標物件(建立一個副本),然後刪除temp物件(原本物件)。ClassName *temp = this.data;data = new ClassName(*rhs.data);delete temp;
  3. 使用copy and swap技術。先建立一個temp物件指向本物件,然後令本物件複製目標物件,然後刪除temp物件(原本物件)。ClassName temp(rhs);swap(temp);

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

  • 複製所有的local成員變數以及所有base class成分
  • 不要嘗試以一個copying函式實現另一個copying函式。應將共同機能放進第三個函式中並由它們共同呼叫

資源管理

13.以物件管理資源

  • 為了防止資源洩漏,請使用RAII物件,在建構函式裡面獲得資源,並在解構函式裡面釋放資源
  • 使用智慧指標(如shared_ptr,unique_ptr)來管理資源類,避免你忘記 delete 資源類。

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

  • 一般資源管理類複製時可以選擇以下做法:
  1. 禁止複製(複製不合理)
  2. 引用計數法”(使用shared_ptr指定“刪除器”阻止引用次數為0時的刪除行為
  3. 複製底層資源(“深度拷貝”)
  4. 轉移底部資源的擁有權(unique_ptr)

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

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

16.成對使用new和delete要採用相同的格式

  • new和delete對應,new[ ]和delete[ ]對應

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

  • 在一個語句中編譯器擁有重新排列操作的自由,如此一來可能被異常干擾,就很難察覺發生了資源洩露

設計與宣告

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

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

19.設計class猶如設計type

  • 在設計class時,要考慮一系列的問題,包括
  1. 物件的建立和銷燬(構造、析構)
  2. 物件的初始化與賦值(構造、賦值操作符)
  3. 複製操作(複製構造)
  4. 合法值(約束條件)
  5. 繼承體系(注意虛擬函式)
  6. 支援的型別轉換(顯示轉換、型別轉換操作符)
  7. 成員函式和成員變數的可見範圍(public/protected/private)
  8. 是否用模板就能實現?

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

  • 儘量以pass-by-reference-to-const替換pass-by-value,比較高效,並可避免切割問題
  • 對於內建型別,以及STL的迭代器和函式物件pass-by-value往往更高效

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

  • 絕不要返回pointer或reference指向一個local stack物件(在函式退出前被銷燬)
  • 不要返回pointer或reference指向一個heap物件(使用者不知道如何delete)
  • 不要返回pointer或者reference指向local static物件而有可能需要多個這樣的物件(同一行不能呼叫多次該函式,static只有一份)

22.將成員變數申明為private

  • 切記將成員變數申明為private,這可具有語法的一致性、更精確的訪問控制、封裝、提供class作者充分的實現彈性等優點
  • protected並不比public更有封裝性

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

  • 因為這種函式位於函式之外,不能訪問類的private成員變數和函式,保證了封裝性(沒有增加可以看到內部資料的函式量)
  • 此外,這些函式只要位於同一個名稱空間內,就可以被拆分為多個不同的標頭檔案,客戶可以按需引入標頭檔案來獲得這些函式,而類是無法拆分的(子類繼承與此需求不同),因此這種做法有更好的擴充性

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

  • 只有當引數被列於引數列內,這個引數才是隱式型別轉換的合格參與者;this物件(隱喻引數)絕不是隱式型別轉換的合格參與者
  • menber函式的反面是non-member函式,不是friend函式

  注:  

  舉個例子,你想為一個有理數類實現乘法函式,支援與int型別的乘積,可以,因為傳參int進去後會呼叫建構函式隱式轉換為有理數型別,同時你想滿足交換律,這時就會報錯,因為int型別並沒有一個函式用來支援你的有理數類做引數的乘法運算。解決方案是將該乘法運算函式作為一個非成員函式,傳兩個引數進去,這樣不管你的int放在前面還是後面,都能作為引數被轉換型別了。但是,非成員函式不代表就一定成為友元函式,能夠通過public函式呼叫完成功能的,就不該設為友元函式,避免權力過大造成麻煩。

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

  • 當std::swap對自定義型別效率不高時(例如深拷貝),提供一個swap成員函式,並確定不會丟擲異常
  • 如果提供一個member swap,也該提供一個non-member swap用來呼叫前者 (對class而言,需特化std::swap;對class template而言,新增一個過載模板到非std名稱空間內)
  • 不可以新增新的東西到std內
  • 呼叫swap時應該針對std::swap使用using宣告式,然後呼叫swap不帶任何”名稱空間修飾”

實現

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

  • 不只應該延後變數定義直到非得使用該變數的前一刻為止,甚至應該嘗試延後這份定義直到能夠給它初值實參為止,這樣可增加程式的清晰度並改善程式效率

27.儘量少做轉型動作

  • 如果可以,儘量避免轉型,特別是在注重效率的程式碼中避免dynamic_cast;試著發展無需轉型的替代設計
  • 如果轉型是必要的,試著將它隱藏於某個函式後
  • 寧可使用C++-style轉型,不要使用舊式轉型(新式轉型很容易辨識出來,而分門別類)

  四種新式轉型如下:

  1. static_cast:適用範圍最廣的,適用於很多隱式轉換,基本資料型別的轉換,基類指標與子類指標的相互轉換,或者新增const屬性,任何型別轉換為void型別  
  2. dynamic_cast:主要用來執行“安全向下轉型”,決定某物件是否歸屬繼承體系中的某個型別。static_cast在下行轉換時不安全,是因為即使轉換失敗,它也不返回NULL ,而dynamic_cast轉換失敗會返回NULL;對於上行轉換,dynamic_cast和static_cast是一樣的
  3. const_cast:通常用來將物件的常量性消除
  4. reinterpret_cast:在位元位級別上進行轉換。它可以把一個指標轉換成一個整數,也可以把一個整數轉換成一個指標,不能將非32bit的例項轉成指標。最普通的用途就是在函式指標型別之間進行轉換,不可移植

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

  • 避免返回handles(包括references、指標、迭代器)指向物件內部(包括成員變數和不被公開的成員函式),否則會破壞封裝性,使const成員函式的行為矛盾,以及發生“空懸虛吊號牌碼”

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

  • “異常安全函式”即使發生異常也不會有洩漏資源或允許任何資料結構敗壞,區分為以下三種保證:
  1. 基本承諾:異常丟擲,程式內的任何事物仍然保持在有效狀態下
  2. 強烈保證:異常丟擲,程式狀態不改變,回覆到呼叫函式之前的狀態(往往能夠以copy-and-swap實現出來)
  3. 不拋擲保證:絕不丟擲異常(如內建型別)
  • 可能的話提供“nothrow保證”,當“強烈保證”不切實際時,就必須提供“基本保證”
  • 函式提供的“異常安全保證”通常最高只等於其所呼叫之各個函式的“異常安全保證”中的最弱者

30.透徹瞭解inline函式的裡裡外外

  • 將大多數inlining限制在小型、被頻繁呼叫的函式身上
  • Template的具現化與inlining無關(Template放在標頭檔案只是因為一般在編譯器完成具現化動作)
  • inline只是給編譯器的建議,大部分的編譯器拒絕將太過複雜的函式inlining,隱喻方式是將函式定義於class定義式內
  • 建構函式和解構函式往往是inlining的糟糕候選人
  • 隨著程式庫的升級,inline函式需要重新編譯,而non-inline函式只需重新連線

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

  為了增加編譯速度,應該減少類檔案之間的相互依存性(include),但是類內又常常使用到其他類,不得不相互依存,解決方案是:將類的宣告和定義分開(不同的標頭檔案),宣告相互依存,而定義不相依存,這樣當定義需要變更時,編譯時不需要再因為依賴而全部編譯。
  • 依賴關係複雜導致的問題就是你修改了某個實現卻需要編譯很多檔案,最好是 介面和實現分離。
  • 支援 “編譯依存最小化” 的一般構想是:相依於宣告式,不相依於定義式。基於此構想的兩個手段是 Handle classes 和 Interface classes。
  • 程式庫標頭檔案應該以 “完全且僅有宣告式” 的形式存在。

繼承與面對物件設計

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

  • public繼承意味著is-a。適用於base class身上的每一件事情也一定適用於derived class身上

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

  • derived classes 內的名稱會遮掩base classes內的名稱,應該避免。
  • 編譯器對於各作用域有查詢順序,所以會造成名稱遮掩,各作用域查順序:
  1. local作用域
  2. derived class
  3. base class
  4. namespace
  5. globle作用域
  • 可以利用using宣告式或者inline轉交函式使遮掩函式重見天日

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

  • pure virtual函式使derived class只繼承函式介面
  • impure virtual函式使derived class繼承函式介面和預設實現
  • non-virtual函式使derived class繼承函式的介面和一份強制性實現

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

可以考慮一些 virtual 函式的替代方案,如:

  • 使用 non-virtual interface(NVI)手法,這是一種名為模版方法模式的設計模式,使用成員函式包裹虛擬函式。
  • 將虛擬函式替換為函式指標的成員變數
  • 將虛擬函式替換為 std::function
  • 將繼承體系內的虛擬函式替換為另一個繼承體系內的虛擬函式(策略模式)

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

  注

  non-virtual函式是靜態繫結的,virtual函式是動態繫結的

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

  • 如拒絕這樣做,可能會在呼叫一個定義於derived class內的virtual函式時,使用base class指定的預設引數值
  • 使用NVI手法(令public non-virtual函式呼叫private virtual函式)可以防止預設引數值被重新定義

  注:
  為了執行期效率,c++堅持預設引數值為靜態繫結,防止執行期複雜的決定

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

  • 當複合發生於應用域內的物件之間,表現has-a的關係;當它發生於實現域內則是表現is-implemented-in-terms-of的關係
  • 複合的意義和public繼承完全不同

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

  • private 繼承意味著父類所有非私有成員在子類中都是 private 的。這樣就幫我們複用父類程式碼且防止父類介面曝光
  • 但是私有繼承意味著不再是 is-a 關係,而更像是 has-a 關係。我們總是可以通過複合的方式替代私有繼承,並且更容易理解,所以無論什麼時候,只要可以,我們還是應該選擇複合。
  • 一種極端情況下,即我們有一個空白的父類,私有繼承可以更小的佔用空間。

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

  • 多繼承比單繼承複雜,而且多繼承可能導致二義性,以及對 virtual 繼承的需要。
  • 父類中存在資料的話,virtual 繼承會增加大小、速度、初始化(及賦值)複雜度等成本,應儘量不使用 virtual 繼承。
  • 多繼承適用的一種場景是:public 繼承某個介面類並 private 繼承某個協助實現的類。

模板與泛型程式設計

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

  • classe和template都支援介面和多型
  • 對class而言介面是顯式的,由函式簽名式構成;多型是通過virtual函式發生於執行期
  • 對template而言介面是隱式的,由有效表示式組成;多型是通過template具現化和函式過載解析發生於編譯期

42.瞭解typename的雙重意義

  • 宣告template引數時,字首關鍵字class和typename可以互換
  • 使用typename標識巢狀從屬型別名稱(如果編譯器在template中遭遇一個巢狀從屬名稱,它便假設這名稱不是個型別),但是不得在base class lists或member initialization list內作為base class修飾符

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

  • template特化版本可能不提供和一般性template相同的介面,所以從Object Oriented C++跨進Template C++時,繼承就不像以前那般暢行無阻了
  • 為了令c++不進入templatized base classes觀察的行為失效,可以:
  1. 在呼叫動作之前加上“this->”
  2. 使用using宣告式(using baseclass::func;)
  3. 明白指出被呼叫的函式位於base class內(baseclass::func())

44.將引數無關程式碼抽離template

  • 非型別模板引數造成的程式碼膨脹,以函式引數或者class成員變數替換template引數
  • 型別模板引數造成的程式碼膨脹,可以讓具有完全相同二進位制表述的具現型別共享實現碼

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

  • 請使用成員函式模版生成“可接受所有相容型別”的函式
  • 即使聲明瞭“泛化拷貝建構函式”和“泛化的賦值操作符”,仍然需要宣告正常的拷貝建構函式和拷貝賦值操作符

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

  • 當我們編寫一個class template,而它所提供之“與此template相關的”函式支援“所有引數之隱式型別轉換”時,請將那些函式定義為“class template內部的friend函式”
  • 在一個class template內,template名稱可被用來作為“template”的簡略表達方式

  注:
  template實參推導過程中從不將隱式型別轉換函式納入考慮,而class template並不依賴template實參推導,在生成模板類時就可推匯出函式而非函式模板

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

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

48.認識模板超程式設計

  • 模板超程式設計可將工作由執行期移至編譯期,因而得以實現早期錯誤偵測和更高的執行效率,可能導致較小的可執行檔案,較短的執行期,較少的記憶體需求,可以解決不少問題

定製new和delete

49.瞭解new-handler的行為

  • 當operator new無法滿足某一記憶體分配需求時,它會丟擲異常;丟擲異常之前,也可以先呼叫一個客戶指定的錯誤處理函式(new-handler),呼叫set_new_handler可以指定該函式
  • Nothrow new(在無法分配足夠記憶體時返回NULL)是一個頗為侷限的工具,它只適用於記憶體分配,後繼的建構函式呼叫還是可能丟擲異常

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

  • 有許多理由需要寫個自定的new和delete,包括檢測錯誤、改善效能,以及收集使用上的統計資料等等

51.編寫符合常規的new和delete

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

52.寫了placement new也要寫相應的placement delete

  • new表示式先後呼叫operator new和default建構函式
  • 當你寫一個placement operator new,請確定也寫出了對應的placement operator delete.如果沒有這樣做,你的程式可能會發生隱微而時斷時續的記憶體洩漏(執行期系統尋找“引數個數和型別都與operator new相同”的某個operator delete,如果一個帶額外引數的operator new沒有“帶相同額外引數”的對應版operator delete,那麼當new的記憶體分配動作需要取消並恢復舊觀時就沒有任何operator delete會被呼叫)
  • 當你宣告placement new和placement delete,請確定不要無意識地遮掩了它們的正常版本

雜項討論

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

  • 嚴肅對待編譯器發出的警告資訊,努力在你的編譯器的最高(最嚴苛)警告級別下爭取“無任何警告”的榮譽(在你打發某個警告資訊之前,請確定你瞭解它意圖說出的精確意義)
  • 不要過度依賴編譯器的報警能力,因為不同的編譯器對待事情的態度並不相同。一旦移植到另一個編譯器上,你原本依賴的警告資訊有可能消失

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

  • TR1詳細敘述了14個新元件,放在std名稱空間內(std::tr1)包括:智慧指標、tr1::function、tr1::bind、Hash tables(用來實現sets、multisets、maps和multi-maps)、正則表示式、Tuples(變數組)、tr1::array、tr1::mem_fn(語句構造上與成員函式指標一致)、tr1::reference_wrapper(使容器“猶如持有references”)、隨機數生成工具、數學特殊函式、C99相容擴充以及Type traits(一組traits classes)、tr1::result_of(推導函式呼叫的返回型別)

55.讓自己熟悉Boost

參考

《Effective C++》

https://blog.csdn.net/a245705313/article/details/81783455

https://www.jianshu.com/p/4661bd7b7593

https://blog.csdn.net/afei__/article/details/83624720