《Effective C++》

  1. 儘可能用const和inline而不用#defineios

    儘可能用編譯器而不用預處理。程序員

  2. 儘可能用而不用<stdio.h>算法

    scanf和printf頗有用,但不是類型安全的,並且沒有擴展性。
    on the other hand,①有些iostream的操做實現起來要比相應的C stream效率要低,但不是對全部的iostream而言,而是一些特殊的實現;②在標準化的過程當中,iostream庫在底層作了不少修改,因此對那些要求最大可移植性的應用程序來講,會發現不一樣的廠商遵循標準的程度也不一樣;③iostream庫的類有構造函數而<stdio.h>裏的函數沒有,在某些涉及到靜態對象初始化順序的時候,若是能夠確認不會帶來隱患,用標準C庫更加簡單實用。編程

  3. 儘可能用new和delete而不用malloc和free數組

    malloc和free(及其變體)產生問題的緣由是由於它們太簡單:它們不知道構造函數和析構函數。緩存

  4. 儘可能使用C++風格的註釋安全

  5. 對應的new和delete要採用相同的形式數據結構

    用new的時候會發生兩件事:首先,內存會被分配,而後,爲分配的內存調用一個或多個構造函數;用delete的時候,也有兩件事發生,首先,爲將被釋放的內存調用一個或多個析構函數,而後釋放內存。對於delete來講有這樣一個重要的問題:內存中有多少個對象要被刪除?答案決定了有多少個析構函數將被調用。
    這個問題簡單說來就是,要被刪除的指針指向的是單個對象呢,仍是對象數組?這隻有你來告訴delete。若是你在delete時沒有用括號,delete就會認爲指向的是單個對象;不然,它就會認爲指向的是一個數組:
    string *stringPtr1 = new string;
    string *stringPtr2 = new string[100];
    ……
    delete stringPtr1;
    delete[] stringPtr2;
    若是在stringPtr1前加了[]和stringPtr2前沒有加[],結果都是不可預測的。解決這類問題的規則:若是你調用new時用了[],那麼調用delete時也要用[],若是調用new時沒有用[],那麼調用delete時也不要用[].函數

  6. 析構函數裏對指針成員調用delete性能

    若是在析構函數裏沒有刪除指針,它不會表現出很明顯的外部症狀。相反,它可能只是表現爲一點微小的內存泄露,而且不斷增加,最後吞噬了你的內存空間,致使程序夭折。
    刪除空指針是安全的(由於它什麼也沒作)。
    固然對本條款的使用也不要絕對,好比,你不會用delete去刪除一個沒有用new來初始化的指針,並且,就像用智能指針對象時不用勞你來刪除同樣,你也永遠不會去刪除一個傳遞給你的指針。換句話說,除非類成員最初用了new,不然是不用在析構函數裏用delete的。

  7. 預先準備好內存不夠的狀況

  8. 寫operator new和operator delete時要遵循常規

    要有正確的返回值;可用內存不夠時要調用出錯處理函數;處理好0字節內存請求的狀況;還要避免不當心隱藏了標準形式的new。
    處理零字節請求的技巧在於把它做爲請求一個字節來處理。

  9. 避免隱藏標準形式的new

    在類裏定義了一個稱爲「operator new」的函數後,會不經意地阻止了對標準new 的訪問。條款50 解釋了爲何會這樣,這裏咱們更關心的是如何想個辦法避免這個問題。一個辦法是在類裏寫一個支持標準 new 調用方式的operator new,它和標準new 作一樣的事。這能夠用一個高效的內聯函數來封裝實現。另外一種方法是爲每個增長到 operator new 的參數提供缺省值(見條款24)。

  10. 若是寫了operator new就要同時寫operator delete

    爲何有必要寫本身的operator new和operator delete?爲了效率。缺省的operator new和operator delete具備很是好的通用性,它的這種靈活性也使得在某種特定的場合下,能夠進一步改善它的性能。尤爲在那些須要動態的分配大量的但很小的對象的應用程序裏,狀況更是如此。
    若是寫了一個本身的內存分配程序,就要同時寫一個釋放程序。

  11. 爲須要動態分配內存的類聲明一個拷貝構造函數和一個賦值操做符

    用delete去刪除一個已經被刪除的指針,其結果是不可預測的。
    只要類裏有指針時,就要寫本身版本的拷貝構造函數和賦值操做符函數。在這些函數裏,你能夠拷貝那些被指向的數據結構,從而使每一個對象都有本身的拷貝;或者你能夠採用某種引用計數機制(見條款 M29)去跟蹤當前有多少個對象指向某個數據結構。
    對於有些類,當實現拷貝構造函數和賦值操做符很是麻煩的時候,特別是能夠確信程序中不會作拷貝和賦值操做的時候,去實現它們就會相對來講有點得不償失。照本條款的建議去作:能夠只聲明這些函數(聲明爲private 成員)而不去定義(實現)它們。這就防止了會有人去調用它們,也防止了編譯器去生成它們。關於這個俏皮的小技巧的細節,參見條款27。

  12. 儘可能使用初始化而不要在構造函數裏賦值

    從純實際應用的角度來看,有些狀況下必須用初始化。特別是 const 和引用數據成員只能用初始化,不能被賦值。
    對有基類的對象來講,基類的成員初始化和構造函數體的執行發生在派生類的成員初始化和構造函數體的執行以前。
    經過成員初始化列表來進行初始化老是合法的,效率也毫不低於在構造函數體裏賦值,它只會更高效。另外,它簡化了對類的維護(見條款M32),由於若是一個數據成員之後被修改爲了必須使用成員初始化列表的某種數據類型,那麼,什麼都不用改變。

  13. 初始化列表中成員列出的順序和它們在類中聲明的順序相同

    類成員是按照它們在類裏被聲明的順序進行初始化的,和它們在成員初始化列表中列出的順序沒一點關係。

  14. 肯定基類有虛析構函數

    當經過基類的指針去刪除派生類的對象,而基類又沒有虛析構函數時,結果將是不可肯定的。
    虛函數的目的是讓派生類去定製本身的行爲(見條款36),因此幾乎全部的基類都包含虛函數。若是某個類不包含虛函數,那通常是表示它將不做爲一個基類來使用。當一個類不許備做爲基類使用時,使析構函數爲虛通常是個壞主意。
    實現虛函數須要對象附帶一些額外信息,以使對象在運行時能夠肯定該調用哪一個虛函數。對大多數編譯器來講,這個額外信息的具體形式是一個稱爲vptr(虛函數表指針)的指針。vptr 指向的是一個稱爲vtbl(虛函數表)的函數指針數組。每一個有虛函數的類都附帶有一個vtbl。當對一個對象的某個虛函數進行請求調用時,實際被調用的函數是根據指向vtbl 的vptr 在vtbl 裏找到相應的函數指針來肯定的。
    若是聲明虛析構函數爲inline,將會避免調用它們時產生的開銷,但編譯器仍是必然會在什麼地方產生一個此函數的拷貝。

  15. 讓operator=返回*this的引用

    C++程序員常常犯的一個錯誤是讓operator=返回void,這好象沒什麼不合理的,但它妨礙了連續(鏈式)賦值操做,因此不要這樣作。
    另外一個常犯的錯誤是讓 operator=返回一個const 對象的引用。
    當定義本身的賦值運算符時,必須返回賦值運算符左邊參數的引用,*this。若是不這樣作,就會致使不能連續賦值,或致使調用時的隱式類型轉換不能進行,或兩種狀況同時發生。

  16. 在operator=中對全部數據成員賦值

    只要想對賦值過程的某一個部分進行控制,就必須負責作賦值過程當中全部的事。
    派生類的賦值運算符也必須處理它的基類成員的賦值。

  17. 在operator=中檢查給本身賦值的狀況

    在賦值運算符中要特別注意可能出現別名的狀況,其理由基於兩點。其中之一是效率。若是能夠在賦值運算符函數體的首部檢測到是給本身賦值,就能夠當即返回,從而能夠節省大量的工做,不然必須去實現整個賦值操做。另外一個更重要的緣由是保證正確性。一個賦值運算符必須首先釋放掉一個對象的資源(去掉舊值),而後根據新值分配新的資源。在本身給本身賦值的狀況下,釋放舊的資源將是災難性的,由於在分配新的資源時會須要舊的資源。

  18. 爭取使類的接口完整而且最小

    歸納起來就是說,無故地在接口裏增長函數不是沒有代價的,因此在增長一個新函數時要仔細考慮:它所帶來的方便性(只有在接口完整的前提下才應該考慮增長一個新函數以提供方便性)是否超過它所帶來的額外代價,如複雜性,可讀性,可維護性和編譯時間等。

  19. 分清成員函數、非成員函數和友元函數

    成員函數和非成員函數最大的區別在於成員函數能夠是虛擬的而非成員函數不行。因此,若是有個函數必須進行動態綁定(見條款38),就要採用虛擬函數,而虛擬函數一定是某個類的成員函數。
    explicit 構造函數不能用於隱式轉換。
    假設 f 是想正確聲明的函數,C 是和它相關的類:虛函數必須是成員函數。若是 f 必須是虛函數,就讓它成爲C 的成員函數。operator>>和operator<<毫不能是成員函數。若是f 是operator>>或operator<<,讓f 成爲非成員函數。若是f 還須要訪問C 的非公有成員,讓f 成爲C 的友元函數。只有非成員函數對最左邊的參數進行類型轉換。若是 f 須要對最左邊的參數進行類型轉換,讓f 成爲非成員函數。若是f 還須要訪問C 的非公有成員,讓f 成爲C 的友元函數。其它狀況下都聲明爲成員函數。若是以上狀況都不是,讓 f 成爲C 的成員函數。

  20. 避免public接口出現數據成員

    在public 接口裏放上數據成員無異於自找麻煩,因此要把數據成員安全地隱藏在與功能分離的高牆後。

  21. 儘量使用const

    char p = "Hello"; // 非const 指針,非const 數據
    const char *p = "Hello"; // 非const 指針, const 數據
    char * const p = "Hello"; // const 指針,非const 數據
    const char * const p = "Hello"; // const 指針,const 數據
    語法並不是看起來那麼變化無窮。通常來講,你能夠在頭腦裏畫一條垂直線穿過指針聲明中的星號(
    )位置,若是const 出如今線的左邊,指針指向的數據爲常量;若是const 出如今線的右邊,指針自己爲常量;若是const 在線的兩邊都出現,兩者都是常量。
    一個好的用戶自定義類型的特徵是,它會避免那種沒道理的與固定類型不兼容的行爲。

  22. 儘可能用傳引用而不用傳值

    C 語言中,什麼都是經過傳值來實現的,C++繼承了這一傳統並將它做爲默認方式。除非明確指定,函數的形參老是經過「實參的拷貝」來初始化的,函數的調用者獲得的也是函數返回值的拷貝。
    傳遞引用是個很好的作法,但它會致使自身的複雜性,最大的一個問題就是別名問題,這在條款17 進行了討論。另外,更重要的是,有時不能用引用來傳遞對象,參見條款23。最後要說的是,引用幾乎都是經過指針來實現的,因此經過引用傳遞對象其實是傳遞指針。所以,若是是一個很小的對象——例如int— — 傳值實際上會比傳引用更高效。

  23. 必須返回一個對象時不要試圖返回一個引用

    引用只是一個名字,一個其它某個已經存在的對象的名字。不管什麼時候看到一個引用的聲明,就要當即問本身:它的另外一個名字是什麼呢?由於它必然還有另一個什麼名字(見條款M1)。

  24. 在函數重載和設定參數缺省值間慎重選擇

    答案取決於另外兩個問題。第一,確實有那麼一個值能夠做爲缺省嗎?第二,要用到多少種算法?通常來講,若是能夠選擇一個合適的缺省值而且只是用到一種算法,就使用缺省參數(參見條款38)。不然,就使用函數重載。

  25. 避免對指針和數字類型重載

  26. 小心潛在的二義性

    C++認爲潛在的二義性不是一種錯誤。多繼承(見條款43)充滿了潛在二義性的可能。最常發生的一種狀況是當一個派生類從多個基類繼承了相同的成員名時。

  27. 若是不想使用隱式生成的函數就要顯式的禁止它

    方法是聲明這個函數(operator=),並使之爲private。顯式地聲明一個成員函數,就防止了編譯器去自動生成它的版本;使函數爲private,就防止了別人去調用它。
    可是,這個方法還不是很安全,成員函數和友元函數仍是能夠調用私有函數,除非——若是你夠聰明的話——不去定義(實現)這個函數。這樣,當無心間調用了這個函數時,程序在連接時就會報錯。
    這適用於條款45 所介紹的每個編譯器自動生成的函數。實際應用中,你會發現賦值和拷貝構造函數具備行爲上的類似性(見條款11 和16),這意味着幾乎任什麼時候候當你想禁止它們其中的一個時,就也要禁止另一個。

  28. 劃分全局名字空間

    名字空間帶來的最大的好處之一在於:潛在的二義不會形成錯誤(參見條款26)。因此,從多個不一樣的名字空間引入同一個符號名不會形成衝突(假如確實真的從不使用這個符號的話)。

  29. 避免返回內部數據的句柄

    對於 const 成員函數來講,返回句柄是不明智的,由於它會破壞數據抽象。對於非const 成員函數來講,返回句柄會帶來麻煩,特別是涉及到臨時對象時。句柄就象指針同樣,能夠是懸浮(dangle)的。因此必定要象避免懸浮的指針那樣,儘可能避免懸浮的句柄。

  30. 避免這樣的成員函數:其返回值是指向成員的非const 指針或引用,但成員的訪問級比這個函數要低

  31. 千萬不要返回局部對象的引用,也不要返回函數內部用new 初始化的指針的引用

    先看第一種狀況:返回一個局部對象的引用。它的問題在於,局部對象 ----- 顧名思義 ---- 僅僅是局部的。也就是說,局部對象是在被定義時建立,在離開生命空間時被銷燬的。所謂生命空間,是指它們所在的函數體。當函數返回時,程序的控制離開了這個空間,因此函數內部全部的局部對象被自動銷燬。所以,若是返回局部對象的引用,那個局部對象其實已經在函數調用者使用它以前被銷燬了。當想提升程序的效率而使函數的結果經過引用而不是值返回時,這個問題就會出現。
    寫一個返回廢棄指針的函數無異於坐等內存泄漏的來臨。

  32. 儘量的推遲變量的定義

    推遲變量定義能夠提升程序的效率,加強程序的條理性,還能夠減小對變量含義的註釋。

  33. 明智的使用內聯

    在一臺內存有限的計算機裏,過度地使用內聯所產生的程序會由於有太大的體積而致使可用空間不夠。即便可使用虛擬內存,內聯形成的代碼膨脹也可能會致使不合理的頁面調度行爲(系統顛簸),這將使你的程序運行慢得象在爬。過多的內聯還會下降指令高速緩存的命中率,從而使取指令的速度下降,由於從主存取指令固然比從緩存要慢。
    另外一方面,若是內聯函數體很是短,編譯器爲這個函數體生成的代碼就會真的比爲函數調用生成的代碼要小許多。若是是這種狀況,內聯這個函數將會確實帶來更小的目標代碼和更高的緩存命中率!
    一個給定的內聯函數是否真的被內聯取決於所用的編譯器的具體實現。幸運的是,大多數編譯器均可以設置診斷級,當聲明爲內聯的函數實際上沒有被內聯時,編譯器就會爲你發出警告信息。

  34. 將文件間的編譯依賴性降到最低

    C++的類定義中不只包含接口規範,還有很多實現細節。
    若是可使用對象的引用和指針,就要避免使用對象自己。定義某個類型的引用和指針只會涉及到這個類型的聲明。定義此類型的對象則須要類型定義的參與。
    儘量使用類的聲明,而不使用類的定義。由於在聲明一個函數時,若是用到某個類,是絕對不須要這個類的定義的,即便函數是經過傳值來傳遞和返回這個類。
    不要在頭文件中再(經過#include 指令)包含其它頭文件,除非缺乏了它們就不能編譯。相反,要一個一個地聲明所須要的類,讓使用這個頭文件的用戶本身(經過#include 指令)去包含其它的頭文件,以使用戶代碼最終得以經過編譯。

  35. 使公有繼承體現「是一個」的含義

    C++面向對象編程中一條重要的規則是:公有繼承意味着 "是一個" 。

  36. 區分接口繼承和實現繼承

    純虛函數最顯著的特徵是:它們必須在繼承了它們的任何具體類中從新聲明,並且它們在抽象類中每每沒有定義。
    定義純虛函數的目的在於,使派生類僅僅只是繼承函數的接口。聲明簡單虛函數的目的在於,使派生類繼承函數的接口和缺省實現。聲明非虛函數的目的在於,使派生類繼承函數的接口和強制性實現。

  37. 毫不要從新定義繼承而來的非虛函數

    若是寫類 D 時從新定義了從類B 繼承而來的非虛函數mf,D 的對象就可能表現出精神分裂症般的異常行爲。也就是說,D 的對象在mf 被調用時,行爲有可能象B,也有可能象D,決定因素和對象自己沒有一點關係,而是取決於指向它的指針所聲明的類型。引用也會和指針同樣表現出這樣的異常行爲。
    任何條件下都要禁止從新定義繼承而來的非虛函數。

38.毫不要從新定義繼承而來的缺省參數值

虛函數是動態綁定而缺省參數值是靜態綁定的。這意味着你最終可能調用的是一個定義在派生類,但使用了基類中的缺省參數值的虛函數。
若是缺省參數值被動態綁定,編譯器就必須想辦法爲虛函數在運行時肯定合適的缺省值,這將比如今採用的在編譯階段肯定缺省值的機制更慢更復雜。作出這種選擇是想求得速度上的提升和實現上的簡便,因此你們如今才能感覺獲得程序運行的高效;固然,若是忽視了本條款的建議,就會帶來混亂。

39.避免「向下轉換」繼承層次

這種類型的轉換 ---- 從一個基類指針到一個派生類指針 ---- 被稱爲 "向下轉換",由於它向下轉換了繼承的層次結構。在剛看到的例子中,向下轉換碰巧能夠工做;但正以下面即將看到的,它將給從此的維護人員帶來惡夢。
"向下轉換" 能夠經過幾種方法來消除。最好的方法是將這種轉換用虛函數調用來代替,同時,它可能對有些類不適用,因此要使這些類的每一個虛函數成爲一個空操做。第二個方法是增強類型約束,使得指針的聲明類型和你所知道的真的指針類型之間沒有出入。爲了消除向下轉換,不管費多大工夫都是值得的,由於向下轉換難看、容易致使錯誤,並且使得代碼難於理解、升級和維護(參見條款M32)。

40.經過分層來體現「有一個」或「用···來實現」

使某個類的對象成爲另外一個類的數據成員,從而實現將一個類構築在另外一個類之上,這一過程稱爲 "分層"(Layering)。"分層" 這一術語有不少同義詞,它也常被稱爲:構成(composition),包含(containment)或嵌入(embedding)。條款35 解釋了公有繼承的含義是 "是一個"。對應地,分層的含義是 "有一個" 或 "用...來實現"。
當經過分層使兩個類產生聯繫時,實際上在兩個類之間創建了編譯時的依賴關係。

41.區分繼承和模板

模板類的特色:行爲不依賴於類型。
當對象的類型不影響類中函數的行爲時,就要使用模板來生成這樣一組類。當對象的類型影響類中函數的行爲時,就要使用繼承來獲得這樣一組類。

42.明智的使用私有繼承

這爲咱們引出了私有繼承的含義:私有繼承意味着 "用...來實現"。若是使類D 私有繼承於類B,這樣作是由於你想利用類B 中已經存在的某些代碼,而不是由於類型B 的對象和類型D 的對象之間有什麼概念上的關係。於是,私有繼承純粹是一種實現技術。
私有繼承意味着 "用...來實現" 這一事實會給程序員帶來一點混淆,由於條款40 指出,"分層" 也具備相同的含義。怎麼在兩者之間進行選擇呢?答案很簡單:儘量地使用分層,必須時才使用私有繼承。

43.明智的使用多繼承

44.說你想說的;理解你所說的

45.弄清C++在幕後爲你所寫、所調用的函數

至於拷貝構造函數和賦值運算符,官方的規則是:缺省拷貝構造函數(賦值運算符)對類的非靜態數據成員進行 "以成員爲單位的" 逐一拷貝構造(賦值)。即,若是m 是類C 中類型爲T 的非靜態數據成員,而且C 沒有聲明拷貝構造函數(賦值運算符),m 將會經過類型T 的拷貝構造函數(賦值運算符)被拷貝構造(賦值)---- 若是T 有拷貝構造函數(賦值運算符)的話。若是沒有,規則遞歸應用到m 的數據成員,直至找到一個拷貝構造函數(賦值運算符)或固定類型(例如,int,double,指針,等)爲止。默認狀況下,固定類型的對象拷貝構造(賦值)時是從源對象到目標對象的 "逐位" 拷貝。對於從別的類繼承而來的類來講,這條規則適用於繼承層次結構中的每一層,因此,用戶自定義的構造函數和賦值運算符不管在哪一層被聲明,都會被調用。

46.寧肯編譯和連接時出錯,也不要運行時出錯

將檢查從運行時轉移到編譯或連接時一直是值得努力的目標,只要實際可行,就要追求這一目標。這樣作的獎賞是,程序會更小,更快,更可靠。

47.確保非局部靜態對象在使用前被初始化

非局部靜態對象指的是這樣的對象:

定義在全局或名字空間範圍內(例如:theFileSystem 和tempDir),
在一個類中被聲明爲static,或,
在一個文件範圍被定義爲static。
對於不一樣被編譯單元中的非局部靜態對象,你必定不但願本身的程序行爲依賴於它們的初始化順序,由於你沒法控制這種順序。讓我再重複一遍:你絕對沒法控制不一樣被編譯單元中非局部靜態對象的初始化順序。
很天然地想知道,爲何沒法控制?
這是由於,肯定非局部靜態對象初始化的 " 正確" 順序很困難,很是困難,極其困難。即便在它最普通的形式下 ---- 多個被編譯單元,多個經過隱式模板實例化所生成的非局部靜態對象(隱式模板實例化時,它們自己可能都會產生這樣的問題) ---- 不只不可能肯定正確的初始化順序,每每連找一個能夠肯定正確順序的特殊狀況都不值得。

48.重視編譯器警告

49.熟悉標準庫

標準庫提供了下列高效的實現:vector(就象動態可擴充的數組),list(雙鏈表),queue, stack,deque,map,set和bitset。唉,居然沒有hash table(雖然不少製造商做爲擴充提供),但多少能夠做爲補償的一點是, string 是容器。這很重要,由於它意味着對容器所作的任何操做(見下文)對string 也適用。

50.提升對C++的認識

相關文章
相關標籤/搜索