天天看點

Google C++程式設計風格整理(二)

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 的開源項目中使用異常. 如果我們需要把這些項目推倒重來顯然不太現實.

(注: 對于異常處理, 顯然不是短短幾句話能夠說清楚的, 以構造函數為例, 很多 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 在前. 但要保持代碼的一緻性! (注: 也就是不要在一些地方把 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 所淹沒. 看看下面的例子:

上述循環永遠不會退出! 有時 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"
           
Google C++程式設計風格整理(二)

•注意 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 = ×L;
uint64_t my_mask = LL << ;
           

•如果你确實需要 32 位和 64 位系統具有不同代碼, 可以使用 #ifdef _LP64 指令來切分 32/64 位代碼. (盡量不要這麼做, 如果非用不可, 盡量使修改局部化)

5.14. 預處理宏

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

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

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

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

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

•不要在 .h 檔案中定義宏.

•在馬上要使用時才進行 #define, 使用後要立即 #undef.

•不要隻是對已經存在的宏使用#undef,選擇一個不會沖突的名稱;

•不要試圖使用展開後會導緻 C++ 構造不穩定的宏, 不然也至少要附上文檔說明其行為.

5.15. 0 和 NULL

Tip
整數用 , 實數用 , 指針用 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

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" 是啥意思.
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.
           

6.3. 類型命名

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

所有類型命名 —— 類, 結構體, 類型定義 (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

結構體變量:

結構體的資料成員可以和普通變量一樣, 不用像類那樣接下劃線:

struct UrlTableProperties {
    string name;
    int num_entries;
}
           

全局變量:

對全局變量沒有特别要求, 少用就好, 但如果你要用, 可以用 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.
           
Cenum UrlTableErrors {
    kOK = ,
    kErrorOutOfMemory,
    kErrorMalformedInput,
};
enum AlternateUrlTableErrors {
    OK = ,
    OUT_OF_MEMORY = ,
    MALFORMED_INPUT = ,
};
           

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

6.9. 宏命名

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

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

#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. - 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 = ;
           

7.6. 實作注釋

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

代碼前注釋:

巧妙或複雜的代碼段前要加注釋. 比如:

// Divide result by two, taking into account that x
// contains the carry from the add.
for (int i = ; i < result->size(); i++) {
    x = (x << ) + (*result)[i];
    (*result)[i] = x >> ;
    x &= ;
}
           

行注釋:

比較隐晦的地方要在行尾加入注釋. 在行尾空兩格進行注釋. 比如:

// If we have enough memory, mmap the data portion too.
mmap_budget = max<int64>(, 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,
                                  ,
                                  false,
                                  NULL);  // What are these arguments??
           

bool success = CalculateSomething(interesting_value,
                          ,     // Default base value.
                          false,  // Not the first time we're calling this.
                          NULL);  // No callback.
           

或使用常量或描述性變量:

const int kDefaultBaseValue = ;
const bool kFirstTimeCalling = false;
Callback *null_callback = NULL;
bool success = CalculateSomething(interesting_value,
                                  kDefaultBaseValue,
                                  kFirstTimeCalling,
                                  null_callback);
           

不允許:

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

Warning

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

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.”).

筆記

1.關于注釋風格,很多 C++ 的 coders 更喜歡行注釋, C coders 或許對塊注釋依然情有獨鐘, 或者在檔案頭大段大段的注釋時使用塊注釋;

2.檔案注釋可以炫耀你的成就, 也是為了捅了簍子别人可以找你;

3.注釋要言簡意赅, 不要拖沓備援, 複雜的東西簡單化和簡單的東西複雜化都是要被鄙視的;

4.對于 Chinese coders 來說, 用英文注釋還是用中文注釋, it is a problem, 但不管怎樣, 注釋是為了讓别人看懂, 難道是為了炫耀程式設計語言之外的你的母語或外語水準嗎;

5.注釋不要太亂, 适當的縮進才會讓人樂意看. 但也沒有必要規定注釋從第幾列開始 (我自己寫代碼的時候總喜歡這樣), UNIX/LINUX 下還可以約定是使用 tab 還是 space, 個人傾向于 space;

6.TODO 很不錯, 有時候, 注釋确實是為了标記一些未完成的或完成的不盡如人意的地方, 這樣一搜尋, 就知道還有哪些活要幹, 日志都省了.

8. 格式

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

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 格式的源檔案中, 是看不到的. (注: “\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,  //  space indent
        Type par_name2,
        Type par_name3) {
    DoSomething();  //  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) = ;
}

// 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(averyveryveryverylongargument1,
                          argument2, argument3);
           

如果函數參數很多, 出于可讀性的考慮可以在每行隻放一個參數:

bool retval = DoSomething(argument1,
                          argument2,
                          argument3,
                          argument4);
           

如果函數名非常長, 以至于超過 行最大長度, 可以将所有參數獨立成行:

if (...) {
  ...
  ...
  if (...) {
    DoSomethingThatRequiresALongFunctionName(
        very_long_argument1,  //  space indent
        argument2,
        argument3,
        argument4);
  }
           

8.6. 條件語句

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

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

最常見的是沒有空格的格式. 哪種都可以, 但 保持一緻性. 如果你是在修改一個檔案, 參考目前已有格式. 如果是寫新的代碼, 參考目錄下或項目中其它檔案. 還在徘徊的話, 就不要加空格了.

if (condition) {  // no spaces inside parentheses
  ...  //  space indent.
} else {  // The else goes on the same line as the closing brace.
  ...
}
           

如果你更喜歡在圓括号内部加空格:

if ( condition ) {  // spaces inside parentheses - rare
  ...  //  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 : {  //  space indent
    ...      //  space indent
    break;
  }
  case : {
    ...
    break;
  }
  default: {
    assert(false);
  }
}
           

空循環體應使用 {} 或 continue, 而不是一個簡單的分号.

while (condition) {
  // Repeat test until it returns false.
}
for (int i = ; 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 = ;
int x();
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 + ) {
           

// When it requires multiple lines, indent  spaces, putting the colon on
// the first initializer line:
MyClass::MyClass(int var)
    : some_var_(var),             //  space indent
      some_other_var_(var + ) {  // 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 = ;  // Semicolons usually have no space before them.
int x[] = {  };  // Spaces inside braces for array initialization are
int x[] = {};    // 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_ = ; }  // Spaces separating braces from implementation.
  ...
           

添加備援的留白會給其他人編輯時造成額外負擔. 是以, 行尾不要留白格. 如果确定一行代碼已經修改完畢, 将多餘的空格去掉; 或者在專門清理空格時去掉(确信沒有其他人在處理). (注: 現在大部分代碼編輯器稍加設定後, 都支援自動删除行首/行尾空格, 如果不支援, 考慮換一款編輯器或 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 = ; i < ; ++i) {
switch ( i ) {    // Loops and conditions may have spaces inside
if ( test ) {     // parentheses, but this is rare.  Be consistent.
for ( int i = ; i < ; ++i ) {
for ( ; i <  ; ++i) {  // For loops always have a space after the
  ...                   // semicolon, and may have a space before the
                        // semicolon.
switch (i) {
  case :         // No space before colon in a switch case.
    ...
  case : break;  // Use a space after a colon if there's code after it.
           

操作符:

x = ;              // Assignment operators always have spaces around
                    // them.
x = -;             // 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
}
           

筆記

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 注: 如 #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 語句使用空格, 那麼你也要使用. 如果其中的注釋用星号 (*) 圍成一個盒子狀, 你同樣要這麼做.

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

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

繼續閱讀