天天看點

Google C++ 程式設計風格指南 - 中文版 1. 頭檔案¶ 2. 作用域¶ 3. 類¶ 4. 來自 Google 的奇技¶ 5. 其他 C++ 特性¶ 6. 命名約定¶ 7. 注釋¶ 8. 格式¶ 9. 規則特例¶ 10. 結束語¶

 from    http://code.google.com/p/google-styleguide/  

版本:

3.133

原作者: Benjy Weinberger Craig Silverstein Gregory Eitzmann Mark Mentovai Tashana Landray 翻譯: YuleFox yospaly 項目首頁:

  • Google Style Guide
  • Google 開源項目風格指南 - 中文版

PS:    可以對比 Linus的 《 Linux核心代碼風格》 http://blog.csdn.net/shendl/article/details/6230836 的C風格指南閱讀,看看C和C++對風格的不同要求。

背景¶

C++ 是 Google 大部分開源項目的主要程式設計語言. 正如每個 C++ 程式員都知道的, C++ 有很多強大的特性, 但這種強大不可避免的導緻它走向複雜,使代碼更容易産生 bug, 難以閱讀和維護.

本指南的目的是通過詳細闡述 C++ 注意事項來駕馭其複雜性. 這些規則在保證代碼易于管理的同時, 高效使用 C++ 的語言特性.

風格, 亦被稱作可讀性, 也就是指導 C++ 程式設計的約定. 使用術語 “風格” 有些用詞不當, 因為這些習慣遠不止源代碼檔案格式化這麼簡單.

使代碼易于管理的方法之一是加強代碼一緻性. 讓任何程式員都可以快速讀懂你的代碼這點非常重要. 保持統一程式設計風格并遵守約定意味着可以很容易根據 “模式比對” 規則來推斷各種辨別符的含義. 建立通用, 必需的習慣用語和模式可以使代碼更容易了解. 在一些情況下可能有充分的理由改變某些程式設計風格, 但我們還是應該遵循一緻性原則,盡量不這麼做.

本指南的另一個觀點是 C++ 特性的臃腫. C++ 是一門包含大量進階特性的龐大語言. 某些情況下, 我們會限制甚至禁止使用某些特性. 這麼做是為了保持代碼清爽, 避免這些特性可能導緻的各種問題. 指南中列舉了這類特性, 并解釋為什麼這些特性被限制使用.

Google 主導的開源項目均符合本指南的規定.

注意: 本指南并非 C++ 教程, 我們假定讀者已經對 C++ 非常熟悉.

1. 頭檔案¶

通常每一個 .cc 檔案都有一個對應的 .h 檔案. 也有一些常見例外, 如單元測試代碼和隻包含 main() 函數的 .cc 檔案.

正确使用頭檔案可令代碼在可讀性、檔案大小和性能上大為改觀.

下面的規則将引導你規避使用頭檔案時的各種陷阱.

1.1. #define 保護¶

Tip

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

為保證唯一性, 頭檔案的命名應該依據所在項目源代碼樹的全路徑. 例如, 項目 foo 中的頭檔案 foo/src/bar/baz.h 可按如下方式保護:

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

1.2. 頭檔案依賴¶

Tip

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

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

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

不允許通路類的定義的前提下, 我們在一個頭檔案中能對類 Foo 做哪些操作?

  • 我們可以将資料成員類型聲明為 Foo * 或 Foo &.
  • 我們可以将函數參數 / 傳回值的類型聲明為 Foo (但不能定義實作).
  • 我們可以将靜态資料成員的類型聲明為 Foo, 因為靜态資料成員的定義在類定義之外.

反之, 如果你的類是 Foo 的子類, 或者含有類型為 Foo 的非靜态資料成員, 則必須包含 Foo 所在的頭檔案.

有時, 使用指針成員 (如果是 scoped_ptr 更好) 替代對象成員的确是明智之選. 然而, 這會降低代碼可讀性及執行效率, 是以如果僅僅為了少包含頭檔案,還是不要這麼做的好.

當然 .cc 檔案無論如何都需要所使用類的定義部分, 自然也就會包含若幹頭檔案.

1.3. 内聯函數¶

Tip

隻有當函數隻有 10 行甚至更少時才将其定義為内聯函數.

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

一個較為合理的經驗準則是, 不要内聯超過 10 行的函數. 謹慎對待析構函數, 析構函數往往比其表面看起來要更長, 因為有隐含的成員和基類析構函數被調用!

另一個實用的經驗準則: 内聯那些包含循環或 switch 語句的函數常常是得不償失 (除非在大多數情況下, 這些循環或 switch 語句從不被執行).

有些函數即使聲明為内聯的也不一定會被編譯器内聯, 這點很重要; 比如虛函數和遞歸函數就不會被正常内聯. 通常, 遞歸函數不應該聲明成内聯函數.(YuleFox 注: 遞歸調用堆棧的展開并不像循環那麼簡單, 比如遞歸層數在編譯時可能是未知的, 大多數編譯器都不支援内聯遞歸函數). 虛函數内聯的主要原因則是想把它的函數體放在類定義内, 為了圖個友善, 抑或是當作文檔描述其行為, 比如精短的存取函數.

1.4. -inl.h檔案¶

Tip

複雜的内聯函數的定義, 應放在字尾名為 -inl.h 的頭檔案中.

内聯函數的定義必須放在頭檔案中, 編譯器才能在調用點内聯展開定義. 然而, 實作代碼理論上應該放在 .cc 檔案中, 我們不希望 .h 檔案中有太多實作代碼, 除非在可讀性和性能上有明顯優勢.

如果内聯函數的定義比較短小, 邏輯比較簡單, 實作代碼放在 .h 檔案裡沒有任何問題. 比如, 存取函數的實作理所當然都應該放在類定義内. 出于編寫者和調用者的友善, 較複雜的内聯函數也可以放到 .h 檔案中, 如果你覺得這樣會使頭檔案顯得笨重, 也可以把它萃取到單獨的 -inl.h 中. 這樣把實作和類定義分離開來, 當需要時包含對應的 -inl.h 即可。

-inl.h 檔案還可用于函數模闆的定義. 進而增強模闆定義的可讀性.

别忘了 -inl.h 和其他頭檔案一樣, 也需要 #define 保護.

1.5. 函數參數的順序¶

Tip

定義函數時, 參數順序依次為: 輸入參數, 然後是輸出參數.

C/C++ 函數參數分為輸入參數, 輸出參數, 和輸入/輸出參數三種. 輸入參數一般傳值或傳 const 引用, 輸出參數或輸入/輸出參數則是非-const 指針. 對參數排序時, 将隻輸入的參數放在所有輸出參數之前. 尤其是不要僅僅因為是新加的參數, 就把它放在最後; 即使是新加的隻輸入參數也要放在輸出參數.

這條規則并不需要嚴格遵守. 輸入/輸出兩用參數 (通常是類/結構體變量) 把事情變得複雜, 為保持和相關函數的一緻性, 你有時不得不有所變通.

1.6. #include 的路徑及順序¶

Tip

使用标準的頭檔案包含順序可增強可讀性, 避免隐藏依賴: C 庫, C++ 庫, 其他庫的 .h, 本項目内的 .h.

項目内頭檔案應按照項目源代碼目錄樹結構排列, 避免使用 UNIX 特殊的快捷目錄  . (目前目錄) 或  .. (上級目錄). 例如,  google-awesome-project/src/base/logging.h 應該按如下方式包含:
#include “base/logging.h”      
又如,  dir/foo.cc 的主要作用是實作或測試  dir2/foo2.h 的功能,  foo.cc 中包含頭檔案的次序如下:
  1. dir2/foo2.h (優先位置, 詳情如下)
  2. C 系統檔案
  3. C++ 系統檔案
  4. 其他庫的 .h 檔案
  5. 本項目内 .h 檔案

這種排序方式可有效減少隐藏依賴. 我們希望每一個頭檔案都是可被獨立編譯的 (yospaly 譯注: 即該頭檔案本身已包含所有必要的顯式依賴), 最簡單的方法是将其作為第一個 .h 檔案 #included 進對應的 .cc.

dir/foo.cc 和 dir2/foo2.h 通常位于同一目錄下 (如 base/basictypes_unittest.cc 和 base/basictypes.h), 但也可以放在不同目錄下.

按字母順序對頭檔案包含進行二次排序是不錯的主意 (yospaly 譯注: 之前已經按頭檔案類别排過序了).

舉例來說,  google-awesome-project/src/foo/internal/fooserver.cc 的包含次序如下:
#include "foo/public/fooserver.h" // 優先位置 #include <sys/types.h> #include <unistd.h> #include <hash_map> #include <vector> #include "base/basictypes.h" #include "base/commandlineflags.h" #include "foo/public/bar.h"      

譯者 (YuleFox) 筆記¶

  1. 避免多重包含是學程式設計時最基本的要求;
  2. 前置聲明是為了降低編譯依賴,防止修改一個頭檔案引發多米諾效應;
  3. 内聯函數的合理使用可提高代碼執行效率;
  4. -inl.h 可提高代碼可讀性 (一般用不到吧:D);
  5. 标準化函數參數順序可以提高可讀性和易維護性 (對函數參數的堆棧空間有輕微影響, 我以前大多是相同類型放在一起);
  6. 包含檔案的名稱使用 . 和 .. 雖然友善卻易混亂, 使用比較完整的項目路徑看上去很清晰, 很條理, 包含檔案的次序除了美觀之外, 最重要的是可以減少隐藏依賴, 使每個頭檔案在 “最需要編譯” (對應源檔案處 :D) 的地方編譯, 有人提出庫檔案放在最後, 這樣出錯先是項目内的檔案, 頭檔案都放在對應源檔案的最前面, 這一點足以保證内部錯誤的及時發現了.

2. 作用域¶

2.1. 名字空間¶

Tip

鼓勵在 .cc 檔案内使用匿名名字空間. 使用具名的名字空間時, 其名稱可基于項目名或相對路徑. 不要使用 using 關鍵字.

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

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

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

缺點:

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

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

結論:
根據下文将要提到的政策合理使用命名空間.

2.1.1. 匿名名字空間¶

  • 在  .cc 檔案中, 允許甚至鼓勵使用匿名名字空間, 以避免運作時的命名沖突:
    namespace { // .cc 檔案中 // 名字空間的内容無需縮進 enum { kUNUSED, kEOF, kERROR }; // 經常使用的符号 bool AtEof() {return pos_ == kEOF; } // 使用本名字空間内的符号 EOF } // namespace      
    然而, 與特定類關聯的檔案作用域聲明在該類中被聲明為類型, 靜态資料成員或靜态成員函數, 而不是匿名名字空間的成員. 如上例所示, 匿名空間結束時用注釋 // namespace 辨別.
  • 不要在 .h 檔案中使用匿名名字空間.

2.1.2. 具名的名字空間¶

具名的名字空間使用方式如下:

  • 用名字空間把檔案包含,  gflags 的聲明/定義, 以及類的前置聲明以外的整個源檔案封裝起來, 以差別于其它名字空間:
    // .h 檔案 namespace mynamespace { // 所有聲明都置于命名空間中 // 注意不要使用縮進 class MyClass { public: … void Foo(); }; } // namespace mynamespace      
    // .cc 檔案 namespace mynamespace { // 函數定義都置于命名空間中 void MyClass::Foo() { … } } // namespace mynamespace      
    通常的 .cc 檔案包含更多, 更複雜的細節, 比如引用其他名字空間的類等.
    #include “a.h” DEFINE_bool(someflag, false, “dummy flag”); class C; // 全局名字空間中類 C 的前置聲明 namespace a { class A; } // a::A 的前置聲明 namespace b { …code for b… // b 中的代碼 } // namespace b      
  • 不要在名字空間 std 内聲明任何東西, 包括标準庫的類前置聲明. 在 std 名字空間聲明實體會導緻不确定的問題, 比如不可移植. 聲明标準庫下的實體, 需要包含對應的頭檔案.
  • 最好不要使用 ``using`` 關鍵字, 以保證名字空間下的所有名稱都可以正常使用.
    // 禁止 —— 污染名字空間 using namespace foo;      
  • 在 .cc 檔案, .h 檔案的函數, 方法或類中, 可以使用 ``using`` 關鍵字.
    // 允許: .cc 檔案中 // .h 檔案的話, 必須在函數, 方法或類的内部使用 using ::foo::bar;      
  • 在 .cc 檔案, .h 檔案的函數, 方法或類中, 允許使用名字空間别名.
    // 允許: .cc 檔案中 // .h 檔案的話, 必須在函數, 方法或類的内部使用 namespace fbz = ::foo::bar::baz;      

2.2. 嵌套類¶

Tip

當公有嵌套類作為接口的一部分時, 雖然可以直接将他們保持在全局作用域中, 但将嵌套類的聲明置于名字空間内是更好的選擇.

定義: 在一個類内部定義另一個類; 嵌套類也被稱為  成員類 (member class).
class Foo { private: // Bar是嵌套在Foo中的成員類 class Bar { … }; };      
優點:
當嵌套 (或成員) 類隻被外圍類使用時非常有用; 把它作為外圍類作用域内的成員, 而不是去污染外部作用域的同名類. 嵌套類可以在外圍類中做前置聲明, 然後在  .cc 檔案中定義, 這樣避免在外圍類的聲明中定義嵌套類, 因為嵌套類的定義通常隻與實作相關.
缺點:
嵌套類隻能在外圍類的内部做前置聲明. 是以, 任何使用了  Foo::Bar* 指針的頭檔案不得不包含類  Foo 的整個聲明.
結論:
不要将嵌套類定義成公有, 除非它們是接口的一部分, 比如, 嵌套類含有某些方法的一組選項.

2.3. 非成員函數, 靜态成員函數, 和全局函數¶

Tip

使用靜态成員函數或名字空間内的非成員函數, 盡量不要用裸的全局函數.

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

有時, 把函數的定義同類的執行個體脫鈎是有益的, 甚至是必要的. 這樣的函數可以被定義成靜态成員, 或是非成員函數. 非成員函數不應依賴于外部變量, 應盡量置于某個名字空間内. 相比單純為了封裝若幹不共享任何靜态資料的靜态成員函數而建立類, 不如使用命名空間.

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

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

2.4. 局部變量¶

Tip

将函數變量盡可能置于最小作用域内, 并在變量聲明時進行初始化.

C++ 允許在函數的任何位置聲明變量. 我們提倡在盡可能小的作用域中聲明變量, 離第一次使用越近越好. 這使得代碼浏覽者更容易定位變量聲明的位置, 了解變量的類型和初始值. 特别是,應使用初始化的方式替代聲明再指派, 比如:
int i; i = f(); // 壞——初始化和聲明分離 nt j = g(); // 好——初始化時聲明      
注意, GCC 可正确實作了  for (int i = 0; i < 10; ++i) ( i 的作用域僅限  for 循環内), 是以其他  for 循環中可以重新使用  i. 在  if 和 while 等語句中的作用域聲明也是正确的, 如:
while (const char* p = strchr(str, ‘/’)) str = p + 1;      

Warning

如果變量是一個對象, 每次進入作用域都要調用其構造函數, 每次退出作用域都要調用其析構函數.

// 低效的實作 for (int i = 0; i < 1000000; ++i) { Foo f; // 構造函數和析構函數分别調用 1000000 次! f.DoSomething(i); }      
在循環作用域外面聲明這類變量要高效的多:
Foo f; // 構造函數和析構函數隻調用 1 次 for (int i = 0; i < 1000000; ++i) { f.DoSomething(i); }      

2.5. 靜态和全局變量¶

Tip

禁止使用 class 類型的靜态或全局變量: 它們會導緻很難發現的 bug 和不确定的構造和析構函數調用順序.

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

不幸的是, 靜态變量的構造函數, 析構函數以及初始化操作的調用順序在 C++ 标準中未明确定義, 甚至每次編譯建構都有可能會發生變化, 進而導緻難以發現的 bug. 比如, 結束程式時, 某個靜态變量已經被析構了, 但代碼還在跑 – 其它線程很可能 – 試圖通路該變量, 直接導緻崩潰.

是以, 我們隻允許 POD 類型的靜态變量. 本條規則完全禁止 vector (使用 C 數組替代), string (使用 const char*), 及其它以任意方式包含或指向類執行個體的東東, 成為靜态變量. 出于同樣的理由, 我們不允許用函數傳回值來初始化靜态變量.

如果你确實需要一個 class` 類型的靜态或全局變量, 可以考慮在 ``main() 函數或 pthread_once() 内初始化一個你永遠不會回收的指針.

Note

yospaly 譯注:

上文提及的靜态變量泛指靜态生存周期的對象, 包括: 全局變量, 靜态變量, 靜态類成員變量, 以及函數靜态變量.

譯者 (YuleFox) 筆記¶

  1. cc 中的匿名名字空間可避免命名沖突, 限定作用域, 避免直接使用 using 關鍵字污染命名空間;
  2. 嵌套類符合局部使用原則, 隻是不能在其他頭檔案中前置聲明, 盡量不要 public;
  3. 盡量不用全局函數和全局變量, 考慮作用域和命名空間限制, 盡量單獨形成編譯單元;
  4. 多線程中的全局變量 (含靜态成員變量) 不要使用 class 類型 (含 STL 容器), 避免不明确行為導緻的 bug.
  5. 作用域的使用, 除了考慮名稱污染, 可讀性之外, 主要是為降低耦合, 提高編譯/執行效率.

3. 類¶

類是 C++ 中代碼的基本單元. 顯然, 它們被廣泛使用. 本節列舉了在寫一個類時的主要注意事項.

3.1. 構造函數的職責¶

Tip

構造函數中隻進行那些沒什麼意義的 (trivial, YuleFox 注: 簡單初始化對于程式執行沒有實際的邏輯意義, 因為成員變量 “有意義” 的值大多不在構造函數中确定) 初始化, 可能的話, 使用 Init() 方法集中初始化有意義的 (non-trivial) 資料.

定義:
在構造函數體中進行初始化操作.
優點:
排版友善, 無需擔心類是否已經初始化.
缺點:
在構造函數中執行操作引起的問題有:
  • 構造函數中很難上報錯誤, 不能使用異常.
  • 操作失敗會造成對象初始化失敗,進入不确定狀态.
  • 如果在構造函數内調用了自身的虛函數, 這類調用是不會重定向到子類的虛函數實作. 即使目前沒有子類化實作, 将來仍是隐患.
  • 如果有人建立該類型的全局變量 (雖然違背了上節提到的規則), 構造函數将先 main() 一步被調用, 有可能破壞構造函數中暗含的假設條件. 例如, gflags 尚未初始化.
結論:
如果對象需要進行有意義的 (non-trivial) 初始化, 考慮使用明确的  Init() 方法并 (或) 增加一個成員标記用于訓示對象是否已經初始化成功.

3.2. 預設構造函數¶

Tip

如果一個類定義了若幹成員變量又沒有其它構造函數, 必須定義一個預設構造函數. 否則編譯器将自動生産一個很糟糕的預設構造函數.

定義:
new 一個不帶參數的類對象時, 會調用這個類的預設構造函數. 用  new[] 建立數組時,預設構造函數則總是被調用.
優點:
預設将結構體初始化為 “無效” 值, 使調試更友善.
缺點:
對代碼編寫者來說, 這是多餘的工作.
結論:

如果類中定義了成員變量, 而且沒有提供其它構造函數, 你必須定義一個 (不帶參數的) 預設構造函數. 把對象的内部狀态初始化成一緻/有效的值無疑是更合理的方式.

這麼做的原因是: 如果你沒有提供其它構造函數, 又沒有定義預設構造函數, 編譯器将為你自動生成一個. 編譯器生成的構造函數并不會對對象進行合理的初始化.

如果你定義的類繼承現有類, 而你又沒有增加新的成員變量, 則不需要為新類定義預設構造函數.

3.3. 顯式構造函數¶

Tip

對單個參數的構造函數使用 C++ 關鍵字 explicit.

定義:
通常, 如果構造函數隻有一個參數, 可看成是一種隐式轉換. 打個比方, 如果你定義了  Foo::Foo(string name), 接着把一個字元串傳給一個以 Foo 對象為參數的函數, 構造函數  Foo::Foo(string name) 将被調用, 并将該字元串轉換為一個  Foo 的臨時對象傳給調用函數. 看上去很友善, 但如果你并不希望如此通過轉換生成一個新對象的話, 麻煩也随之而來. 為避免構造函數被調用造成隐式轉換, 可以将其聲明為 explicit.
優點:
避免不合時宜的變換.
缺點:
結論:

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

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

3.4. 拷貝構造函數¶

Tip

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

定義:
拷貝構造函數在複制一個對象到建立對象時被調用 (特别是對象傳值時).
優點:
拷貝構造函數使得拷貝對象更加容易. STL 容器要求所有内容可拷貝, 可指派.
缺點:
C++ 中的隐式對象拷貝是很多性能問題和 bug 的根源. 拷貝構造函數降低了代碼可讀性, 相比傳引用, 跟蹤傳值的對象更加困難, 對象修改的地方變得難以捉摸.
結論:
大部分類并不需要可拷貝, 也不需要一個拷貝構造函數或重載指派運算符. 不幸的是, 如果你不主動聲明它們, 編譯器會為你自動生成, 而且是 public 的.
可以考慮在類的  private: 中添加拷貝構造函數和指派操作的空實作, 隻有聲明, 沒有定義. 由于這些空函數聲明為  private, 當其他代碼試圖使用它們的時候, 編譯器将報錯. 友善起見, 我們可以使用  DISALLOW_COPY_AND_ASSIGN 宏:
// 禁止使用拷貝構造函數和 operator= 指派操作的宏 // 應該類的 private: 中使用 #define DISALLOW_COPY_AND_ASSIGN(TypeName) \TypeName(const TypeName&); \ void operator=(const TypeName&)      
在  class foo: 中:
class Foo { public: Foo(int f); ~Foo(); private: DISALLOW_COPY_AND_ASSIGN(Foo); };      
如上所述, 絕大多數情況下都應使用 DISALLOW_COPY_AND_ASSIGN 宏. 如果類确實需要可拷貝, 應在該類的頭檔案中說明原由, 并合理的定義拷貝構造函數和指派操作. 注意在 operator= 中檢測自我指派的情況 (yospaly 注: 即 operator= 接收的參數是該對象本身).

為了能作為 STL 容器的值, 你可能有使類可拷貝的沖動. 在大多數類似的情況下, 真正該做的是把對象的 指針 放到 STL 容器中. 可以考慮使用 std::tr1::shared_ptr.

3.5. 結構體 VS. 類¶

Tip

僅當隻有資料時使用 struct, 其它一概使用 class.

在 C++ 中 struct 和 class 關鍵字幾乎含義一樣. 我們為這兩個關鍵字添加我們自己的語義了解, 以便為定義的資料類型選擇合适的關鍵字.

struct 用來定義包含資料的被動式對象, 也可以包含相關的常量, 但除了存取資料成員之外, 沒有别的函數功能. 并且存取功能是通過直接通路位域 (field), 而非函數調用. 除了構造函數, 析構函數, Initialize(), Reset(), Validate() 外, 不能提供其它功能的函數.

如果需要更多的函數功能, class 更适合. 如果拿不準, 就用 class.

為了和 STL 保持一緻, 對于仿函數 (functors) 和特性 (traits) 可以不用 class 而是使用 struct.

注意: 類和結構體的成員變量使用 不同的命名規則.

3.6. 繼承¶

Tip

使用組合 (composition, YuleFox 注: 這一點也是 GoF 在 <<Design Patterns>> 裡反複強調的) 常常比使用繼承更合理. 如果使用繼承的話, 定義為 public 繼承.

定義:
當子類繼承基類時, 子類包含了父基類所有資料及操作的定義. C++ 實踐中, 繼承主要用于兩種場合: 實作繼承 (implementation inheritance), 子類繼承父類的實作代碼; 接口繼承 (interface inheritance), 子類僅繼承父類的方法名稱.
優點:
實作繼承通過原封不動的複用基類代碼減少了代碼量. 由于繼承是在編譯時聲明, 程式員和編譯器都可以了解相應操作并發現錯誤. 從程式設計角度而言, 接口繼承是用來強制類輸出特定的 API. 在類沒有實作 API 中某個必須的方法時, 編譯器同樣會發現并報告錯誤.
缺點:
對于實作繼承, 由于子類的實作代碼散布在父類和子類間之間, 要了解其實作變得更加困難. 子類不能重寫父類的非虛函數, 當然也就不能修改其實作. 基類也可能定義了一些資料成員, 還要區分基類的實際布局.
結論:

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

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

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

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

3.7. 多重繼承¶

Tip

真正需要用到多重實作繼承的情況少之又少. 隻在以下情況我們才允許多重繼承: 最多隻有一個基類是非抽象類; 其它基類都是以 Interface為字尾的 純接口類.

定義:
多重繼承允許子類擁有多個基類. 要将作為  純接口 的基類和具有  實作 的基類差別開來.
優點:
相比單繼承 (見  繼承), 多重實作繼承可以複用更多的代碼.
缺點:
真正需要用到多重  實作 繼承的情況少之又少. 多重實作繼承看上去是不錯的解決方案, 但你通常也可以找到一個更明确, 更清晰的不同解決方案.
結論:
隻有當所有父類除第一個外都是  純接口類 時, 才允許使用多重繼承. 為確定它們是純接口, 這些類必須以  Interface 為字尾.

Note

關于該規則, Windows 下有個 特例.

3.8. 接口¶

Tip

接口是指滿足特定條件的類, 這些類以 Interface 為字尾 (不強制).

定義:
當一個類滿足以下要求時, 稱之為純接口:
  • 隻有純虛函數 (“=0“) 和靜态函數 (除了下文提到的析構函數).
  • 沒有非靜态資料成員.
  • 沒有定義任何構造函數. 如果有, 也不能帶有參數, 并且必須為 protected.
  • 如果它是一個子類, 也隻能從滿足上述條件并以 Interface 為字尾的類繼承.
接口類不能被直接執行個體化, 因為它聲明了純虛函數. 為確定接口類的所有實作可被正确銷毀, 必須為之聲明虛析構函數 (作為上述第 1 條規則的特例, 析構函數不能是純虛函數). 具體細節可參考 Stroustrup 的 The C++ Programming Language, 3rd edition 第 12.4 節.
優點:
以  Interface 為字尾可以提醒其他人不要為該接口類增加函數實作或非靜态資料成員. 這一點對于  多重繼承 尤其重要. 另外, 對于 Java 程式員來說, 接口的概念已是深入人心.
缺點:
Interface 字尾增加了類名長度, 為閱讀和了解帶來不便. 同時,接口特性作為實作細節不應暴露給使用者.
結論:
隻有在滿足上述需要時, 類才以  Interface 結尾, 但反過來, 滿足上述需要的類未必一定以  Interface 結尾.

3.9. 運算符重載¶

Tip

除少數特定環境外,不要重載運算符.

定義:
一個類可以定義諸如  + 和  / 等運算符, 使其可以像内建類型一樣直接操作.
優點:
使代碼看上去更加直覺, 類表現的和内建類型 (如  int) 行為一緻. 重載運算符使  Equals(),  Add() 等函數名黯然失色. 為了使一些模闆函數正确工作, 你可能必須定義操作符.
缺點:
雖然操作符重載令代碼更加直覺, 但也有一些不足:
  • 混淆視聽, 讓你誤以為一些耗時的操作和操作内建類型一樣輕巧.
  • 更難定位重載運算符的調用點, 查找 Equals() 顯然比對應的 == 調用點要容易的多.
  • 有的運算符可以對指針進行操作, 容易導緻 bug. Foo + 4 做的是一件事, 而 &Foo + 4 可能做的是完全不同的另一件事. 對于二者, 編譯器都不會報錯, 使其很難調試;
重載還有令你吃驚的副作用. 比如, 重載了 operator& 的類不能被前置聲明.
結論:

一般不要重載運算符. 尤其是指派操作 (operator=) 比較詭異, 應避免重載. 如果需要的話, 可以定義類似 Equals(), CopyFrom() 等函數.

然而, 極少數情況下可能需要重載運算符以便與模闆或 “标準” C++ 類互操作 (如 operator<<(ostream&, const T&)). 隻有被證明是完全合理的才能重載, 但你還是要盡可能避免這樣做. 尤其是不要僅僅為了在 STL 容器中用作鍵值就重載 operator== 或 operator<; 相反, 你應該在聲明容器的時候, 建立相等判斷和大小比較的仿函數類型.

有些 STL 算法确實需要重載 operator== 時, 你可以這麼做, 記得别忘了在文檔中說明原因.

參考 拷貝構造函數 和 函數重載.

3.10. 存取控制¶

Tip

将 所有 資料成員聲明為 private, 并根據需要提供相應的存取函數. 例如, 某個名為 foo_ 的變量, 其取值函數是 foo(). 還可能需要一個指派函數 set_foo().

一般在頭檔案中把存取函數定義成内聯函數.

參考 繼承 和 函數命名

3.11. 聲明順序¶

Tip

在類中使用特定的聲明順序: public: 在 private: 之前, 成員函數在資料成員 (變量) 前;

類的通路控制區段的聲明順序依次為: public:, protected:, private:. 如果某區段沒内容, 可以不聲明.

每個區段内的聲明通常按以下順序:

  • typedefs 和枚舉
  • 常量
  • 構造函數
  • 析構函數
  • 成員函數, 含靜态成員函數
  • 資料成員, 含靜态資料成員

宏 DISALLOW_COPY_AND_ASSIGN 的調用放在 private: 區段的末尾. 它通常是類的最後部分. 參考 拷貝構造函數.

.cc 檔案中函數的定義應盡可能和聲明順序一緻.

不要在類定義中内聯大型函數. 通常, 隻有那些沒有特别意義或性能要求高, 并且是比較短小的函數才能被定義為内聯函數. 更多細節參考 内聯函數.

3.12. 編寫簡短函數¶

Tip

傾向編寫簡短, 凝練的函數.

我們承認長函數有時是合理的, 是以并不硬性限制函數的長度. 如果函數超過 40 行, 可以思索一下能不能在不影響程式結構的前提下對其進行分割.

即使一個長函數現在工作的非常好, 一旦有人對其修改, 有可能出現新的問題. 甚至導緻難以發現的 bug. 使函數盡量簡短, 便于他人閱讀和修改代碼.

在處理代碼時, 你可能會發現複雜的長函數. 不要害怕修改現有代碼: 如果證明這些代碼使用 / 調試困難, 或者你需要使用其中的一小段代碼, 考慮将其分割為更加簡短并易于管理的若幹函數.

譯者 (YuleFox) 筆記¶

  1. 不在構造函數中做太多邏輯相關的初始化;
  2. 編譯器提供的預設構造函數不會對變量進行初始化, 如果定義了其他構造函數, 編譯器不再提供, 需要編碼者自行提供預設構造函數;
  3. 為避免隐式轉換, 需将單參數構造函數聲明為 explicit;
  4. 為避免拷貝構造函數, 指派操作的濫用和編譯器自動生成, 可将其聲明為 private 且無需實作;
  5. 僅在作為資料集合時使用 struct;
  6. 組合 > 實作繼承 > 接口繼承 > 私有繼承, 子類重載的虛函數也要聲明 virtual 關鍵字, 雖然編譯器允許不這樣做;
  7. 避免使用多重繼承, 使用時, 除一個基類含有實作外, 其他基類均為純接口;
  8. 接口類類名以 Interface 為字尾, 除提供帶實作的虛析構函數, 靜态成員函數外, 其他均為純虛函數, 不定義非靜态資料成員, 不提供構造函數, 提供的話,聲明為 protected;
  9. 為降低複雜性, 盡量不重載操作符, 模闆, 标準類中使用時提供文檔說明;
  10. 存取函數一般内聯在頭檔案中;
  11. 聲明次序: public -> protected -> private;
  12. 函數體盡量短小, 緊湊, 功能單一;

4. 來自 Google 的奇技¶

Google 用了很多自己實作的技巧 / 工具使 C++ 代碼更加健壯, 我們使用 C++ 的方式可能和你在其它地方見到的有所不同.

4.1. 智能指針¶

Tip

如果确實需要使用智能指針的話, scoped_ptr 完全可以勝任. 你應該隻在非常特定的情況下使用 std::tr1::shared_ptr, 例如 STL 容器中的對象. 任何情況下都不要使用 auto_ptr.

“智能” 指針看上去是指針, 其實是附加了語義的對象. 以 scoped_ptr 為例, scoped_ptr 被銷毀時, 它會删除所指向的對象. shared_ptr 也是如此, 并且 shared_ptr 實作了引用計數, 是以最後一個 shared_ptr 對象析構時, 如果檢測到引用次數為 0,就會銷毀所指向的對象.

一般來說,我們傾向于設計對象隸屬明确的代碼, 最明确的對象隸屬是根本不使用指針, 直接将對象作為一個作用域或局部變量使用. 另一種極端做法是, 引用計數指針不屬于任何對象. 這種方法的問題是容易導緻循環引用, 或者導緻某個對象無法删除的詭異狀态, 而且在每一次拷貝或指派時連原子操作都會很慢.

雖然不推薦使用引用計數指針, 但有些時候它們的确是最簡單有效的解決方案.

(YuleFox 注: 看來, Google 所謂的不同之處, 在于盡量避免使用智能指針 :D, 使用時也盡量局部化, 并且, 安全第一)

4.2. cpplint¶

Tip

使用 cpplint.py 檢查風格錯誤.

cpplint.py 是一個用來分析源檔案, 能檢查出多種風格錯誤的工具. 它不并完美, 甚至還會漏報和誤報, 但它仍然是一個非常有用的工具. 用行注釋 // NOLINT 可以忽略誤報.

某些項目會指導你如何使用他們的項目工具運作 cpplint.py. 如果你參與的項目沒有提供, 你可以單獨下載下傳 cpplint.py.

5. 其他 C++ 特性¶

5.1. 引用參數¶

Tip

是以按引用傳遞的參數必須加上 const.

定義:
在 C 語言中, 如果函數需要修改變量的值, 參數必須為指針, 如  int foo(int *pval). 在 C++ 中, 函數還可以聲明引用參數:  int foo(int&val).
優點:
定義引用參數防止出現  (*pval)++ 這樣醜陋的代碼. 像拷貝構造函數這樣的應用也是必需的. 而且更明确, 不接受  NULL 指針.
缺點:
容易引起誤解, 因為引用在文法上是值變量卻擁有指針的語義.
結論:
函數參數清單中, 所有引用參數都必須是  const:
void Foo(const string &in, string *out);      

事實上這在 Google Code 是一個硬性約定: 輸入參數是值參或 const 引用, 輸出參數為指針. 輸入參數可以是 const 指針, 但決不能是 非const 的引用參數.

在以下情況你可以把輸入參數定義為 const 指針: 你想強調參數不是拷貝而來的, 在對象生存周期内必須一直存在; 最好同時在注釋中詳細說明一下. bind2nd 和 mem_fun 等 STL 擴充卡不接受引用參數, 這種情況下你也必須把函數參數聲明成指針類型.

5.2. 函數重載¶

Tip

僅在輸入參數類型不同, 功能相同時使用重載函數 (含構造函數). 不要用函數重載模拟 預設函數參數.

定義:
你可以編寫一個參數類型為  const string& 的函數, 然後用另一個參數類型為  const char* 的函數重載它:
class MyClass { public: void Analyze(const string &text); void Analyze(const char *text, size_t textlen); };      
優點:
通過重載參數不同的同名函數, 令代碼更加直覺. 模闆化代碼需要重載, 同時為使用者帶來便利.
缺點:
限制使用重載的一個原因是在某個特定調用點很難确定到底調用的是哪個函數. 另一個原因是當派生類隻重載了某個函數的部分變體, 會令很多人對繼承的語義産生困惑. 此外在閱讀庫的使用者代碼時, 可能會因反對使用  預設函數參數 造成不必要的費解.
結論:
如果你想重載一個函數, 考慮讓函數名包含參數資訊, 例如, 使用  AppendString(),  AppendInt() 而不是  Append().

5.3. 預設參數¶

Tip

我們不允許使用預設函數參數.

優點:
多數情況下, 你寫的函數可能會用到很多的預設值, 但偶爾你也會修改這些預設值. 無須為了這些偶爾情況定義很多的函數, 用預設參數就能很輕松的做到這點.
缺點:
大家通常都是通過檢視别人的代碼來推斷如何使用 API. 用了預設參數的代碼更難維護, 從老代碼複制粘貼而來的新代碼可能隻包含部分參數. 當預設參數不适用于新代碼時可能會導緻重大問題.
結論:
我們規定所有參數必須明确指定, 迫使程式員了解 API 和各參數值的意義, 避免默默使用他們可能都還沒意識到的預設參數.

5.4. 變長數組和 alloca()¶

Tip

我們不允許使用變長數組和 alloca().

優點:
變長數組具有渾然天成的文法. 變長數組和  alloca() 也都很高效.
缺點:
變長數組和  alloca() 不是标準 C++ 的組成部分. 更重要的是, 它們根據資料大小動态配置設定堆棧記憶體, 會引起難以發現的記憶體越界 bugs: “在我的機器上運作的好好的, 釋出後卻莫名其妙的挂掉了”.
結論:
使用安全的記憶體配置設定器, 如  scoped_ptr /  scoped_array.

5.5. 友元¶

Tip

我們允許合理的使用友元類及友元函數.

通常友元應該定義在同一檔案内, 避免代碼讀者跑到其它檔案查找使用該私有成員的類. 經常用到友元的一個地方是将 FooBuilder 聲明為Foo 的友元, 以便 FooBuilder 正确構造 Foo 的内部狀态, 而無需将該狀态暴露出來. 某些情況下, 将一個單元測試類聲明成待測類的友元會很友善.

友元擴大了 (但沒有打破) 類的封裝邊界. 某些情況下, 相對于将類成員聲明為 public, 使用友元是更好的選擇, 尤其是如果你隻允許另一個類通路該類的私有成員時. 當然, 大多數類都隻應該通過其提供的公有成員進行互操作.

5.6. 異常¶

Tip

我們不使用 C++ 異常.

優點:
  • 異常允許上層應用決定如何處理在底層嵌套函數中 “不可能出現的” 失敗, 不像錯誤碼記錄那麼含糊又易出錯;
  • 很多現代語言都使用異常. 引入異常使得 C++ 與 Python, Java 以及其它 C++ 相近的語言更加相容.
  • 許多第三方 C++ 庫使用異常, 禁用異常将導緻很難內建這些庫.
  • 異常是處理構造函數失敗的唯一方法. 雖然可以通過工廠函數或 Init() 方法替代異常, 但他們分别需要堆配置設定或新的 “無效” 狀态;
  • 在測試架構中使用異常确實很友善.
缺點:
  • 在現有函數中添加 throw 語句時, 你必須檢查所有調用點. 所有調用點得至少有基本的異常安全保護, 否則永遠捕獲不到異常, 隻好 “開心的” 接受程式終止的結果. 例如, 如果 f() 調用了 g(), g() 又調用了 h(), h 抛出的異常被 f 捕獲, g 要當心了, 很可能會因疏忽而未被妥善清理.
  • 更普遍的情況是, 如果使用異常, 光憑檢視代碼是很難評估程式的控制流: 函數傳回點可能在你意料之外. 這回導緻代碼管理和調試困難. 你可以通過規定何時何地如何使用異常來降低開銷, 但是讓開發人員必須掌握并了解這些規定帶來的代價更大.
  • 異常安全要求同時采用 RAII 和不同程式設計實踐. 要想輕松編寫正确的異常安全代碼, 需要大量的支撐機制配合. 另外, 要避免代碼讀者去了解整個調用結構圖, 異常安全代碼必須把寫持久化狀态的邏輯部分隔離到 “送出” 階段. 它在帶來好處的同時, 還有成本 (也許你不得不為了隔離 “送出” 而整出令人費解的代碼). 允許使用異常會驅使我們不斷為此付出代價, 即使我們覺得這很不劃算.
  • 啟用異常使生成的二進制檔案體積變大, 延長了編譯時間 (或許影響不大), 還可能增加位址空間壓力;
  • 異常的實用性可能會慫恿開發人員在不恰當的時候抛出異常, 或者在不安全的地方從異常中恢複. 例如, 處理非法使用者輸入時就不應該抛出異常. 如果我們要完全列出這些限制, 這份風格指南會長出很多!
結論:

從表面上看, 使用異常利大于弊, 尤其是在新項目中. 但是對于現有代碼, 引入異常會牽連到所有相關代碼. 如果新項目允許異常向外擴散, 在跟以前未使用異常的代碼整合時也将是個麻煩. 因為 Google 現有的大多數 C++ 代碼都沒有異常處理, 引入帶有異常處理的新代碼相當困難.

鑒于 Google 現有代碼不接受異常, 在現有代碼中使用異常比在新項目中使用的代價多少要大一些. 遷移過程比較慢, 也容易出錯. 我們不相信異常的使用有效替代方案, 如錯誤代碼, 斷言等會造成嚴重負擔.

我們并不是基于哲學或道德層面反對使用異常, 而是在實踐的基礎上. 我們希望在 Google 使用我們自己的開源項目, 但項目中使用異常會為此帶來不便, 是以我們也建議不要在 Google 的開源項目中使用異常. 如果我們需要把這些項目推倒重來顯然不太現實.

對于 Windows 代碼來說, 有個 特例.

(YuleFox 注: 對于異常處理, 顯然不是短短幾句話能夠說清楚的, 以構造函數為例, 很多 C++ 書籍上都提到當構造失敗時隻有異常可以處理, Google 禁止使用異常這一點, 僅僅是為了自身的友善, 說大了, 無非是基于軟體管理成本上, 實際使用中還是自己決定)

5.7. 運作時類型識别¶

Tip

我們禁止使用 RTTI.

定義:
RTTI 允許程式員在運作時識别 C++ 類對象的類型.
優點:

RTTI 在某些單元測試中非常有用. 比如進行工廠類測試時, 用來驗證一個建立對象是否為期望的動态類型.

除測試外, 極少用到.

缺點:
在運作時判斷類型通常意味着設計問題. 如果你需要在運作期間确定一個對象的類型, 這通常說明你需要考慮重新設計你的類.
結論:

除單元測試外, 不要使用 RTTI. 如果你發現自己不得不寫一些行為邏輯取決于對象類型的代碼, 考慮換一種方式判斷對象類型.

如果要實作根據子類類型來确定執行不同邏輯代碼, 虛函數無疑更合适. 在對象内部就可以處理類型識别問題.

如果要在對象外部的代碼中判斷類型, 考慮使用雙重分派方案, 如通路者模式. 可以友善的在對象本身之外确定類的類型.

如果你認為上面的方法你真的掌握不了, 你可以使用 RTTI, 但務必請三思 :-) . 不要試圖手工實作一個貌似 RTTI 的替代方案, 我們反對使用 RTTI 的理由, 同樣适用于那些在類型繼承體系上使用類型标簽的替代方案.

5.8. 類型轉換¶

Tip

使用 C++ 的類型轉換, 如 static_cast<>(). 不要使用 int y = (int)x 或 int y = int(x) 等轉換方式;

定義:
C++ 采用了有别于 C 的類型轉換機制, 對轉換操作進行歸類.
優點:
C 語言的類型轉換問題在于模棱兩可的操作; 有時是在做強制轉換 (如  (int)3.5), 有時是在做類型轉換 (如  (int)"hello"). 另外, C++ 的類型轉換在查找時更醒目.
缺點:
惡心的文法.
結論:
不要使用 C 風格類型轉換. 而應該使用 C++ 風格.
  • 用 static_cast 替代 C 風格的值轉換, 或某個類指針需要明确的向上轉換為父類指針時.
  • 用 const_cast 去掉 const 限定符.
  • 用 reinterpret_cast 指針類型和整型或其它指針之間進行不安全的互相轉換. 僅在你對所做一切了然于心時使用.
  • dynamic_cast 測試代碼以外不要使用. 除非是單元測試, 如果你需要在運作時确定類型資訊, 說明有 設計缺陷.

5.9. 流¶

Tip

隻在記錄日志時使用流.

定義:
流用來替代  printf() 和  scanf().
優點:
有了流, 在列印時不需要關心對象的類型. 不用擔心格式化字元串與參數清單不比對 (雖然在 gcc 中使用  printf 也不存在這個問題). 流的構造和析構函數會自動打開和關閉對應的檔案.
缺點:
流使得  pread() 等功能函數很難執行. 如果不使用  printf 風格的格式化字元串, 某些格式化操作 (尤其是常用的格式字元串  %.*s) 用流處理性能是很低的. 流不支援字元串操作符重新排序 (%1s), 而這一點對于軟體國際化很有用.
結論:

不要使用流, 除非是日志接口需要. 使用 printf 之類的代替.

使用流還有很多利弊, 但代碼一緻性勝過一切. 不要在代碼中使用流.

拓展讨論:
對這一條規則存在一些争論, 這兒給出點深層次原因. 回想一下唯一性原則 (Only One Way): 我們希望在任何時候都隻使用一種确定的 I/O 類型, 使代碼在所有 I/O 處都保持一緻. 是以, 我們不希望使用者來決定是使用流還是 printf + read/write. 相反, 我們應該決定到底用哪一種方式. 把日志作為特例是因為日志是一個非常獨特的應用, 還有一些是曆史原因.
流的支援者們主張流是不二之選, 但觀點并不是那麼清晰有力. 他們指出的流的每個優勢也都是其劣勢. 流最大的優勢是在輸出時不需要關心列印對象的類型. 這是一個亮點. 同時, 也是一個不足: 你很容易用錯類型, 而編譯器不會報警. 使用流時容易造成的這類錯誤:
cout << this; // Prints the address cout << *this; // Prints the contents      
由于 << 被重載, 編譯器不會報錯. 就因為這一點我們反對使用操作符重載.
有人說  printf 的格式化醜陋不堪, 易讀性差, 但流也好不到哪兒去. 看看下面兩段代碼吧, 實作相同的功能, 哪個更清晰?
cerr << "Error connecting to '" << foo->bar()->hostname.first << ":" << foo->bar()->hostname.second << ": " <<strerror(errno); fprintf(stderr, "Error connecting to '%s:%u: %s", foo->bar()->hostname.first, foo->bar()->hostname.second, strerror(errno));      

你可能會說, “把流封裝一下就會比較好了”, 這兒可以, 其他地方呢? 而且不要忘了, 我們的目标是使語言更緊湊, 而不是添加一些别人需要學習的新裝備.

每一種方式都是各有利弊, “沒有最好, 隻有更适合”. 簡單性原則告誡我們必須從中選擇其一, 最後大多數決定采用 printf + read/write.

5.10. 前置自增和自減¶

Tip

對于疊代器和其他模闆對象使用字首形式 (++i) 的自增, 自減運算符.

定義:
對于變量在自增 ( ++i 或  i++) 或自減 ( --i 或  i--) 後表達式的值又沒有沒用到的情況下, 需要确定到底是使用前置還是後置的自增 (自減).
優點:
不考慮傳回值的話, 前置自增 ( ++i) 通常要比後置自增 ( i++) 效率更高. 因為後置自增 (或自減) 需要對表達式的值  i 進行一次拷貝. 如果  i 是疊代器或其他非數值類型, 拷貝的代價是比較大的. 既然兩種自增方式實作的功能一樣, 為什麼不總是使用前置自增呢?
缺點:
在 C 開發中, 當表達式的值未被使用時, 傳統的做法是使用後置自增, 特别是在  for 循環中. 有些人覺得後置自增更加易懂, 因為這很像自然語言, 主語 ( i) 在謂語動詞 ( ++) 前.
結論:
對簡單數值 (非對象), 兩種都無所謂. 對疊代器和模闆類型, 使用前置自增 (自減).

5.11. const 的使用¶

Tip

我們強烈建議你在任何可能的情況下都要使用 const.

定義:
在聲明的變量或參數前加上關鍵字  const 用于指明變量值不可被篡改 (如  const int foo ). 為類中的函數加上  const 限定符表明該函數不會修改類成員變量的狀态 (如  class Foo { int Bar(char c) const; };).
優點:
大家更容易了解如何使用變量. 編譯器可以更好地進行類型檢測, 相應地, 也能生成更好的代碼. 人們對編寫正确的代碼更加自信, 因為他們知道所調用的函數被限定了能或不能修改變量值. 即使是在無鎖的多線程程式設計中, 人們也知道什麼樣的函數是安全的.
缺點:
const 是入侵性的: 如果你向一個函數傳入  const 變量, 函數原型聲明中也必須對應  const 參數 (否則變量需要  const_cast 類型轉換), 在調用庫函數時顯得尤其麻煩.
結論:
const 變量, 資料成員, 函數和參數為編譯時類型檢測增加了一層保障; 便于盡早發現錯誤. 是以, 我們強烈建議在任何可能的情況下使用const:
  • 如果函數不會修改傳入的引用或指針類型參數, 該參數應聲明為 const.
  • 盡可能将函數聲明為 const. 通路函數應該總是 const. 其他不會修改任何資料成員, 未調用非 const 函數, 不會傳回資料成員非 const 指針或引用的函數也應該聲明成 const.
  • 如果資料成員在對象構造之後不再發生變化, 可将其定義為 const.

然而, 也不要發了瘋似的使用 const. 像 const int * const * const x; 就有些過了, 雖然它非常精确的描述了常量 x. 關注真正有幫助意義的資訊: 前面的例子寫成 const int** x 就夠了.

關鍵字 mutable 可以使用, 但是在多線程中是不安全的, 使用時首先要考慮線程安全.

const 的位置:

有人喜歡 int const *foo 形式, 不喜歡 const int* foo, 他們認為前者更一緻是以可讀性也更好: 遵循了 const 總位于其描述的對象之後的原則. 但是一緻性原則不适用于此, “不要過度使用” 的聲明可以取消大部分你原本想保持的一緻性. 将 const 放在前面才更易讀, 因為在自然語言中形容詞 (const) 是在名詞 (int) 之前.

這是說, 我們提倡但不強制 const 在前. 但要保持代碼的一緻性! (yospaly 注: 也就是不要在一些地方把 const 寫在類型前面, 在其他地方又寫在後面, 确定一種寫法, 然後保持一緻.)

5.12. 整型¶

Tip

C++ 内建整型中, 僅使用 int. 如果程式中需要不同大小的變量, 可以使用 <stdint.h> 中長度精确的整型, 如 int16_t.

定義:
C++ 沒有指定整型的大小. 通常人們假定  short 是 16 位,  int``是 32 位, ``long 是 32 位,  long long 是 64 位.
優點:
保持聲明統一.
缺點:
C++ 中整型大小因編譯器和體系結構的不同而不同.
結論:

<stdint.h> 定義了 int16_t, uint32_t, int64_t 等整型, 在需要確定整型大小時可以使用它們代替 short, unsigned long long 等. 在 C 整型中, 隻使用 int. 在合适的情況下, 推薦使用标準類型如 size_t 和 ptrdiff_t.

如果已知整數不會太大, 我們常常會使用 int, 如循環計數. 在類似的情況下使用原生類型 int. 你可以認為 int 至少為 32 位, 但不要認為它會多于 32 位. 如果需要 64 位整型, 用 int64_t 或 uint64_t.

對于大整數, 使用 int64_t.

不要使用 uint32_t 等無符号整型, 除非你是在表示一個位組而不是一個數值, 或是你需要定義二進制補碼溢出. 尤其是不要為了指出數值永不會為負, 而使用無符号類型. 相反, 你應該使用斷言來保護資料.

關于無符号整數:
有些人, 包括一些教科書作者, 推薦使用無符号類型表示非負數. 這種做法試圖達到自我文檔化. 但是, 在 C 語言中, 這一優點被由其導緻的 bug 所淹沒. 看看下面的例子:
for (unsigned int i = foo.Length()-1; i >= 0; --i) ...      

上述循環永遠不會退出! 有時 gcc 會發現該 bug 并報警, 但大部分情況下都不會. 類似的 bug 還會出現在比較有符合變量和無符号變量時. 主要是 C 的類型提升機制會緻使無符号類型的行為出乎你的意料.

是以, 使用斷言來指出變量為非負數, 而不是使用無符号型!

5.13. 64 位下的可移植性¶

Tip

代碼應該對 64 位和 32 位系統友好. 處理列印, 比較, 結構體對齊時應切記:

  • 對于某些類型, printf() 的訓示符在 32 位和 64 位系統上可移植性不是很好. C99 标準定義了一些可移植的格式化訓示符. 不幸的是, MSVC 7.1 并非全部支援, 而且标準中也有所遺漏, 是以有時我們不得不自己定義一個醜陋的版本 (頭檔案 inttypes.h 仿标準風格):
    // printf macros for size_t, in the style of inttypes.h #ifdef _LP64 #define __PRIS_PREFIX "z" #else #define __PRIS_PREFIX #endif // Use these macros after a % in a printf format string // to get correct 32/64 bit behavior, like this: // size_t size = records.size(); // printf("%"PRIuS"\n", size); #define PRIdS __PRIS_PREFIX "d" #define PRIxS __PRIS_PREFIX "x" #define PRIuS __PRIS_PREFIX "u" #define PRIXS __PRIS_PREFIX "X" #define PRIoS __PRIS_PREFIX "o"      
    類型 不要使用 使用 備注
    void *(或其他指針類型) %lx %p
    int64_t %qd, %lld %"PRId64"
    uint64_t %qu, %llu, %llx %"PRIu64", %"PRIx64"
    size_t %u %"PRIuS", %"PRIxS" C99 規定 %zu
    ptrdiff_t %d %"PRIdS" C99 規定 %zd
    注意 PRI* 宏會被編譯器擴充為獨立字元串. 是以如果使用非常量的格式化字元串, 需要将宏的值而不是宏名插入格式中. 使用 PRI* 宏同樣可以在 % 後包含長度訓示符. 例如, printf("x = %30"PRIuS"\n", x) 在 32 位 Linux 上将被展開為 printf("x = %30" "u" "\n", x), 編譯器當成 printf("x = %30u\n", x) 處理 (yospaly 注: 這在 MSVC 6.0 上行不通, VC 6 編譯器不會自動把引号間隔的多個字元串連接配接一個長字元串).
  • 記住 sizeof(void *) != sizeof(int). 如果需要一個指針大小的整數要用 intptr_t.
  • 你要非常小心的對待結構體對齊, 尤其是要持久化到磁盤上的結構體 (yospaly 注: 持久化 - 将資料按位元組流順序儲存在磁盤檔案或資料庫中). 在 64 位系統中, 任何含有 int64_t/uint64_t 成員的類/結構體, 預設都以 8 位元組在結尾對齊. 如果 32 位和 64 位代碼要共用持久化的結構體, 需要確定兩種體系結構下的結構體對齊一緻. 大多數編譯器都允許調整結構體對齊. gcc 中可使用 __attribute__((packed)). MSVC 則提供了 #pragma pack() 和 __declspec(align()) (YuleFox 注, 解決方案的項目屬性裡也可以直接設定).
  • 建立 64 位常量時使用 LL 或 ULL 作為字尾, 如:
    int64_t my_value = 0×123456789LL; uint64_t my_mask = 3ULL << 48;      
  • 如果你确實需要 32 位和 64 位系統具有不同代碼, 可以使用 #ifdef _LP64 指令來切分 32/64 位代碼. (盡量不要這麼做, 如果非用不可, 盡量使修改局部化)

5.14. 預處理宏¶

Tip

使用宏時要非常謹慎, 盡量以内聯函數, 枚舉和常量代替之.

宏意味着你和編譯器看到的代碼是不同的. 這可能會導緻異常行為, 尤其因為宏具有全局作用域.

值得慶幸的是, C++ 中, 宏不像在 C 中那麼必不可少. 以往用宏展開性能關鍵的代碼, 現在可以用内聯函數替代. 用宏表示常量可被 const 變量代替. 用宏 “縮寫” 長變量名可被引用代替. 用宏進行條件編譯... 這個, 千萬别這麼做, 會令測試更加痛苦 (#define 防止頭檔案重包含當然是個特例).

宏可以做一些其他技術無法實作的事情, 在一些代碼庫 (尤其是底層庫中) 可以看到宏的某些特性 (如用 # 字元串化, 用 ## 連接配接等等). 但在使用前, 仔細考慮一下能不能不使用宏達到同樣的目的.

下面給出的用法模式可以避免使用宏帶來的問題; 如果你要宏, 盡可能遵守:

  • 不要在 .h 檔案中定義宏.
  • 在馬上要使用時才進行 #define, 使用後要立即 #undef.
  • 不要隻是對已經存在的宏使用#undef,選擇一個不會沖突的名稱;
  • 不要試圖使用展開後會導緻 C++ 構造不穩定的宏, 不然也至少要附上文檔說明其行為.

5.15. 0 和 NULL¶

Tip

整數用 0, 實數用 0.0, 指針用 NULL, 字元 (串) 用 '\0'.

整數用 0, 實數用 0.0, 這一點是毫無争議的.

對于指針 (位址值), 到底是用 0 還是 NULL, Bjarne Stroustrup 建議使用最原始的 0. 我們建議使用看上去像是指針的 NULL, 事實上一些 C++ 編譯器 (如 gcc 4.1.0) 對 NULL 進行了特殊的定義, 可以給出有用的警告資訊, 尤其是 sizeof(NULL) 和 sizeof(0) 不相等的情況.

字元 (串) 用 '\0', 不僅類型正确而且可讀性好.

5.16. sizeof¶

Tip

盡可能用 sizeof(varname) 代替 sizeof(type).

使用  sizeof(varname) 是因為當代碼中變量類型改變時會自動更新. 某些情況下  sizeof(type) 或許有意義, 但還是要盡量避免, 因為它會導緻變量類型改變後不能同步.
Struct data; Struct data; memset(&data, 0, sizeof(data));      
Warning
memset(&data, 0, sizeof(Struct));      

5.17. Boost 庫¶

Tip

隻使用 Boost 中被認可的庫.

定義:
Boost 庫集 是一個廣受歡迎, 經過同行鑒定, 免費開源的 C++ 庫集.
優點:
Boost代碼品質普遍較高, 可移植性好, 填補了 C++ 标準庫很多空白, 如型别的特性, 更完善的綁定器, 更好的智能指針, 同時還提供了  TR1(标準庫擴充) 的實作.
缺點:
某些 Boost 庫提倡的程式設計實踐可讀性差, 比如元程式設計和其他進階模闆技術, 以及過度 “函數化” 的程式設計風格.
結論:
為了向閱讀和維護代碼的人員提供更好的可讀性, 我們隻允許使用 Boost 一部分經認可的特性子集. 目前允許使用以下庫:
  • Compressed Pair : boost/compressed_pair.hpp
  • Pointer Container : boost/ptr_container (序列化除外)
  • Array : boost/array.hpp
  • The Boost Graph Library (BGL) : boost/graph (序列化除外)
  • Property Map : boost/property_map.hpp
  • Iterator 中處理疊代器定義的部分 : boost/iterator/iterator_adaptor.hpp, boost/iterator/iterator_facade.hpp, 以及boost/function_output_iterator.hpp

我們正在積極考慮增加其它 Boost 特性, 是以清單中的規則将不斷變化.

6. 命名約定¶

最重要的一緻性規則是命名管理. 命名風格快速獲知名字代表是什麼東東: 類型? 變量? 函數? 常量? 宏 ... ? 甚至不需要去查找類型聲明. 我們大腦中的模式比對引擎可以非常可靠的處理這些命名規則.

命名規則具有一定随意性, 但相比按個人喜好命名, 一緻性更重, 是以不管你怎麼想, 規則總歸是規則.

6.1. 通用命名規則¶

Tip

函數命名, 變量命名, 檔案命名應具備描述性; 不要過度縮寫. 類型和變量應該是名詞, 函數名可以用 “指令性” 動詞.

如何命名:
盡可能給出描述性的名稱. 不要節約行空間, 讓别人很快了解你的代碼更重要. 好的命名風格:
int num_errors; // Good. int num_completed_connections; // Good.      
糟糕的命名使用含糊的縮寫或随意的字元:
int n; // Bad - meaningless. int nerr; // Bad - ambiguous abbreviation. int n_comp_conns; // Bad - ambiguous abbreviation.      

類型和變量名一般為名詞: 如 FileOpener, num_errors.

函數名通常是指令性的 (确切的說它們應該是指令), 如 OpenFile(), set_num_errors(). 取值函數是個特例 (在 函數命名 處詳細闡述), 函數名和它要取值的變量同名.

縮寫:
除非該縮寫在其它地方都非常普遍, 否則不要使用. 例如:
// Good // These show proper names with no abbreviations. int num_dns_connections; // 大部分人都知道 "DNS" 是啥意思. intprice_count_reader; // OK, price count. 有意義.      
Warning
// Bad! // Abbreviations can be confusing or ambiguous outside a small group. int wgc_connections; // Only your group knows what this stands for. int pc_reader; // Lots of things can be abbreviated "pc".      
永遠不要用省略字母的縮寫:
int error_count; // Good. int error_cnt; // Bad.      

6.2. 檔案命名¶

Tip

檔案名要全部小寫, 可以包含下劃線 (_) 或連字元 (-). 按項目約定來.

可接受的檔案命名:

my_useful_class.cc my-useful-class.cc myusefulclass.cc      

C++ 檔案要以 .cc 結尾, 頭檔案以 .h 結尾.

不要使用已經存在于 /usr/include 下的檔案名 (yospaly 注: 即編譯器搜尋系統頭檔案的路徑), 如 db.h.

通常應盡量讓檔案名更加明确. http_server_logs.h 就比 logs.h 要好. 定義類時檔案名一般成對出現, 如 foo_bar.h 和 foo_bar.cc, 對應于類 FooBar.

内聯函數必須放在 .h 檔案中. 如果内聯函數比較短, 就直接放在 .h 中. 如果代碼比較長, 可以放到以 -inl.h 結尾的檔案中. 對于包含大量内聯代碼的類, 可以使用三個檔案:

url_table.h // The class declaration. url_table.cc // The class definition. url_table-inl.h // Inline functions that include lots of code.      

參考 -inl.h 檔案 一節.

6.3. 類型命名¶

Tip

類型名稱的每個單詞首字母均大寫, 不包含下劃線: MyExcitingClass, MyExcitingEnum.

所有類型命名 —— 類, 結構體, 類型定義 ( typedef), 枚舉 —— 均使用相同約定. 例如:
// classes and structs class UrlTable { ... class UrlTableTester { ... struct UrlTableProperties { ... // typedefstypedef hash_map<UrlTableProperties *, string> PropertiesMap; // enums enum UrlTableErrors { ...      

6.4. 變量命名¶

Tip

變量名一律小寫, 單詞之間用下劃線連接配接. 類的成員變量以下劃線結尾, 如:

my_exciting_local_variable my_exciting_member_variable_      
普通變量命名:
舉例:
string table_name; // OK - uses underscore. string tablename; // OK - all lowercase.      
Warning
string tableName; // Bad - mixed case.      
結構體變量:
結構體的資料成員可以和普通變量一樣, 不用像類那樣接下劃線:
struct UrlTableProperties { string name; int num_entries; }      
結構體與類的讨論參考 結構體 vs. 類 一節.
全局變量:
對全局變量沒有特别要求, 少用就好, 但如果你要用, 可以用  g_ 或其它标志作為字首, 以便更好的區分局部變量.

6.5. 常量命名¶

Tip

在名稱前加 k: kDaysInAWeek.

所有編譯時常量, 無論是局部的, 全局的還是類中的, 和其他變量稍微差別一下.  k 後接大寫字母開頭的單詞::
const int kDaysInAWeek = 7;

6.6. 函數命名¶

Tip

正常函數使用大小寫混合, 取值和設值函數則要求與變量名比對: MyExcitingFunction(), MyExcitingMethod(),my_exciting_member_variable(), set_my_exciting_member_variable().

正常函數:
函數名的每個單詞首字母大寫, 沒有下劃線:
AddTableEntry() DeleteUrl()      
取值和設值函數:
取值和設值函數要與存取的變量名比對. 這兒摘錄一個類,  num_entries_ 是該類的執行個體變量:
class MyClass { public: ... int num_entries() const { return num_entries_; } void set_num_entries(int num_entries) {num_entries_ = num_entries; } private: int num_entries_; };      
其它非常短小的内聯函數名也可以用小寫字母, 例如. 如果你在循環中調用這樣的函數甚至都不用緩存其傳回值, 小寫命名就可以接受.

6.7. 名字空間命名¶

Tip

名字空間用小寫字母命名, 并基于項目名稱和目錄結構: google_awesome_project.

關于名字空間的讨論和如何命名, 參考 名字空間 一節.

6.8. 枚舉命名¶

Tip

枚舉的命名應當和 常量 或 宏 一緻: kEnumName 或是 ENUM_NAME.

單獨的枚舉值應該優先采用  常量 的命名方式. 但  宏 方式的命名也可以接受. 枚舉名  UrlTableErrors (以及  AlternateUrlTableErrors) 是類型, 是以要用大小寫混合的方式.
enum UrlTableErrors { kOK = 0, kErrorOutOfMemory, kErrorMalformedInput, }; enum AlternateUrlTableErrors { OK = 0,OUT_OF_MEMORY = 1, MALFORMED_INPUT = 2, };      

2009 年 1 月之前, 我們一直建議采用 宏 的方式命名枚舉值. 由于枚舉值和宏之間的命名沖突, 直接導緻了很多問題. 由此, 這裡改為優先選擇常量風格的命名方式. 新代碼應該盡可能優先使用常量風格. 但是老代碼沒必要切換到常量風格, 除非宏風格确實會産生編譯期問題.

6.9. 宏命名¶

Tip

你并不打算 使用宏, 對吧? 如果你一定要用, 像這樣命名: MY_MACRO_THAT_SCARES_SMALL_CHILDREN.

參考 預處理宏 <preprocessor-macros>; 通常 不應該 使用宏. 如果不得不用, 其命名像枚舉命名一樣全部大寫, 使用下劃線:

#define ROUND(x) ... #define PI_ROUNDED 3.0      

6.10. 命名規則的特例¶

Tip

如果你命名的實體與已有 C/C++ 實體相似, 可參考現有命名政策.

bigopen():
函數名, 參照  open() 的形式
uint:
typedef
bigpos:
struct 或  class, 參照  pos 的形式
sparse_hash_map:
STL 相似實體; 參照 STL 命名約定
LONGLONG_MAX:
常量, 如同  INT_MAX

7. 注釋¶

注釋雖然寫起來很痛苦, 但對保證代碼可讀性至關重要. 下面的規則描述了如何注釋以及在哪兒注釋. 當然也要記住: 注釋固然很重要, 但最好的代碼本身應該是自文檔化. 有意義的類型名和變量名, 要遠勝過要用注釋解釋的含糊不清的名字.

你寫的注釋是給代碼讀者看的: 下一個需要了解你的代碼的人. 慷慨些吧, 下一個人可能就是你!

7.1. 注釋風格¶

Tip

使用 // 或 , 統一就好.

// 或  都可以; 但 // 更 常用. 要在如何注釋及注釋風格上確定統一.

7.2. 檔案注釋¶

Tip

在每一個檔案開頭加入版權公告, 然後是檔案内容描述.

法律公告和作者資訊:
每個檔案都應該包含以下項, 依次是:
  • 版權聲明 (比如, Copyright 2008 Google Inc.)
  • 許可證. 為項目選擇合适的許可證版本 (比如, Apache 2.0, BSD, LGPL, GPL)
  • 作者: 辨別檔案的原始作者.
如果你對原始作者的檔案做了重大修改, 将你的資訊添加到作者資訊裡. 這樣當其他人對該檔案有疑問時可以知道該聯系誰.
檔案内容:

緊接着版權許可和作者資訊之後, 每個檔案都要用注釋描述檔案内容.

通常, .h 檔案要對所聲明的類的功能和用法作簡單說明. .cc 檔案通常包含了更多的實作細節或算法技巧讨論, 如果你感覺這些實作細節或算法技巧讨論對于了解 .h 檔案有幫助, 可以該注釋挪到 .h, 并在 .cc 中指出文檔在 .h.

不要簡單的在 .h 和 .cc 間複制注釋. 這種偏離了注釋的實際意義.

7.3. 類注釋¶

Tip

每個類的定義都要附帶一份注釋, 描述類的功能和用法.

// Iterates over the contents of a GargantuanTable. Sample usage: // GargantuanTable_Iterator* iter = table->NewIterator(); // for (iter->Seek("foo"); !iter->done(); iter->Next()) { // process(iter->key(), iter->value()); // } // delete iter; class GargantuanTable_Iterator { ... };      

如果你覺得已經在檔案頂部較長的描述了該類, 想直接簡單的來上一句 “完整描述見檔案頂部” 也不打緊, 但務必確定有這類注釋.

如果類有任何同步前提, 文檔說明之. 如果該類的執行個體可被多線程通路, 要特别注意文檔說明多線程環境下相關的規則和常量使用.

7.4. 函數注釋¶

Tip

函數聲明處注釋描述函數功能; 定義處描述函數實作.

函數聲明:

注釋位于聲明之前, 對函數功能及用法進行描述. 注釋使用叙述式 (“Opens the file”) 而非指令式 (“Open the file”); 注釋隻是為了描述函數, 而不是指令函數做什麼. 通常, 注釋不會描述函數如何工作. 那是函數定義部分的事情.

函數聲明處注釋的内容:

  • 函數的輸入輸出.
  • 對類成員函數而言: 函數調用期間對象是否需要保持引用參數, 是否會釋放這些參數.
  • 如果函數配置設定了空間, 需要由調用者釋放.
  • 參數是否可以為 NULL.
  • 是否存在函數使用上的性能隐患.
  • 如果函數是可重入的, 其同步前提是什麼?
舉例如下:
// Returns an iterator for this table. It is the client's // responsibility to delete the iterator when it is done with it, // and it must not use the iterator once the GargantuanTable object // on which the iterator was created has been deleted. // // The iterator is initially positioned at the beginning of the table. // // This method is equivalent to: // Iterator* iter = table->NewIterator(); // iter->Seek(""); // return iter; // If you are going to immediately seek to another place in the // returned iterator, it will be faster to use NewIterator() // and avoid the extra seek. Iterator*GetIterator() const;      
但也要避免羅羅嗦嗦, 或做些顯而易見的說明. 下面的注釋就沒有必要加上 “returns false otherwise”, 因為已經暗含其中了:
// Returns true if the table cannot hold any more entries. bool IsTableFull();      
注釋構造/析構函數時, 切記讀代碼的人知道構造/析構函數是幹啥的, 是以 “destroys this object” 這樣的注釋是沒有意義的. 注明構造函數對參數做了什麼 (例如, 是否取得指針所有權) 以及析構函數清理了什麼. 如果都是些無關緊要的内容, 直接省掉注釋. 析構函數前沒有注釋是很正常的.
函數定義:

每個函數定義時要用注釋說明函數功能和實作要點. 比如說說你用的程式設計技巧, 實作的大緻步驟, 或解釋如此實作的理由, 為什麼前半部分要加鎖而後半部分不需要.

不要 從 .h 檔案或其他地方的函數聲明處直接複制注釋. 簡要重述函數功能是可以的, 但注釋重點要放在如何實作上.

7.5. 變量注釋¶

Tip

通常變量名本身足以很好說明變量用途. 某些情況下, 也需要額外的注釋說明.

類資料成員:
每個類資料成員 (也叫執行個體變量或成員變量) 都應該用注釋說明用途. 如果變量可以接受 NULL 或 -1 等警戒值, 須加以說明. 比如:
private: // Keeps track of the total number of entries in the table. // Used to ensure we do not go over the limit. -1 means // that we don't yet know how many entries the table has. int num_total_entries_;      
全局變量:
和資料成員一樣, 所有全局變量也要注釋說明含義及用途. 比如:
// The total number of tests cases that we run through in this regression test. const int kNumTestCases = 6;      

7.6. 實作注釋¶

Tip

對于代碼中巧妙的, 晦澀的, 有趣的, 重要的地方加以注釋.

代碼前注釋:
巧妙或複雜的代碼段前要加注釋. 比如:
// Divide result by two, taking into account that x // contains the carry from the add. for (int i = 0; i < result->size(); i++) { x = (x << 8) + (*result)[i]; (*result)[i] = x >> 1; x &= 1; }      
行注釋:
比較隐晦的地方要在行尾加入注釋. 在行尾空兩格進行注釋. 比如:
// If we have enough memory, mmap the data portion too. mmap_budget = max<int64>(0, mmap_budget - index_->length()); if(mmap_budget >= data_size_ && !MmapData(mmap_chunk_bytes, mlock)) return; // Error already logged.      

注意, 這裡用了兩段注釋分别描述這段代碼的作用, 和提示函數傳回時錯誤已經被記入日志.

如果你需要連續進行多行注釋, 可以使之對齊獲得更好的可讀性:

DoSomething(); // Comment here so the comments line up. DoSomethingElseThatIsLonger(); // Comment here so there are two spaces between // the code and the comment. { // One space before comment when opening a new scope is allowed, // thus the comment lines up with the following comments and code. DoSomethingElse(); // Two spaces before line comments normally. }      
NULL, true/false, 1, 2, 3...:
向函數傳入 NULL, 布爾值或整數時, 要注釋說明含義, 或使用常量讓代碼望文知意. 例如, 對比:
Warning
bool success = CalculateSomething(interesting_value, 10, false, NULL); // What are these arguments??      
和:
bool success = CalculateSomething(interesting_value, 10, // Default base value. false, // Not the first time we're calling this. NULL); // No callback.      
或使用常量或描述性變量:
const int kDefaultBaseValue = 10; const bool kFirstTimeCalling = false; Callback *null_callback = NULL; bool success =CalculateSomething(interesting_value, kDefaultBaseValue, kFirstTimeCalling, null_callback);      
不允許:

注意 永遠不要 用自然語言翻譯代碼作為注釋. 要假設讀代碼的人 C++ 水準比你高, 即便他/她可能不知道你的用意:

Warning

// 現在, 檢查 b 數組并確定 i 是否存在, // 下一個元素是 i+1. ... // 天哪. 令人崩潰的注釋.      

7.7. 标點, 拼寫和文法¶

Tip

注意标點, 拼寫和文法; 寫的好的注釋比差的要易讀的多.

注釋的通常寫法是包含正确大小寫和結尾句号的完整語句. 短一點的注釋 (如代碼行尾注釋) 可以随意點, 依然要注意風格的一緻性. 完整的語句可讀性更好, 也可以說明該注釋是完整的, 而不是一些不成熟的想法.

雖然被别人指出該用分号時卻用了逗号多少有些尴尬, 但清晰易讀的代碼還是很重要的. 正确的标點, 拼寫和文法對此會有所幫助.

7.8. TODO 注釋¶

Tip

對那些臨時的, 短期的解決方案, 或已經夠好但仍不完美的代碼使用 TODO 注釋.

TODO 注釋要使用全大寫的字元串 TODO, 在随後的圓括号裡寫上你的大名, 郵件位址, 或其它身份辨別. 冒号是可選的. 主要目的是讓添加注釋的人 (也是可以請求提供更多細節的人) 可根據規範的 TODO 格式進行查找. 添加 TODO 注釋并不意味着你要自己來修正.

// TODO([email protected]): Use a "*" here for concatenation operator. // TODO(Zeke) change this to use relations.      

如果加 TODO 是為了在 “将來某一天做某事”, 可以附上一個非常明确的時間 “Fix by November 2005”), 或者一個明确的事項 (“Remove this code when all clients can handle XML responses.”).

譯者 (YuleFox) 筆記¶

  1. 關于注釋風格,很多 C++ 的 coders 更喜歡行注釋, C coders 或許對塊注釋依然情有獨鐘, 或者在檔案頭大段大段的注釋時使用塊注釋;
  2. 檔案注釋可以炫耀你的成就, 也是為了捅了簍子别人可以找你;
  3. 注釋要言簡意赅, 不要拖沓備援, 複雜的東西簡單化和簡單的東西複雜化都是要被鄙視的;
  4. 對于 Chinese coders 來說, 用英文注釋還是用中文注釋, it is a problem, 但不管怎樣, 注釋是為了讓别人看懂, 難道是為了炫耀程式設計語言之外的你的母語或外語水準嗎;
  5. 注釋不要太亂, 适當的縮進才會讓人樂意看. 但也沒有必要規定注釋從第幾列開始 (我自己寫代碼的時候總喜歡這樣), UNIX/LINUX 下還可以約定是使用 tab 還是 space, 個人傾向于 space;
  6. TODO 很不錯, 有時候, 注釋确實是為了标記一些未完成的或完成的不盡如人意的地方, 這樣一搜尋, 就知道還有哪些活要幹, 日志都省了.

8. 格式¶

代碼風格和格式确實比較随意, 但一個項目中所有人遵循同一風格是非常容易的. 個體未必同意下述每一處格式規則, 但整個項目服從統一的程式設計風格是很重要的, 隻有這樣才能讓所有人能很輕松的閱讀和了解代碼.

另外, 我們寫了一個 emacs 配置檔案 來幫助你正确的格式化代碼.

8.1. 行長度¶

Tip

每一行代碼字元數不超過 80.

我們也認識到這條規則是有争議的, 但很多已有代碼都已經遵照這一規則, 我們感覺一緻性更重要.

優點:
提倡該原則的人主張強迫他們調整編輯器視窗大小很野蠻. 很多人同時并排開幾個代碼視窗, 根本沒有多餘空間拉伸視窗. 大家都把視窗最大尺寸加以限定, 并且 80 列寬是傳統标準. 為什麼要改變呢?
缺點:
反對該原則的人則認為更寬的代碼行更易閱讀. 80 列的限制是上個世紀 60 年代的大型機的古闆缺陷; 現代裝置具有更寬的顯示屏, 很輕松的可以顯示更多代碼.
結論:

80 個字元是最大值.

特例:

  • 如果一行注釋包含了超過 80 字元的指令或 URL, 出于複制粘貼的友善允許該行超過 80 字元.
  • 包含長路徑的 #include 語句可以超出80列. 但應該盡量避免.
  • 頭檔案保護 可以無視該原則.

8.2. 非 ASCII 字元¶

Tip

盡量不使用非 ASCII 字元, 使用時必須使用 UTF-8 編碼.

即使是英文, 也不應将使用者界面的文本寫死到源代碼中, 是以非 ASCII 字元要少用. 特殊情況下可以适當包含此類字元. 如, 代碼分析外部資料檔案時, 可以适當寫死資料檔案中作為分隔符的非 ASCII 字元串; 更常見的是 (不需要本地化的) 單元測試代碼可能包含非 ASCII 字元串. 此類情況下, 應使用 UTF-8 編碼, 因為很多工具都可以了解和處理 UTF-8 編碼. 十六進制編碼也可以, 能增強可讀性的情況下尤其鼓勵 —— 比如 "\xEF\xBB\xBF" 在 Unicode 中是 零寬度 無間斷 的間隔符号, 如果不用十六進制直接放在 UTF-8 格式的源檔案中, 是看不到的. (yospaly 注: "\xEF\xBB\xBF" 通常用作 UTF-8 with BOM 編碼标記)

8.3. 空格還是制表位¶

Tip

隻使用空格, 每次縮進 2 個空格.

我們使用空格縮進. 不要在代碼中使用制符表. 你應該設定編輯器将制符表轉為空格.

8.4. 函數聲明與定義¶

Tip

傳回類型和函數名在同一行, 參數也盡量放在同一行.

函數看上去像這樣:
ReturnType ClassName::FunctionName(Type par_name1, Type par_name2) { DoSomething(); ... }      
如果同一行文本太多, 放不下所有參數:
ReturnType ClassName::ReallyLongFunctionName(Type par_name1, Type par_name2, Type par_name3) { DoSomething(); ... }      
甚至連第一個參數都放不下:
ReturnType LongClassName::ReallyReallyReallyLongFunctionName( Type par_name1, // 4 space indent Type par_name2, Typepar_name3) { DoSomething(); // 2 space indent ... }      

注意以下幾點:

  • 傳回值總是和函數名在同一行;
  • 左圓括号總是和函數名在同一行;
  • 函數名和左圓括号間沒有空格;
  • 圓括号與參數間沒有空格;
  • 左大括号總在最後一個參數同一行的末尾處;
  • 右大括号總是單獨位于函數最後一行;
  • 右圓括号和左大括号間總是有一個空格;
  • 函數聲明和實作處的所有形參名稱必須保持一緻;
  • 所有形參應盡可能對齊;
  • 預設縮進為 2 個空格;
  • 換行後的參數保持 4 個空格的縮進;
如果函數聲明成  const, 關鍵字  const 應與最後一個參數位于同一行:=
// Everything in this function signature fits on a single line ReturnType FunctionName(Type par) const { ... } // This function signature requires multiple lines, but // the const keyword is on the line with the last parameter. ReturnTypeReallyLongFunctionName(Type par1, Type par2) const { ... }      
如果有些參數沒有用到, 在函數定義處将參數名注釋起來:
// Always have named parameters in interfaces. class Shape { public: virtual void Rotate(double radians) = 0; } // Always have named parameters in the declaration. class Circle : public Shape { public: virtual void Rotate(double radians); } // Comment out unused named parameters in definitions. void Circle::Rotate(double /*radians*/) {}      
Warning
// Bad - if someone wants to implement later, it's not clear what the // variable means. void Circle::Rotate(double) {}      

8.5. 函數調用¶

Tip

盡量放在同一行, 否則, 将實參封裝在圓括号中.

函數調用遵循如下形式:
bool retval = DoSomething(argument1, argument2, argument3);      
如果同一行放不下, 可斷為多行, 後面每一行都和第一個實參對齊, 左圓括号後和右圓括号前不要留白格:
bool retval = DoSomething(averyveryveryverylongargument1, argument2, argument3);      
如果函數參數很多, 出于可讀性的考慮可以在每行隻放一個參數:
bool retval = DoSomething(argument1, argument2, argument3, argument4);      
如果函數名非常長, 以至于超過  行最大長度, 可以将所有參數獨立成行:
if (...) { ... ... if (...) { DoSomethingThatRequiresALongFunctionName( very_long_argument1, // 4 space indent argument2,argument3, argument4); }      

8.6. 條件語句¶

Tip

傾向于不在圓括号内使用空格. 關鍵字 else 另起一行.

對基本條件語句有兩種可以接受的格式. 一種在圓括号和條件之間有空格, 另一種沒有.

最常見的是沒有空格的格式. 哪種都可以, 但  保持一緻性. 如果你是在修改一個檔案, 參考目前已有格式. 如果是寫新的代碼, 參考目錄下或項目中其它檔案. 還在徘徊的話, 就不要加空格了.
if (condition) { // no spaces inside parentheses ... // 2 space indent. } else { // The else goes on the same line as the closing brace. ... }      
如果你更喜歡在圓括号内部加空格:
if ( condition ) { // spaces inside parentheses - rare ... // 2 space indent. } else { // The else goes on the same line as the closing brace. ... }      
注意所有情況下  if 和左圓括号間都有個空格. 右圓括号和左大括号之間也要有個空格:
Warning
if(condition) // Bad - space missing after IF. if (condition){ // Bad - space missing before {. if(condition){ // Doubly bad.      
if (condition) { // Good - proper space after IF and before {.      
如果能增強可讀性, 簡短的條件語句允許寫在同一行. 隻有當語句簡單并且沒有使用  else 子句時使用:
if (x == kFoo) return new Foo(); if (x == kBar) return new Bar();      
如果語句有  else 分支則不允許:
Warning
// Not allowed - IF statement on one line when there is an ELSE clause if (x) DoThis(); else DoThat();      
通常, 單行語句不需要使用大括号, 如果你喜歡用也沒問題; 複雜的條件或循環語句用大括号可讀性會更好. 也有一些項目要求  if 必須總是使用大括号:
if (condition) DoSomething(); // 2 space indent. if (condition) { DoSomething(); // 2 space indent. }      
但如果語句中某個  if-else 分支使用了大括号的話, 其它分支也必須使用:
Warning
// Not allowed - curly on IF but not ELSE if (condition) { foo; } else bar; // Not allowed - curly on ELSE but not IF if(condition) foo; else { bar; }      
// Curly braces around both IF and ELSE required because // one of the clauses used braces. if (condition) { foo; } else{ bar; }      

8.7. 循環和開關選擇語句¶

Tip

switch 語句可以使用大括号分段. 空循環體應使用 {} 或 continue.

switch 語句中的 case 塊可以使用大括号也可以不用, 取決于你的個人喜好. 如果用的話, 要按照下文所述的方法.

如果有不滿足  case 條件的枚舉值,  switch 應該總是包含一個  default 比對 (如果有輸入值沒有 case 去處理, 編譯器将報警). 如果  default應該永遠執行不到, 簡單的加條  assert:
switch (var) { case 0: { // 2 space indent ... // 4 space indent break; } case 1: { ... break; } default: {assert(false); } }      
空循環體應使用  {} 或  continue, 而不是一個簡單的分号.
while (condition) { // Repeat test until it returns false. } for (int i = 0; i < kSomeNumber; ++i) {} // Good - empty body. while (condition) continue; // Good - continue indicates no logic.      
Warning
while (condition); // Bad - looks like part of do/while loop.      

8.8. 指針和引用表達式¶

Tip

句點或箭頭前後不要有空格. 指針/位址操作符 (*, &) 之後不能有空格.

下面是指針和引用表達式的正确使用範例:
x = *p; p = &x; x = r.y; x = r->y;      
注意:
  • 在通路成員時, 句點或箭頭前後沒有空格.
  • 指針操作符 * 或 & 後沒有空格.
在聲明指針變量或參數時, 星号與類型或變量名緊挨都可以:
// These are fine, space preceding. char *c; const string &str; // These are fine, space following. char* c; // but remember to do "char* c, *d, *e, ...;"! const string& str;      
Warning
char * c; // Bad - spaces on both sides of * const string & str; // Bad - spaces on both sides of &      

在單個檔案内要保持風格一緻, 是以, 如果是修改現有檔案, 要遵照該檔案的風格.

8.9. 布爾表達式¶

Tip

如果一個布爾表達式超過 标準行寬, 斷行方式要統一一下.

下例中, 邏輯與 ( &&) 操作符總位于行尾:
if (this_one_thing > this_other_thing && a_third_thing == a_fourth_thing && yet_another & last_one) { ... }      

注意, 上例的邏輯與 (&&) 操作符均位于行尾. 可以考慮額外插入圓括号, 合理使用的話對增強可讀性是很有幫助的.

8.10. 函數傳回值¶

Tip

return 表達式中不要用圓括号包圍.

函數傳回時不要使用圓括号:
return x; // not return(x);      

8.11. 變量及數組初始化¶

Tip

用 = 或 () 均可.

在二者中做出選擇; 下面的方式都是正确的:
int x = 3; int x(3); string name("Some Name"); string name = "Some Name";      

8.12. 預處理指令¶

Tip

預處理指令不要縮進, 從行首開始.

即使預處理指令位于縮進代碼塊中, 指令也應從行首開始.
// Good - directives at beginning of line if (lopsided_score) { #if DISASTER_PENDING // Correct -- Starts at beginning of line DropEverything(); #endif BackToNormal(); }      
Warning
// Bad - indented directives if (lopsided_score) { #if DISASTER_PENDING // Wrong! The "#if" should be at beginning of line DropEverything(); #endif // Wrong! Do not indent "#endif" BackToNormal(); }      

8.13. 類格式¶

Tip

通路控制塊的聲明依次序是 public:, protected:, private:, 每次縮進 1 個空格.

類聲明 (對類注釋不了解的話, 參考  類注釋) 的基本格式如下:
class MyClass : public OtherClass { public: // Note the 1 space indent! MyClass(); // Regular 2 space indent. explicitMyClass(int var); ~MyClass() {} void SomeFunction(); void SomeFunctionThatDoesNothing() { } void set_some_var(int var) {some_var_ = var; } int some_var() const { return some_var_; } private: bool SomeInternalFunction(); int some_var_; intsome_other_var_; DISALLOW_COPY_AND_ASSIGN(MyClass); };      
注意事項:
  • 所有基類名應在 80 列限制下盡量與子類名放在同一行.
  • 關鍵詞 public:, protected:, private: 要縮進 1 個空格.
  • 除第一個關鍵詞 (一般是 public) 外, 其他關鍵詞前要空一行. 如果類比較小的話也可以不空.
  • 這些關鍵詞後不要保留白行.
  • public 放在最前面, 然後是 protected, 最後是 private.
  • 關于聲明順序的規則請參考 聲明順序 一節.

8.14. 初始化清單¶

Tip

構造函數初始化清單放在同一行或按四格縮進并排幾行.

下面兩種初始化清單方式都可以接受:

// When it all fits on one line: MyClass::MyClass(int var) : some_var_(var), some_other_var_(var + 1) {      

// When it requires multiple lines, indent 4 spaces, putting the colon on // the first initializer line:MyClass::MyClass(int var) : some_var_(var), // 4 space indent some_other_var_(var + 1) { // lined up ... DoSomething();... }      

8.15. 名字空間格式化¶

Tip

名字空間内容不縮進.

名字空間 不要增加額外的縮進層次, 例如:
namespace { void foo() { // Correct. No extra indentation within namespace. ... } } // namespace      
不要縮進名字空間:
Warning
namespace { // Wrong. Indented when it should not be. void foo() { ... } } // namespace      

8.16. 水準留白¶

Tip

水準留白的使用因地制宜. 永遠不要在行尾添加沒意義的留白.

正常:
void f(bool b) { // Open braces should always have a space before them. ... int i = 0; // Semicolons usually have no space before them. int x[] = { 0 }; // Spaces inside braces for array initialization are int x[] = {0}; // optional. If you use them, put them on both sides! // Spaces around the colon in inheritance and initializer lists. class Foo : publicBar { public: // For inline function implementations, put spaces between the braces // and the implementation itself.Foo(int b) : Bar(), baz_(b) {} // No spaces inside empty braces. void Reset() { baz_ = 0; } // Spaces separating braces from implementation. ...      
添加備援的留白會給其他人編輯時造成額外負擔. 是以, 行尾不要留白格. 如果确定一行代碼已經修改完畢, 将多餘的空格去掉; 或者在專門清理空格時去掉(确信沒有其他人在處理). (yospaly 注: 現在大部分代碼編輯器稍加設定後, 都支援自動删除行首/行尾空格, 如果不支援, 考慮換一款編輯器或 IDE)
循環和條件語句:
if (b) { // Space after the keyword in conditions and loops. } else { // Spaces around else. } while (test) {} // There is usually no space inside parentheses. switch (i) { for (int i = 0; i < 5; ++i) { switch ( i ) { // Loops and conditions may have spaces inside if ( test ) { // parentheses, but this is rare. Be consistent. for ( int i = 0; i < 5; ++i ) { for( ; i < 5 ; ++i) { // For loops always have a space after the ... // semicolon, and may have a space before the // semicolon. switch (i) { case 1: // No space before colon in a switch case. ... case 2: break; // Use a space after a colon if there's code after it.      
操作符:
x = 0; // Assignment operators always have spaces around // them. x = -5; // No spaces separating unary operators and their ++x; // arguments. if (x && !y) ... v = w * x + y / z; // Binary operators usually have spaces around them, v = w*x+ y/z; // but it's okay to remove spaces around factors. v = w * (x + z); // Parentheses should have no spaces inside them.      
模闆和轉換:
vector<string> x; // No spaces inside the angle y = static_cast<char*>(x); // brackets (< and >), before // <, or between >( in a cast. vector<char *> x; // Spaces between type and pointer are // okay, but be consistent. set<list<string> > x;// C++ requires a space in > >. set< list<string> > x; // You may optionally make use // symmetric spacing in < <.      

8.17. 垂直留白¶

Tip

垂直留白越少越好.

這不僅僅是規則而是原則問題了: 不在萬不得已, 不要使用空行. 尤其是: 兩個函數定義之間的空行不要超過 2 行, 函數體首尾不要留白行, 函數體中也不要随意添加空行.

基本原則是: 同一屏可以顯示的代碼越多, 越容易了解程式的控制流. 當然, 過于密集的代碼塊和過于疏松的代碼塊同樣難看, 取決于你的判斷. 但通常是垂直留白越少越好.

Warning

函數首尾不要有空行

void Function() { // Unnecessary blank lines before and after }      

Warning

代碼塊首尾不要有空行

while (condition) { // Unnecessary blank line after } if (condition) { // Unnecessary blank line before }      
if-else 塊之間空一行是可以接受的:
if (condition) { // Some lines of code too small to move to another function, // followed by a blank line. } else { // Another block of code }      

譯者 (YuleFox) 筆記¶

  1. 對于代碼格式, 因人, 系統而異各有優缺點, 但同一個項目中遵循同一标準還是有必要的;
  2. 行寬原則上不超過 80 列, 把 22 寸的顯示屏都占完, 怎麼也說不過去;
  3. 盡量不使用非 ASCII 字元, 如果使用的話, 參考 UTF-8 格式 (尤其是 UNIX/Linux 下, Windows 下可以考慮寬字元), 盡量不将字元串常量耦合到代碼中, 比如獨立出資源檔案, 這不僅僅是風格問題了;
  4. UNIX/Linux 下無條件使用空格, MSVC 的話使用 Tab 也無可厚非;
  5. 函數參數, 邏輯條件, 初始化清單: 要麼所有參數和函數名放在同一行, 要麼所有參數并排分行;
  6. 除函數定義的左大括号可以置于行首外, 包括函數/類/結構體/枚舉聲明, 各種語句的左大括号置于行尾, 所有右大括号獨立成行;
  7. ./-> 操作符前後不留白格, */& 不要前後都留, 一個就可, 靠左靠右依各人喜好;
  8. 預處理指令/命名空間不使用額外縮進, 類/結構體/枚舉/函數/語句使用縮進;
  9. 初始化用 = 還是 () 依個人喜好, 統一就好;
  10. return 不要加 ();
  11. 水準/垂直留白不要濫用, 怎麼易讀怎麼來.
  12. 關于 UNIX/Linux 風格為什麼要把左大括号置于行尾 (.cc 檔案的函數實作處, 左大括号位于行首), 我的了解是代碼看上去比較簡約, 想想行首除了函數體被一對大括号封在一起之外, 隻有右大括号的代碼看上去确實也舒服; Windows 風格将左大括号置于行首的優點是比對情況一目了然.

9. 規則特例¶

前面說明的程式設計習慣基本都是強制性的. 但所有優秀的規則都允許例外, 這裡就是探讨這些特例.

9.1. 現有不合規範的代碼¶

Tip

對于現有不符合既定程式設計風格的代碼可以網開一面.

當你修改使用其他風格的代碼時, 為了與代碼原有風格保持一緻可以不使用本指南約定. 如果不放心可以與代碼原作者或現在的負責人員商讨, 記住, 一緻性 包括原有的一緻性.

9.2. Windows 代碼¶

Tip

Windows 程式員有自己的程式設計習慣, 主要源于 Windows 頭檔案和其它 Microsoft 代碼. 我們希望任何人都可以順利讀懂你的代碼, 是以針對所有平台的 C++ 程式設計隻給出一個單獨的指南.

如果你習慣使用 Windows 編碼風格, 這兒有必要重申一下某些你可能會忘記的指南:

  • 不要使用匈牙利命名法 (比如把整型變量命名成 iNum). 使用 Google 命名約定, 包括對源檔案使用 .cc 擴充名.
  • Windows 定義了很多原生類型的同義詞 (YuleFox 注: 這一點, 我也很反感), 如 DWORD, HANDLE 等等. 在調用 Windows API 時這是完全可以接受甚至鼓勵的. 但還是盡量使用原有的 C++ 類型, 例如, 使用 const TCHAR * 而不是 LPCTSTR.
  • 使用 Microsoft Visual C++ 進行編譯時, 将警告級别設定為 3 或更高, 并将所有 warnings 當作 errors 處理.
  • 不要使用 #pragma once; 而應該使用 Google 的頭檔案保護規則. 頭檔案保護的路徑應該相對于項目根目錄 (yospaly 注: 如 #ifndefSRC_DIR_BAR_H_, 參考 #define 保護 一節).
  • 除非萬不得已, 不要使用任何非标準的擴充, 如 #pragma 和 __declspec. 允許使用 __declspec(dllimport) 和 __declspec(dllexport); 但你必須通過宏來使用, 比如 DLLIMPORT 和 DLLEXPORT, 這樣其他人在分享使用這些代碼時很容易就去掉這些擴充.

在 Windows 上, 隻有很少的一些情況下, 我們可以偶爾違反規則:

  • 通常我們 禁止使用多重繼承, 但在使用 COM 和 ATL/WTL 類時可以使用多重繼承. 為了實作 COM 或 ATL/WTL 類/接口, 你可能不得不使用多重實作繼承.
  • 雖然代碼中不應該使用異常, 但是在 ATL 和部分 STL(包括 Visual C++ 的 STL) 中異常被廣泛使用. 使用 ATL 時, 應定義_ATL_NO_EXCEPTIONS 以禁用異常. 你要研究一下是否能夠禁用 STL 的異常, 如果無法禁用, 啟用編譯器異常也可以. (注意這隻是為了編譯 STL, 自己代碼裡仍然不要含異常處理.)
  • 通常為了利用頭檔案預編譯, 每個每個源檔案的開頭都會包含一個名為 StdAfx.h 或 precompile.h 的檔案. 為了使代碼友善與其他項目共享, 避免顯式包含此檔案 (precompile.cc), 使用 /FI 編譯器選項以自動包含.
  • 資源頭檔案通常命名為 resource.h, 且隻包含宏的, 不需要遵守本風格指南.

10. 結束語¶

Tip

運用常識和判斷力, 并 保持一緻.

編輯代碼時, 花點時間看看項目中的其它代碼, 并熟悉其風格. 如果其它代碼中 if 語句使用空格, 那麼你也要使用. 如果其中的注釋用星号 (*) 圍成一個盒子狀, 你同樣要這麼做.

風格指南的重點在于提供一個通用的程式設計規範, 這樣大家可以把精力集中在實作内容而不是表現形式上. 我們展示了全局的風格規範, 但局部風格也很重要, 如果你在一個檔案中新加的代碼和原有代碼風格相去甚遠, 這就破壞了檔案本身的整體美觀, 也影響閱讀, 是以要盡量避免.

好了, 關于編碼風格寫的夠多了; 代碼本身才更有趣. 盡情享受吧!

Revision 3.133 Benjy Weinberger Craig Silverstein Gregory Eitzmann Mark Mentovai Tashana Landray