1. 程式人生 > >筆記:《C++ Primer》(第5版)

筆記:《C++ Primer》(第5版)

條件語句 拷貝賦值運算符 局部變量 his 缺陷 nullptr 命令 直觀 執行文件

好習慣

1. 通常用cerr來輸出警告和錯誤消息。

2. 打印語句應保證“一直”刷新流。

3. 註釋掉代碼的最好的方式是用單行註釋方式註釋掉代碼段的每一行(所以用自動註釋時最好連用兩次)。

4. 按照報告的順序來逐個修正錯誤。(但有時前面沒有錯,後面錯了能導致前面也標錯。)

5. 一旦選擇了一種格式風格,就要堅持使用。

6. 當明確知曉數值不可能為負時,選用無符號類型。

7. 使用int執行整數運算。如果你的數值超過了int的表示範圍,選用long long。

8. 在算數表達式中不要使用char或bool,只有在存放字符或布爾值時才使用它們。如果你需要使用一個不大的整數,那麽明確指定它的類型是signed char或者unsigned char。

9. 執行浮點數運算選用double。

10. 避免無法預知和依賴於實現環境的行為。

11. 切勿混用帶符號類型和無符號類型。

12. 當使用一個長整型字面值時,請使用大寫字母L來標記,因為小寫字母l和數字1太容易混淆了。

13. 建議初始化每一個內置類型的變量。雖然並非必須這麽做,但如果我們不能確保初始化後程序安全,那麽這麽做不失為一種簡單可靠的方法。

14. 標識符要能體現實際含義。

15. 變量名一般用小寫字母。

16. 用戶自定義的類名一般以大寫字母開頭。

17. 如果標識符由多個單詞組成,則單詞間應有明顯區分,如student_loan或studentLoan。

18. 當你第一次使用變量時再定義它。

19. 如果函數有可能用到某全局變量,則不宜再定義一個同名的局部變量。

20. 建議初始化所有指針,並且在可能的情況下,盡量等定義了對象之後再定義指向它的指針。如果實在不清楚指針應該指向何處,就把它初始化為nullptr或者0。

21. 將*(或是&)與變量名連在一起。

22. 不要在程序中使用goto語句,因為它使得程序既難理解又難修改。

23. 最好不要把對象的定義和類的定義放在一起。

24. 類通常被定義在頭文件中,而且類所在頭文件的名字應該與類的名字一樣。

25. 使用C++版本的C標準庫頭文件。

26. 下標必須大於等於0而小於字符串的size()的值。一種簡便易行的方法是,總是設下標的類型為string::size_type。此時,代碼只需保證下標小於size()的值就可以了。

27. 在定義vector對象的時候設定其大小沒什麽必要。只有一種例外情況,就是所有元素的值都一樣。

28. 確保下標合法的一種有效手段就是盡可能使用範圍for語句。

29. 只要我們養成使用叠代器和!=的習慣,就不用太在意用的到底是哪種容器類型。

30. 一般來說,最好避免使用非標準特性,因為含有非標準特性的程序很可能在其他的編譯器上無法正常工作。

31. 盡量使用標準庫類型而非數組:現代的C++程序應當盡量使用vector和叠代器,避免使用內置數組和指針;盡量使用string,避免使用C風格的基於數組的字符串。

32. 以下兩條經驗準則對書寫復合表達式有益:

(1) 拿不準的時候最好用括號來強制讓表達式的組合關系符合程序邏輯的要求。

(2) 如果改變了某個運算對象的值,在表達式的其他地方不要再使用這個運算對象。

第2條規則有一個重要例外,當改變運算對象的子表達式本身就是另外一個子表達式的運算對象時該規則無效。

33. 除非必須,否則不用遞增遞減運算符的後置版本(而應該用前置版本)。

34. 簡潔。如書寫cout << *iter++ << endl;而不是cout << *iter << endl;++iter;。

35. 隨著條件運算嵌套層數的增加,代碼的可讀性急劇下降。條件運算的嵌套最好別超過兩到三層。

36. 強烈建議僅將位運算符用於處理無符號類型,因為關於符號位如何處理沒有明確的規定。

37. 建議:避免強制類型轉換。這個建議對於reinterpret_cast尤其適用。在有重載函數的上下文中const_cast無可厚非,但是在其他情況下使用const_cast也就意味著程序存在某種設計缺陷。其他強制類型轉換語句,都應該反復斟酌能否以其他方式實現相同的目標。就算實在無法避免,也應該盡量限制類型轉換值的作用域,並且記錄對相關類型的所有假定。

38. 與命名的強制類型轉換相比,舊式的強制類型轉換從表現形式上來說不那麽清晰明了,容易被看漏,所以一旦轉換過程出現問題,追蹤起來也更加困難。

39. 使用空語句時應該加上註釋,從而令讀這段代碼的人知道該語句是有意省略的。

40. 有些編碼風格要求在if或else之後必須寫上花括號(對while和for語句的循環體兩端也有同樣的要求)。

41. 為了安全起見,一般不要省略case分支最後的break語句。

42. 即使不準備在default標簽下做任何工作,定義一個default標簽也是有用的。其目的在於告訴讀者,我們已經考慮到了默認的情況,只是目前什麽也沒做。

43. 事實上,在函數的聲明中經常省略形參的名字。盡管如此,寫上形參的名字還是有用處的,它可以幫助使用者更好地理解函數的功能。

44. 我們建議變量在頭文件中聲明,在源文件中定義。與之類似,函數也應該在頭文件中聲明而在源文件中定義(否則函數可能會被重復定義而出錯;但是函數可以定義在頭文件的類內部,因為會隱式內聯;如果非要在頭文件內、類外定義函數,就定義成內聯函數)。

45. 把函數的聲明直接放在使用該函數的源文件中可能會很煩瑣而且容易出錯。相反,如果把函數聲明放在頭文件中,就能確保同一函數的所有聲明保持一致。而且一旦我們想改變函數的接口,只需改變一條聲明即可。

46. 熟悉C的程序員常常使用指針類型的形參訪問函數外部的對象。在C++語言中,建議使用引用類型的形參替代指針。

47. 參數傳遞:使用引用避免拷貝。拷貝大的類類型對象或者容器對象比較低效,甚至有的類類型(包括IO類型在內)根本就不支持拷貝操作。

48. 如果函數無須改變引用形參的值,最好將其聲明為常量引用。

49. 盡管函數重載能在一定程度上減輕我們為函數起名字、記名字的負擔,但是最好只重載那些確實非常相似的操作。有些情況下,給函數起不同的名字能使得程序更易理解。

50. 一般來說,將函數聲明置於局部作用域內不是一個明智的選擇。

51. 現在的C++程序最好使用nullprt,同時盡量避免使用NULL。

52. 在一些簡單的應用程序中,類的用戶和類的設計者常常是同一個人。盡管如此,還是最好把角色區分開來。當我們設計類的接口時,應該考慮如何才能使得類易於使用;當我們使用類時,不應該顧及類的實現機理。

53. 把this設置為指向常量的指針有助於提高函數的靈活性。

54. 構造函數不應該輕易覆蓋掉類內的初始值,除非新賦的值與原值不同。

55. 出於統一編程風格的考慮,當我們希望定義的類的所有成員是public的時,使用struct;反之,如果希望成員是private的,使用class。

56. 一般來說,最好在類定義的開始或結束前的位置集中聲明友元。

57. 雖然我們無須在聲明和定義的地方同時說明inline,但這麽做其實是合法的。不過,最好只在類外部定義的地方說明inline,這樣可以使類更容易理解。

58. 和我們在頭文件中定義inline函數的原因一樣,inline成員函數也應該與相應的類定義在同一個頭文件中。

59. 對於公共代碼使用私有功能函數。

60. 類型名的定義通常出現在類的開始處,這樣就能確保所有使用該類型的成員都出現在類名的定義之後。

61. 建議的寫法:不要把成員名字作為參數或其他局部變量使用。

62. 建議讀者養成使用構造函數初始值(列表而不是通過函數體賦值)的習慣,這樣能避免某些意想不到的編譯錯誤。

63. 最好令構造函數初始值的順序與成員聲明的順序保持一致。而且如果可能的話,盡量避免使用某些成員初始化其他成員。

64. 使用explicit的優點是避免因隱式類型轉換而帶來意想不到的錯誤,缺點是當用戶的確需要這樣的類類型轉換時,不得不使用略顯煩瑣的方式來實現。

65. 要想確保對象只定義一次,最好的辦法是把靜態數據成員的定義與其他非內聯函數的定義放在同一個文件中。

66. 文件輸入輸出:因為調用打開文件可能失敗,進行打開文件是否成功的檢測通常是一個好習慣。

67. 現代C++程序應該使用標準庫容器,而不是更原始的數據結構,如內置數組。

68. 確定使用哪種順序容器:通常,使用vector是最好的選擇,除非你有很好的理由選擇其他容器。

69. 當不需要寫訪問時,應使用cbegin和cend。

70. 統一使用非成員版本的swap是一個好習慣。

71. 由於向叠代器添加元素和從叠代器刪除元素的代碼可能會使叠代器失效,因此必須保證每次改變容器的操作之後都正確重新定位叠代器。這個建議對vector、string和deque尤為重要。

72. 如果在一個循環中插入/刪除deque、string或vector中的元素,不要緩存end返回的叠代器。

73. 對於只讀取而不改變元素的泛型算法,通常最好使用cbegin()和cend()。但是,如果你計劃使用算法返回的叠代器來改變元素的值,就需要使用begin()和end()的結果作為參數。

74. 建議:盡量保存lambda的變量捕獲簡單化。一般來說,我們應該盡量減少捕獲的數據量,來避免潛在的捕獲導致的問題。而且,如果可能的話,應該避免捕獲指針、叠代器或引用。

75. 新的C++程序應該使用bind而不是bind1st和bind2nd。

76. 對於list,應該優先使用成員函數版本的算法而不是通用算法。

77. 我們通常不對關聯容器使用泛型算法(關聯容器有專用算法)。在實際編程中,如果我們真要對一個關聯容器使用算法,要麽是將它當作一個源序列,要麽當作一個目的位置。

78. 出於與變量初始化相同的原因,對動態分配的對象進行初始化通常是個好主意(12.1.2)。

79. 堅持只使用智能指針,就可以避免使用new和delete管理動態內存的三個常見問題:忘記delete內存,使用已經釋放掉的對象,同一塊內存釋放兩次。

80. 不要混合使用普通指針和智能指針:當將一個shared_ptr綁定到一個普通指針時,我們就將內存的管理責任交給了這個shared_ptr。一旦這樣做了,我們就不應該使用內置指針來訪問shared_ptr所指向的內存了。

81. get用來將指針的訪問權限傳遞給代碼,你只有在確定代碼不會delete指針的情況下,才能使用get。特別是,永遠不要用get初始化另一個智能指針或者為另一個智能指針賦值。

82. 希望阻止拷貝的類應該使用=delete來定義它們自己的拷貝構造函數和拷貝賦值運算符(說明賦值要先拷貝),而不應該將它們聲明為private的。

83. 盡管從語法上來說我們可以在派生類的構造函數體內給它的公有或受保護的基類成員賦值,但是最好不要這麽做。和使用基類的其他場合一樣,派生類應該遵循基類的接口,並且通過調用基類的構造函數來初始化那些從基類中繼承而來的成員。

84. 如果我們希望能覆蓋基類中的虛函數,就使用override關鍵字。這樣,如果實際上並未覆蓋,編譯器就會報錯。

85. 如果虛函數使用默認實參,則基類和派生類中定義的默認實參最好一致。

86. 一個私有派生的類最好顯式地將private聲明出來,而不要僅僅依賴於默認的設置。顯式聲明的好處是可以令私有繼承關系清晰明了,不至於產生誤會。

87. 除了覆蓋繼承而來的虛函數之外,派生類最好不要重用其他定義在基類中的名字。

88. 看起來用關鍵字typename來指定模板類型參數比用class更為直觀,而且,typename更清楚地指出隨後的名字是一個類型名。

89. C++程序員喜歡用!=而不喜歡用<。

常規筆記

1. 對於那種只在一兩個地方使用的簡單操作,lambda表達式是最有用的。如果我們需要在很多地方使用相同的操作,通常應該定義一個函數,而不是多次編寫相同的lambda表達式。雷聲大,如果一個操作需要很多語句才能完成,通常使用函數更好。

2. 通常情況下,不應該重載逗號、取地址、邏輯與和邏輯或運算符。

3. 通常,輸出運算符應該主要負責打印對象的內容而非控制格式,輸出運算符不應該打印換行符。

4. 輸入運算符必須處理輸入可能失敗的情況(為了確保輸入失敗時對象處於正確狀態),而輸出運算符不需要。

5. 當讀取操作發生錯誤時,輸入運算符應該負責從錯誤中恢復。

6. 拷貝構造函數永遠都會合成默認的(但可能合成的默認的是刪除的)。

7. lanmada是通過匿名的函數對象(如果類定義了調用運算符,則該類的對象稱作函數對象(function object))來實現的,因此我們可以把lamada看作是對函數對象在使用方式上進行的簡化。

當代碼需要一個簡單的函數,並且這個函數並不會在其他地方使用時,就可以使用lambda來實現。但如果這個函數需要多次使用,並且它需要保存某些狀態的話,使用函數對象更合適一些。

8. 如果在調用重載函數時我們需要使用構造函數或者強制類型轉換來改變實參的類型,則這通常意味著程序的設計存在不足。

9. 一條聲明語句的目的時令程序知曉某個名字的存在以及改名字表示一個什麽樣的實體。一個文件如果像使用別處定義的名字則必須包含對那個名字的聲明。

PS:定義負責創建與名字關聯的實體。

10. 之所以存在派生類向基類的類型轉換是因為每個派生類對象都包含一個基類部分,而基類的引用或指針可以綁定到該基類部分上(從而綁定到相應的派生類對象)。當我們用一個派生類對象為一個基類對象初始化或賦值時,只有該派生類對象中的基類部分會被拷貝、移動或賦值,它的派生類部分將被忽略掉。

11. 只有當派生類公有地繼承基類時,用戶代碼才能使用派生類向基類的轉換。無論派生類以什麽方式繼承基類,派生類的成員函數和友元都能使用派生類向基類的轉換。

已錯的易錯點

1. const auto 聲明的是頂層const,而c.cbegin()返回的是不能更改所指對象的叠代器。

2. 函數的返回值用於初始化調用點的一個臨時量,該臨時量就是函數調用的結果。

3. 聲明函數指針的時候,*和函數名兩端的括號必不可少。如果不寫這對括號,則*修飾的是返回類型。

4. 如果定義類的非成員成員函數時用到類的數據成員,或定義類的成員函數時用到別的類的數據成員,就要記得在數據成員前加“類類型的變量.”

5. 那些令人摸不著頭腦的錯誤,造成它們的原因往往時低級錯誤:比如括號少了半邊,關鍵字拼寫錯誤。

6. this,指向調用者。

7. 需要自定義的拷貝構造函數的類也需要自定義的拷貝賦值運算符,反之亦然(PS:拷貝構造函數(構造函數:控制類的對象(對象:內存的一塊區域,具有某種類型,變量是命名了的對象)的初始化過程的類的特殊的成員函數)控制拷貝初始化,拷貝初始化在用=定義變量時、在將一個對象作為實參傳遞給一個非引用類型的形參時、在從一個返回類型為非引用類型的函數返回一個對象、在初始化標準庫容器或是調用其insert或push(此時還會發生析構)成員時會發生(書上所謂的直接初始化,至少像Employee a(b)這種,也會發生);拷貝賦值運算符控制賦值(賦值:抹去一個對象的當前值,用一個新值取代之)。

8. 類聲明,解決C++兩個類互相包含問題:

在構造自己的類時,有可能會碰到兩個類之間的相互引用問題,例如:定義了類A類B,A中使用了B定義的類型,B中也使用了A定義的類型。

在這種情況下,想想可以有A.B.A.B.A.B.…………,很有點子子孫孫無窮盡之狀,那麽我們的機器也無法承受。最主要的還是這種關系很難存在,也很難管理。這種定義方式類同程序中的死循環。所以,一般來說,兩者的定義,至少有一方是使用指針,或者兩者都使用指針,但是決不能兩者都定義實體對象。

言歸正傳,那麽,在定義時因為相互引用肯定會需要相互包含頭文件,如果僅僅只是在各自的頭文件中包含對方的頭文件,是會鏈接失敗的。這樣的包含方式可能會造成編譯器有錯誤提示:A.h文件中使用了未知類型B。

怎麽辦?

一般的做法是:兩個類的頭文件之中,選一個包含另一個類的頭文件,但另一個頭文件中只能采用前向聲明,而在實現文件(*.cpp)中包含頭文件。或者(我發現的)把兩個類寫在一個頭文件中(寫在前面的類,需要使用後面的類的時候要聲明,即前向聲明,並承擔相應的限制,見PS1)(相當於只是把一個頭文件中的#include “*.h”換成了另一個頭文件的內容而得到的頭文件),然後把以不完全類型作為參數或者返回類型的函數的定義放在兩個類的定義之後就可以了。綜上所述,以不完全類型作為參數或者返回類型的函數要麽,定義在頭文件中,要麽定義在相應的類的定義之後。註意:內置類型、復合類型和標準庫類型當然都是完全類型。

PS1:不完全類型(聲明之後定義之前的類類型)只能在非常有限的情景下使用:只可以定義指向這種類型的指針或引用,也可以聲明(但不能定義)以不完全類型作為參數或者返回類型的函數(因為編譯器無法了解不完全類型的對象需要多少存儲空間)。

PS2:預處理器是在編譯之前執行的一段程序,預處理功能之一是#include(當預處理器看到#include標記時就會用指定的頭文件的內容代替#include),即先執行頭文件後執行源文件。因此,雖然不能在頭文件中定義以不完全類型作為參數或者返回類型的函數,但是,如果在頭文件中只聲明而在源文件中定義就沒問題。

PS3:編譯(compile):利用編譯程序從源語言編寫的源程序產生目標程序(即*.obj文件,是二進制的文件)。

鏈接(link):將生成的*.obj文件與庫文件*.lib等文件鏈接成可執行文件(*.exe文件)。

一個現代編譯器的主要工作流程如下:

源程序(source code)→ 預處理器(preprocessor)→ 編譯器(compiler)→ 匯編程序(assembler)→ 目標程序(object code)→鏈接器(Linker)→ 可執行程序(executables)

9. 內聯的構造函數(包括拷貝構造函數)、拷貝賦值運算符和析構函數(至少這4個)都不能定義在源文件。為了避免麻煩,把內聯的都定義在頭文件裏,定義在源文件的就都不內聯(只用inline修飾聲明也不行)。

錯誤提示:無法解析的外部命令/符號。

10. 以下代碼會出錯,因為(我猜)刪除了元素導致叠代器失效,進而導致不能遍歷了(遍歷實際上時通過叠代器)。

set<int> is{ 1,2,3 };

for (auto a : is)

{

is.erase(a);

}

PS:錯誤提示是map/set iterator not incrementable

11. 重載下標運算符時註意返回類型,不是相應的類類型。

12. 括號,兩邊同時寫。

13. 類的某個成員函數可直接調用其他成員函數而不用指出是誰調用(this指針隱式調用),就像訪問類的數據成員一樣。

14. 把一個類聲明成友元之前要聲明這個類,把一個函數聲明成友元之前似乎不要聲明這個函數。

15. if條件語句:如果前面的分支可能改變判斷語句的結果,則必須用if-else結構而不能用兩個並列的if。

技巧

1. 如果把變量定義為全局變量,就便於在函數間共享。當然也可以定義為局部變量,通過函數參數傳遞。

2. 關系運算符直接就能返回bool值了,不必畫蛇添足加上條件運算符。

程序設計

1. 開始一個程序的設計的一種好方法時列出程序的操作,即從需求入手。了解需要哪些操作會幫助我們分析處需要什麽樣的數據結構。

2. 當我們設計一個類時,在真正實現成員之前先編寫程序使用這個類,是一種非常有用的方法。通過這種方法,可以看到類是否具有我們所需要的操作。

3. 當你開始設計一個類時,首先應該考慮的是這個類將提供哪些操作。在確定類需要哪些操作之後,才能思考到底應該把每個類操作設成普通函數還是重載的運算符。如果某些操作在邏輯上與運算符相關,則它們適合於定義成重載的運算符。我們的建議是:只有當操作的含義對於用戶來說清晰明了時才使用運算符。如果用戶對運算符可能有幾種不同的理解,則使用這樣的運算符將產生二義性。

OOP

1. 當我們令一個類公有地繼承另一個類時,派生類應當反映與基類的“是一種(Is A)”關系。在設計良好的類體系當中,公有派生類的對象應該可以用在任何需要基類對象的地方。

泛型編程

1. 模板程序應該盡量減少對實參類型的要求。編寫泛型代碼的兩個重要原則:

l 模板中的函數參數是const的引用。

l 函數體中的條件判斷僅使用<比較運算。

2. 模板的設計者應該提供一個頭文件,包含模板定義以及在類模板或成員定義中用的所有名字的聲明。模板的用戶必須包含模板的頭文件,以及用來實例化模板的任何類型的頭文件。

結構化程序設計

1. 基本思想是采用"自頂向下,逐步求精"的程序設計方法和"單入口單出口"(即只用順序、選擇和循環三種基本程序結構,不使用goto)的控制結構。

2. 模塊化。

筆記:《C++ Primer》(第5版)