NO IMAGE

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

版本:

 

3.133

原作者:

Benjy Weinberger
Craig Silverstein
Gregory Eitzmann
Mark Mentovai
Tashana Landray

翻譯:

專案主頁:

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): 只能是 intcharfloat,
和 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 限定符表明該函式不會修改類成員變數的狀態 (如 classFoo { 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. 關注真正有幫助意義的資訊: 前面的例子寫成 constint** 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_tuint32_tint64_t 等整型,
在需要確保整型大小時可以使用它們代替 shortunsigned 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 一部分經認可的特性子集. 目前允許使用以下庫:

我們正在積極考慮增加其它 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.

型別和變數名一般為名詞: 如 FileOpenernum_errors.

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

縮寫:
除非該縮寫在其它地方都非常普遍, 否則不要使用. 例如:
// Good
// These show proper names with no abbreviations.
int num_dns_connections;  // 大部分人都知道 "DNS" 是啥意思.
int price_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

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

所有型別命名 —— 類, 結構體, 型別定義 (typedef), 列舉 —— 均使用相同約定. 例如:
// classes and structs
class UrlTable { ...
class UrlTableTester { ...
struct UrlTableProperties { ...
// typedefs
typedef 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,
Type par_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.
ReturnType ReallyLongFunctionName(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.
explicit MyClass(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_;
int some_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 : public Bar {
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 注: 這一點, 我也很反感), 如 DWORDHANDLE 等等.
    在呼叫 Windows API 時這是完全可以接受甚至鼓勵的. 但還是儘量使用原有的 C++ 型別, 例如, 使用 const TCHAR * 而不是 LPCTSTR.
  • 使用 Microsoft Visual C++ 進行編譯時, 將警告級別設定為 3 或更高, 並將所有 warnings 當作 errors 處理.
  • 不要使用 #pragma once;
    而應該使用 Google 的標頭檔案保護規則. 標頭檔案保護的路徑應該相對於專案根目錄 (yospaly 注: 如 #ifndef SRC_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