c++編碼規範

1.所有頭文件都應該使用 #define 防止頭文件被多重包含, 命名格式當是: <PROJECT>_<PATH>_<FILE>_H_

#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_
…
#endif // FOO_BAR_BAZ_H_

2.能用前置聲明的地方儘量不使用 #include.

    當一個頭文件被包含的同時也引入了新的依賴, 一旦該頭文件被修改, 代碼就會被重新編譯. 如果這個頭文件又包含了其他頭文件, 這些頭文件的任何改變都將導致所有包含了該頭文件的代碼被重新編譯. 因此, 我們傾向於減少包含頭文件, 尤其是在頭文件中包含頭文件.

    使用前置聲明可以顯著減少需要包含的頭文件數量. 舉例說明: 如果頭文件中用到類 File, 但不需要訪問 File 類的聲明, 頭文件中只需前置聲明 class File; 而無須 #include "file/base/file.h".

3.只有當函數只有 10 行甚至更少時纔將其定義爲內聯函數.

定義:
 當函數被聲明爲內聯函數之後, 編譯器會將其內聯展開, 而不是按通常的函數調用機制進行調用.
優點:
 當函數體比較小的時候, 內聯該函數可以令目標代碼更加高效. 對於存取函數以及其它函數體比較短, 性能關鍵的函數, 鼓勵 使用內聯.
缺點:
 濫用內聯將導致程序變慢. 內聯可能使目標代碼量或增或減, 這取決於內聯函數的大小. 內聯非常短小的存取函數通常會減少 代碼大小, 但內聯一個相當大的函數將戲劇性的增加代碼大小. 現代處理器由於更好的利用了指令緩存, 小巧的代碼往往執行 更快。 

4.使用標準的頭文件包含順序可增強可讀性, 避免隱藏依賴: C 庫, C++ 庫, 其他庫的 .h, 本項目內的 .h.

5.鼓勵在 .ccp 文件內使用匿名名字空間. 使用具名的名字空間時, 其名稱可基於項目名或相對路徑. 不要使用 using 關鍵字.

定義:名字空間將全局作用域細分爲獨立的, 具名的作用域, 可有效防止全局作用域的命名衝突.優點:

雖然類已經提供了(可嵌套的)命名軸線 (將命名分割在不同類的作用域內), 名字空間在這基礎上又封裝了一層.

舉例來說, 兩個不同項目的全局作用域都有一個類 Foo, 這樣在編譯或運行時造成衝突. 如果每個項目將代碼置於不同名字空間中, project1::Foo 和 project2::Foo 作爲不同符號自然不會衝突.

缺點:

名字空間具有迷惑性, 因爲它們和類一樣提供了額外的 (可嵌套的) 命名軸線.

在頭文件中使用匿名空間導致違背 C++ 的唯一定義原則 (One Definition Rule (ODR)).

6.不要在 .h 文件中使用匿名名字空間.

7.不要在名字空間 std 內聲明任何東西, 包括標準庫的類前置聲明. 在 std 名字空間聲明實體會導致不確定的問題, 比如不可移植. 聲明標準庫下的實體, 需要包含對應的頭文件.

8.不要將嵌套類定義成公有, 除非它們是接口的一部分, 比如, 嵌套類含有某些方法的一組選項. 當公有嵌套類作爲接口的一部分時, 雖然可以直接將他們保持在全局作用域中, 但將嵌套類的聲明置於名字空間內是更好的選擇.

優點:
 當嵌套 (或成員) 類只被外圍類使用時非常有用; 把它作爲外圍類作用域內的成員, 而不是去污染外部作用域的同名類. 嵌套類 可以在外圍類中做前置聲明, 然後在 .cc 文件中定義, 這樣避免在外圍類的聲明中定義嵌套類, 因爲嵌套類的定義通常只與實 現相關.
缺點:
 嵌套類只能在外圍類的內部做前置聲明. 因此, 任何使用了 Foo::Bar* 指針的頭文件不得不包含類 Foo 的整個聲明. 

9.使用靜態成員函數或名字空間內的非成員函數, 儘量不要用裸的全局函數.

優點:
 某些情況下, 非成員函數和靜態成員函數是非常有用的, 將非成員函數放在名字空間內可避免污染全局作用域.
缺點:
 將非成員函數和靜態成員函數作爲新類的成員或許更有意義, 當它們需要訪問外部資源或具有重要的依賴關係時更是 如此.
結論:

有時, 把函數的定義同類的實例脫鉤是有益的, 甚至是必要的. 這樣的函數可以被定義成靜態成員, 或是非成員函數. 非成員函數不應依賴於外部變量, 應儘量置於某個名字空間內. 相比單純爲了封裝若干不共享任何靜態數據的靜態成員函數而創建類, 不如使用命名空間.

定義在同一編譯單元的函數, 被其他編譯單元直接調用可能會引入不必要的耦合和鏈接時依賴; 靜態成員函數對此尤其敏感. 可以考慮提取到新類中, 或者將函數置於獨立庫的名字空間內.

如果你必須定義非成員函數, 又只是在 .cc 文件中使用它, 可使用匿名名字空間或 static 鏈接關鍵字 (如 static int Foo() {...}) 限定其作用域.

10.將函數變量儘可能置於最小作用域內, 並在變量聲明時進行初始化.如果變量是一個對象, 每次進入作用域都要調用其構造函數, 每次退出作用域都要調用其析構函數.
11.禁止使用 class 類型的靜態或全局變量: 它們會導致很難發現的 bug 和不確定的構造和析構函數調用順序.

 靜態生存週期的對象, 包括全局變量, 靜態變量, 靜態類成員變量, 以及函數靜態變量, 都必須是原生數據類型 (POD : Plain Old Data): 只能是 int, char, float, 和 void, 以及 POD 類型的數組/結構體/指針. 永遠不要使用函數返回值初始化靜態變量; 不要在多線程代碼中使用非 const 的靜態變量.


12.構造函數中只進行那些沒什麼意義的 ( 簡單初始化對於程序執行沒有實際的邏輯意義, 因爲成員變量 “有意義” 的值大多不在構造函數中確定) 初始化, 可能的話, 使用 Init() 方法集中初始化有意義的 (non-trivial) 數據.

定義:
 在構造函數體中進行初始化操作.
優點:
 排版方便, 無需擔心類是否已經初始化.
缺點:

 在構造函數中執行操作引起的問題有:

  • 構造函數中很難上報錯誤,不能使用異常。
  • 操作失敗會造成對象初始化失敗,進入不確定狀態.
  • 如果在構造函數內調用了自身的虛函數, 這類調用是不會重定向到子類的虛函數實現. 即使當前沒有子類化實現, 將來仍是隱患.
  • 如果有人創建該類型的全局變量 (雖然違背了上節提到的規則), 構造函數將先 main() 一步被調用, 有可能破壞構造函數中暗含的假設條件. 例如,gflags尚未初始化.
結論:
 如果對象需要進行有意義的 (non-trivial) 初始化, 考慮使用明確的 Init() 方法並 (或) 增加一個成員標記用於指示對 象是否已經初始化成功. 

13.如果一個類定義了若干成員變量又沒有其它構造函數, 必須定義一個默認構造函數. 否則編譯器將自動生產一個很糟糕的默認構造函數.
14.對單個參數的構造函數使用 C++ 關鍵字 explicit.

定義:
 通常, 如果構造函數只有一個參數, 可看成是一種隱式轉換. 打個比方, 如果你定義了 Foo::Foo(string name), 接着把一個字符串傳給一個以 Foo 對象爲參數的函數, 構造函數 Foo::Foo(string name) 將被調用, 並將該字符串轉換爲一個 Foo 的臨時對象傳給調用函數. 看上去很方便, 但如果你並不希望如此通過轉換生成一個新對象的話, 麻煩也隨之而來. 爲避免構造函數被調用造成隱式轉換, 可以將其聲明爲 explicit.

優點:
 避免不合時宜的變換.
缺點:
 
結論:

所有單參數構造函數都必須是顯式的. 在類定義中, 將關鍵字 explicit 加到單參數構造函數前: explicit Foo(string name);

例外: 在極少數情況下, 拷貝構造函數可以不聲明成 explicit. 作爲其它類的透明包裝器的類也是特例之一. 類似的例外情況應在註釋中明確說明.

15.僅在代碼中需要拷貝一個類對象的時候使用拷貝構造函數; 大部分情況下都不需要, 此時應使用 DISALLOW_COPY_AND_ASSIGN.

定義:
 拷貝構造函數在複製一個對象到新建對象時被調用 (特別是對象傳值時).
優點:
 拷貝構造函數使得拷貝對象更加容易. STL 容器要求所有內容可拷貝, 可賦值.
缺點:
 C++ 中的隱式對象拷貝是很多性能問題和 bug 的根源. 拷貝構造函數降低了代碼可讀性, 相比傳引用, 跟蹤傳值的對 象更加困難, 對象修改的地方變得難以捉摸.
結論:
 可以考慮在類的 private: 中添加拷貝構造函數和賦值操作的空實現, 只有聲明, 沒有定義. 由於這些空函數聲明爲  private, 當其他代碼試圖使用它們的時候, 編譯器將報錯. 方便起見, 我們可以使用 DISALLOW_COPY_AND_ASSIGN 宏:  

   大部分類並不需要可拷貝, 也不需要一個拷貝構造函數或重載賦值運算符. 不幸的是, 如果你不主動聲明它們, 編譯器      會爲你自動生成, 而且是 public 的.

16.僅當只有數據時使用 struct, 其它一概使用 class
17.使用組合 常常比使用繼承更合理. 如果使用繼承的話, 定義爲 public 繼承.

定義:
 當子類繼承基類時, 子類包含了父基類所有數據及操作的定義. C++ 實踐中, 繼承主要用於兩種場合: 實現繼承 (implementation inheritance), 子類繼承父類的實現代碼; 接口繼承 (interface inheritance), 子類僅繼承父類的方法名稱. 

優點:
 實現繼承通過原封不動的複用基類代碼減少了代碼量. 由於繼承是在編譯時聲明, 程序員和編譯器都可以理解相應操作並發現錯誤. 從編程角度而言, 接口繼承是用來強制類輸出特定的 API. 在類沒有實現 API 中某個必須的方法時, 編譯器同樣會發現並報告錯誤.

缺點:
 對於實現繼承, 由於子類的實現代碼散佈在父類和子類間之間, 要理解其實現變得更加困難. 子類不能重寫父類的非虛函數, 當然也就不能修改其實現. 基類也可能定義了一些數據成員, 還要區分基類的實際佈局.

結論:

所有繼承必須是 public 的. 如果你想使用私有繼承, 你應該替換成把基類的實例作爲成員對象的方式.

不要過度使用實現繼承. 組合常常更合適一些. 儘量做到只在 “是一個” (“is-a”, YuleFox 注: 其他 “has-a” 情況下請使用組合) 的情況下使用繼承: 如果 Bar 的確 “是一種” Foo, Bar 才能繼承 Foo.

必要的話, 析構函數聲明爲 virtual. 如果你的類有虛函數, 則析構函數也應該爲虛函數. 注意數據成員在任何情況下都應該是私有的.

當重載一個虛函數, 在衍生類中把它明確的聲明爲 virtual. 理論依據: 如果省略 virtual 關鍵字, 代碼閱讀者不得不檢查所有父類, 以判斷該函數是否是虛函數.

18.避免使用多重繼承, 使用時, 除一個基類含有實現外, 其他基類均爲純接口; 

19.接口類類名以 Interface 爲後綴, 除提供帶實現的虛析構函數, 靜態成員函數外, 其他均爲純虛函數, 不定義非靜態數據成員, 不提供構造函數, 提供的話,聲明爲 protected

20.存取函數一般內聯在頭文件中;

21.聲明次序: public -> protected -> private

22.函數體儘量短小, 緊湊, 功能單一; 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章