天天看點

C++程式設計規範_Google

一、頭檔案的引用

 每一個.cc 檔案(C++的源檔案)都有一個對應的.h 檔案(頭檔案),也有一些例

 外,如單元測試代碼和隻包含main()的.cc 檔案。

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

1、#define的保護

 所有頭檔案都應該使用宏定義來防止頭檔案被多重包含(Multiple inclusion),

 其格式為:<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_

2、頭檔案的依賴

 使用前置聲明(forward declarations)盡量減少.h 檔案中#include 的數量。

 當一個頭檔案被包含的同時也引入了一項新的依賴(dependency),隻要該頭檔案被修改,

 代碼就要重新編譯。如果你的頭檔案包含了其他頭檔案,這些頭檔案的任何改變也将導緻那

 些包含了你的頭檔案的代碼重新編譯。是以,我們甯可盡量少包含頭檔案,尤其是那些包含

 在其他頭檔案中的。

 使用前置聲明可以顯著減少需要包含的頭檔案數量。舉例說明:頭檔案中用到類File,但不

 需要通路File 的聲明,則頭檔案中隻需前置聲明class File;無需#include "file/base/file.h"。

 在頭檔案如何做到使用類Foo 而無需通路類的定義?

 1) 将資料成員類型聲明為Foo *或Foo &;

 2) 參數、傳回值類型為Foo 的函數隻是聲明(但不定義實作);

 3) 靜态資料成員的類型可以被聲明為Foo,因為靜态資料成員的定義在類定義之外。

 另一方面,如果你的類是Foo 的子類,或者含有類型為Foo 的非靜态資料成員,則必須為

 之包含頭檔案。

 有時,使用指針成員(pointer members,如果是scoped_ptr 更好)替代對象成員(object

 members)的确更有意義。然而,這樣的做法會降低代碼可讀性及執行效率。如果僅僅為

 了少包含頭檔案,還是不要這樣替代的好。

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

 譯者注:能依賴聲明的就不要依賴定義。

3. 内聯函數

   隻有當函數隻有10 行甚至更少時才會将其定義為内聯函數(inline function)。

   定義(Definition):當函數被聲明為内聯函數之後,編譯器可能會将其内聯展開,無需

   按通常的函數調用機制調用内聯函數。

   優點:當函數體比較小的時候,内聯該函數可以令目标代碼更加高效。對于存取函數

  (accessor、mutator)以及其他一些比較短的關鍵執行函數。

   缺點:濫用内聯将導緻程式變慢,内聯有可能是目标代碼量或增或減,這取決于被内聯的函

   數的大小。内聯較短小的存取函數通常會減少代碼量,但内聯一個很大的函數(譯者注:如

   果編譯器允許的話)将戲劇性的增加代碼量。在現代處理器上,由于更好的利用指令緩存

  (instruction cache),小巧的代碼往往執行更快。

 結論:一個比較得當的處理規則是,不要内聯超過10 行的函數。對于析構函數應慎重對待,

 析構函數往往比其表面看起來要長,因為有一些隐式成員和基類析構函數(如果有的話)被

 調用!

 另一有用的處理規則:内聯那些包含循環或switch 語句的函數是得不償失的,除非在大多

 數情況下,這些循環或switch 語句從不執行。

 重要的是,虛函數和遞歸函數即使被聲明為内聯的也不一定就是内聯函數。通常,遞歸函數

 不應該被聲明為内聯的(譯者注:遞歸調用堆棧的展開并不像循環那麼簡單,比如遞歸層數

 在編譯時可能是未知的,大多數編譯器都不支援内聯遞歸函數)。析構函數内聯的主要原因

 是其定義在類的定義中,為了友善抑或是對其行為給出文檔。

4. -inl.h檔案

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

 在頭檔案中給出内聯函數的定義,可令編譯器将其在調用處内聯展開。然而,實作代碼應完

 全放到.cc 檔案中,我們不希望.h 檔案中出現太多實作代碼,除非這樣做在可讀性和效率上

 有明顯優勢。

 如果内聯函數的定義比較短小、邏輯比較簡單,其實作代碼可以放在.h 檔案中。例如,存

 取函數的實作理所當然都放在類定義中。出于實作和調用的友善,較複雜的内聯函數也可以

 放到.h 檔案中,如果你覺得這樣會使頭檔案顯得笨重,還可以将其分離到單獨的-inl.h 中。

 這樣即把實作和類定義分離開來,當需要時包含實作所在的-inl.h 即可。

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

 要提醒的一點是,-inl.h 和其他頭檔案一樣,也需要#define 保護。

5. 函數參數順序(Function Parameter Ordering) 

   定義函數時,參數順序為:輸入參數在前,輸出參數在後。

   C/C++函數參數分為輸入參數和輸出參數兩種,有時輸入參數也會輸出(譯者注:值被修

   改時)。輸入參數一般傳值或常數引用(const references),輸出參數或輸入/輸出參數

   為非常數指針(non-const pointers)。對參數排序時,将所有輸入參數置于輸出參數之

   前。不要僅僅因為是新添加的參數,就将其置于最後,而應該依然置于輸出參數之前。

   這一點并不是必須遵循的規則,輸入/輸出兩用參數(通常是類/結構體變量)混在其中,會

   使得規則難以遵循。

6. 包含檔案的名稱及次序

   将包含次序标準化可增強可讀性、避免隐藏依賴(hidden dependencies,譯者注:隐藏

   依賴主要是指包含的檔案中編譯時),次序如下:C 庫、C++庫、其他庫的.h、項目内的.h。

   項目内頭檔案應按照項目源代碼目錄樹結構排列,并且避免使用UNIX 檔案路徑.(目前目

   錄)和..(父目錄)。例如,google-awesome-project/src/base/logging.h 應像這樣

   被包含:

   #include "base/logging.h"

   dir/foo.cc 的主要作用是執行或測試dir2/foo2.h 的功能,foo.cc 中包含頭檔案的次序如

   下:

   dir2/foo2.h(優先位置,詳情如下)

   C 系統檔案

   C++系統檔案

   其他庫頭檔案

   本項目内頭檔案

   這種排序方式可有效減少隐藏依賴,我們希望每一個頭檔案獨立編譯。最簡單的實作方式是

   将其作為第一個.h 檔案包含在對應的.cc 中。

   這種排序方式可有效減少隐藏依賴,我們希望每一個頭檔案獨立編譯。最簡單的實作方式是

   将其作為第一個.h 檔案包含在對應的.cc 中。 

 dir/foo.cc 和dir2/foo2.h 通常位于相同目錄下(像base/basictypes_unittest.cc 和

 base/basictypes.h),但也可在不同目錄下。

 相同目錄下頭檔案按字母序是不錯的選擇。

 舉例來說,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"

二、作用域

1、命名空間(Namespaces)

 在.cc 檔案中,提倡使用不具名的命名空間(unnamed namespaces,譯者注:不具名的

 命名空間就像不具名的類一樣,似乎被介紹的很少:-()。使用具名命名空間時,其名稱可

 基于項目或路徑名稱,不要使用using 訓示符。

 定義:命名空間将全局作用域細分為不同的、具名的作用域,可有效防止全局作用域的命名

 沖突。

 優點:命名空間提供了(可嵌套)命名軸線(name axis,譯者注:将命名分割在不同命

 名空間内),當然,類也提供了(可嵌套)的命名軸線(譯者注:将命名分割在不同類的作

 用域内)。

 舉例來說,兩個不同項目的全局作用域都有一個類Foo,這樣在編譯或運作時造成沖突。如

 果每個項目将代碼置于不同命名空間中,project1::Foo 和project2::Foo 作為不同符号

 自然不會沖突。

 缺點:命名空間具有迷惑性,因為它們和類一樣提供了額外的(可嵌套的)命名軸線。在頭

 檔案中使用不具名的空間容易違背C++的唯一定義原則(One Definition Rule (ODR))。

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

    1) 不具名命名空間(Unnamed Namespaces)

   在.cc 檔案中,允許甚至提倡使用不具名命名空間,以避免運作時的命名沖突:

   namespace { // .cc 檔案中

  // 命名空間的内容無需縮進

   enum { UNUSED, EOF, ERROR }; // 經常使用的符号

   bool AtEof() { return pos_ == EOF; } // 使用本命名空間内的符号EOF

   } // namespace

   然而,與特定類關聯的檔案作用域聲明在該類中被聲明為類型、靜态資料成員或靜态成員函

   數,而不是不具名命名空間的成員。像上文展示的那樣,不具名命名空間結束時用注釋//

   namespace 辨別。

   不能在.h 檔案中使用不具名命名空間。

 2) 具名命名空間(Named Namespaces)

   具名命名空間使用方式如下:

   命名空間将除檔案包含、全局辨別的聲明/定義以及類的前置聲明外的整個源檔案封裝起來,

   以同其他命名空間相區分。

   // .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::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. 嵌套類(Nested Class)

 當公開嵌套類作為接口的一部分時,雖然可以直接将他們保持在全局作用域中,但将嵌套類

 的聲明置于命名空間中是更好的選擇。

 定義:可以在一個類中定義另一個類,嵌套類也稱成員類(member class)。 

 class Foo {

 private:

 // Bar 是嵌套在Foo 中的成員類

 class Bar {

 ...

 };

 };

 優點:當嵌套(成員)類隻在被嵌套類(enclosing class)中使用時很有用,将其置于被

 嵌套類作用域作為被嵌套類的成員不會污染其他作用域同名類。可在被嵌套類中前置聲明嵌

 套類,在.cc 檔案中定義嵌套類,避免在被嵌套類中包含嵌套類的定義,因為嵌套類的定義

 通常隻與實作相關。

 缺點:隻能在被嵌套類的定義中才能前置聲明嵌套類。是以,任何使用Foo::Bar*指針的

 頭檔案必須包含整個Foo 的聲明。

 結論:不要将嵌套類定義為public,除非它們是接口的一部分,比如,某個方法使用了這

 個類的一系列選項。

3. 非成員函數(Nonmember)、靜态成員函數(Static Member)和全局函數(GlobalFunctions)

   使用命名空間中的非成員函數或靜态成員函數,盡量不要使用全局函數。

   優點:某些情況下,非成員函數和靜态成員函數是非常有用的,将非成員函數置于命名空間

   中可避免對全局作用域的污染。

   缺點:将非成員函數和靜态成員函數作為新類的成員或許更有意義,當它們需要通路外部資

   源或具有重要依賴時更是如此。

   結論:

 有時,不把函數限定在類的實體中是有益的,甚至需要這麼做,要麼作為靜态成員,要麼作

 為非成員函數。非成員函數不應依賴于外部變量,并盡量置于某個命名空間中。相比單純為

 了封裝若幹不共享任何靜态資料的靜态成員函數而建立類,不如使用命名空間。

   定義于同一編譯單元的函數,被其他編譯單元直接調用可能會引入不必要的耦合和連接配接依

   賴;靜态成員函數對此尤其敏感。可以考慮提取到新類中,或者将函數置于獨立庫的命名空

   間中。

   如果你确實需要定義非成員函數,又隻是在.cc 檔案中使用它,可使用不具名命名空間或

   static 關聯(如static int Foo() {...})限定其作用域。

4. 局部變量(Local Variables)

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

   C++允許在函數的任何位置聲明變量。我們提倡在盡可能小的作用域中聲明變量,離第一

   次使用越近越好。這使得代碼易于閱讀,易于定位變量的聲明位置、變量類型和初始值。特

   别是,應使用初始化代替聲明+指派的方式。

   int i;

   i = f(); // 壞——初始化和聲明分離

   int j = g(); // 好——初始化時聲明

   注意:gcc 可正确執行for (int i = 0; i < 10; ++i)(i 的作用域僅限for 循環),是以其

   他for 循環中可重用i。if 和while 等語句中,作用域聲明(scope declaration)同樣是

   正确的。

   while (const char* p = strchr(str, '/')) str = p + 1;

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

   用其析構函數。

   // 低效的實作

 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);

 }

5. 全局變量(Global Variables)

 class 類型的全局變量是被禁止的,内建類型的全局變量是允許的,當然多線程代碼中非常

 數全局變量也是被禁止的。永遠不要使用函數傳回值初始化全局變量。

 不幸的是,全局變量的構造函數、析構函數以及初始化操作的調用順序隻是被部分規定,每

 次生成有可能會有變化,進而導緻難以發現的bugs。

 是以,禁止使用class 類型的全局變量(包括STL 的string, vector 等等),因為它們的

 初始化順序有可能導緻構造出現問題。内建類型和由内建類型構成的沒有構造函數的結構體

 可以使用,如果你一定要使用class 類型的全局變量,請使用單件模式(singleton

 pattern)。

 對于全局的字元串常量,使用C 風格的字元串,而不要使用STL 的字元串:

 const char kFrogSays[] = "ribbet";

 雖然允許在全局作用域中使用全局變量,使用時務必三思。大多數全局變量應該是類的靜态

 資料成員,或者當其隻在.cc 檔案中使用時,将其定義到不具名命名空間中,或者使用靜态

 關聯以限制變量的作用域。

 記住,靜态成員變量視作全局變量,是以,也不能是class 類型!

6.譯者:這一篇主要提到的是作用域的一些規則,總結一下:

 1. .cc中的不具名命名空間可避免命名沖突、限定作用域,避免直接使用using提示符污

  染命名空間;

 2. 嵌套類符合局部使用原則,隻是不能在其他頭檔案中前置聲明,盡量不要public;

 3. 盡量不用全局函數和全局變量,考慮作用域和命名空間限制,盡量單獨形成編譯單元;

 4. 多線程中的全局變量(含靜态成員變量)不要使用class類型(含STL容器),避免

    不明确行為導緻的bugs。

    作用域的使用,除了考慮名稱污染、可讀性之外,主要是為降低耦合度,提高編譯、執行

    效率。

三、類

類是C++中基本的代碼單元,自然被廣泛使用。本節列舉了在寫一個類時要做什麼、不要

做什麼。

1、構造函數(Constructor)的職責

 構造函數中隻進行那些沒有實際意義的(trivial,譯者注:簡單初始化對于程式執行沒有實

 際的邏輯意義,因為成員變量的“有意義”的值大多不在構造函數中确定)初始化,可能的話,

 使用Init()方法集中初始化為有意義的(non-trivial)資料。

 定義:在構造函數中執行初始化操作。

 優點:排版友善,無需擔心類是否初始化。

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

 1) 構造函數中不易報告錯誤,不能使用異常。

 2) 操作失敗會造成對象初始化失敗,引起不确定狀态。

 3) 構造函數内調用虛函數,調用不會派發到子類實作中,即使目前沒有子類化實作,将來

 仍是隐患。

 4) 如果有人建立該類型的全局變量(雖然違背了上節提到的規則),構造函數将在main()

 之前被調用,有可能破壞構造函數中暗含的假設條件。例如,gflags 尚未初始化。

 結論:如果對象需要有意義的(non-trivial)初始化,考慮使用另外的Init()方法并(或)

 增加一個成員标記用于訓示對象是否已經初始化成功。

2、 預設構造函數(Default Constructors)

 如果一個類定義了若幹成員變量又沒有其他構造函數,需要定義一個預設構造函數,否則編

 譯器将自動生産預設構造函數。

 定義:建立一個沒有參數的對象時,預設構造函數被調用,當調用new[](為數組)時,

 預設構造函數總是被調用。

 優點:預設将結構體初始化為“不可能的”值,使調試更加容易。

 缺點:對代碼編寫者來說,這是多餘的工作。

 結論:

 如果類中定義了成員變量,沒有提供其他構造函數,你需要定義一個預設構造函數(沒有參

 數)。預設構造函數更适合于初始化對象,使對象内部狀态(internal state)一緻、有效。

 提供預設構造函數的原因是:如果你沒有提供其他構造函數,又沒有定義預設構造函數,編

 譯器将為你自動生成一個,編譯器生成的構造函數并不會對對象進行初始化。

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

 函數。

3、明确的構造函數(Explicit Constructors)

 對單參數構造函數使用C++關鍵字explicit。

 定義:通常,隻有一個參數的構造函數可被用于轉換(conversion,譯者注:主要指隐式

 轉換,下文可見),例如,定義了Foo::Foo(string name),當向需要傳入一個Foo 對象

 的函數傳入一個字元串時,構造函數Foo::Foo(string name)被調用并将該字元串轉換為

 一個Foo 臨時對象傳給調用函數。看上去很友善,但如果你并不希望如此通過轉換生成一

 個新對象的話,麻煩也随之而來。為避免構造函數被調用造成隐式轉換,可以将其聲明為

 explicit。

 優點:避免不合時宜的變換。

 缺點:無。

 結論:

 所有單參數構造函數必須是明确的。在類定義中,将關鍵字explicit 加到單參數構造函數前:

 explicit Foo(string name);

 例外:在少數情況下,拷貝構造函數可以不聲明為explicit;特意作為其他類的透明包裝器

 的類。類似例外情況應在注釋中明确說明。

4. 拷貝構造函數(Copy Constructors)

 僅在代碼中需要拷貝一個類對象的時候使用拷貝構造函數;不需要拷貝時應使用

 DISALLOW_COPY_AND_ASSIGN。

 定義:通過拷貝建立對象時可使用拷貝構造函數(特别是對象的傳值時)。

 優點:拷貝構造函數使得拷貝對象更加容易,STL 容器要求所有内容可拷貝、可指派。

 缺點:C++中對象的隐式拷貝是導緻很多性能問題和bugs 的根源。拷貝構造函數降低了

 代碼可讀性,相比按引用傳遞,跟蹤按值傳遞的對象更加困難,對象修改的地方變得難以捉

 摸。

 結論:

 大量的類并不需要可拷貝,也不需要一個拷貝構造函數或指派操作(assignment

 operator)。不幸的是,如果你不主動聲明它們,編譯器會為你自動生成,而且是public

 的。

 可以考慮在類的private 中添加空的(dummy)拷貝構造函數和指派操作,隻有聲明,沒

 有定義。由于這些空程式聲明為private,當其他代碼試圖使用它們的時候,編譯器将報錯。

 為了友善,可以使用宏DISALLOW_COPY_AND_ASSIGN:

 // 禁止使用拷貝構造函數和指派操作的宏

 // 應在類的private:中使用

 #define DISALLOW_COPY_AND_ASSIGN(TypeName) \

 TypeName(const TypeName&); \

 void operator=(const TypeName&)

 class Foo {

 public:

 Foo(int f);

 ~Foo();

 private:

 DISALLOW_COPY_AND_ASSIGN(Foo);

 };

 如上所述,絕大多數情況下都應使用DISALLOW_COPY_AND_ASSIGN,如果類确實需

 要可拷貝,應在該類的頭檔案中說明原由,并适當定義拷貝構造函數和指派操作,注意在

 operator=中檢測自指派(self-assignment)情況。

 在将類作為STL 容器值得時候,你可能有使類可拷貝的沖動。類似情況下,真正該做的是

 使用指針指向STL 容器中的對象,可以考慮使用std::tr1::shared_ptr。

5. 結構體和類(Structs vs. Classes)

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

 在C++中,關鍵字struct 和class 幾乎含義等同,我們為其人為添加語義,以便為定義的

 資料類型合理選擇使用哪個關鍵字。

 struct 被用在僅包含資料的消極對象(passive objects)上,可能包括有關聯的常量,但

 沒有存取資料成員之外的函數功能,而存取功能通過直接通路實作而無需方法調用,這兒提

 到的方法是指隻用于處理資料成員的,如構造函數、析構函數、Initialize()、Reset()、

 Validate()。

 如果需要更多的函數功能,class 更适合,如果不确定的話,直接使用class。

 如果與STL 結合,對于仿函數(functors)和特性( traits)可以不用class 而是使用struct。

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

6. 繼承(Inheritance)

 使用組合(composition,譯者注,這一點也是GoF 在《Design Patterns》裡反複強調

 的)通常比使用繼承更适宜,如果使用繼承的話,隻使用公共繼承。

 定義:當子類繼承基類時,子類包含了父基類所有資料及操作的定義。C++實踐中,繼承

 主要用于兩種場合:實作繼承(implementation inheritance),子類繼承父類的實作代

 碼;接口繼承(interface inheritance),子類僅繼承父類的方法名稱。

 優點:實作繼承通過原封不動的重用基類代碼減少了代碼量。由于繼承是編譯時聲明

 (compile-time declaration),編碼者和編譯器都可以了解相應操作并發現錯誤。接口

 繼承可用于程式上增強類的特定API 的功能,在類沒有定義API 的必要實作時,編譯器同

 樣可以偵錯。

 缺點:對于實作繼承,由于實作子類的代碼在父類和子類間延展,要了解其實作變得更加困

 難。子類不能重寫父類的非虛函數,當然也就不能修改其實作。基類也可能定義了一些資料

 成員,還要區分基類的實體輪廓(physical layout)。

 結論:

 所有繼承必須是public 的,如果想私有繼承的話,應該采取包含基類執行個體作為成員的方式

 作為替代。

 不要過多使用實作繼承,組合通常更合适一些。努力做到隻在“是一個”("is-a", 譯者注,

 其他"has-a"情況下請使用組合)的情況下使用繼承:如果Bar 的确“是一種”Foo,才令Bar

 是Foo 的子類。

 必要的話,令析構函數為virtual,必要是指,如果該類具有虛函數,其析構函數應該為虛

 函數。

 譯者注:至于子類沒有額外資料成員,甚至父類也沒有任何資料成員的特殊情況下,析構

 函數的調用是否必要是語義争論,從程式設計設計規範的角度看,在含有虛函數的父類中,定

 義虛析構函數絕對必要。

 限定僅在子類通路的成員函數為protected,需要注意的是資料成員應始終為私有。

 當重定義派生的虛函數時,在派生類中明确聲明其為virtual。根本原因:如果遺漏virtual,

 閱讀者需要檢索類的所有祖先以确定該函數是否為虛函數(譯者注,雖然不影響其為虛函數

 的本質)。

7. 多重繼承(Multiple Inheritance)

 真正需要用到多重實作繼承(multiple implementation inheritance)的時候非常少,

 隻有當最多一個基類中含有實作,其他基類都是以Interface 為字尾的純接口類時才會使用

 多重繼承。

 定義:多重繼承允許子類擁有多個基類,要将作為純接口的基類和具有實作的基類差別開來。

 優點:相比單繼承,多重實作繼承可令你重用更多代碼。

 缺點:真正需要用到多重實作繼承的時候非常少,多重實作繼承看上去是不錯的解決方案,

 通常可以找到更加明确、清晰的、不同的解決方案。

 結論:隻有當所有超類(superclass)除第一個外都是純接口時才能使用多重繼承。為确

 保它們是純接口,這些類必須以Interface 為字尾。

 注意:關于此規則,Windows 下有種例外情況(譯者注,将在本譯文最後一篇的規則例外

 中闡述)。 

8. 接口(Interface)

 接口是指滿足特定條件的類,這些類以Interface 為字尾(非必需)。

 定義:當一個類滿足以下要求時,稱之為純接口:

 1) 隻有純虛函數("=0")和靜态函數(下文提到的析構函數除外);

 2) 沒有非靜态資料成員;

 3) 沒有定義任何構造函數。如果有,也不含參數,并且為protected;

 4) 如果是子類,也隻能繼承滿足上述條件并以Interface 為字尾的類。

 接口類不能被直接執行個體化,因為它聲明了純虛函數。為確定接口類的所有實作可被正确銷毀,

 必須為之聲明虛析構函數.

 優點:以Interface 為字尾可令他人知道不能為該接口類增加實作函數或非靜态資料成員,

 這一點對于多重繼承尤其重要。另外,對于Java 程式員來說,接口的概念已經深入人心。

 缺點:Interface 字尾增加了類名長度,為閱讀和了解帶來不便,同時,接口特性作為實作

 細節不應暴露給客戶。

 結論:。隻有在滿足上述需要時,類才以Interface 結尾,但反過來,滿足上述需要的類未

 必一定以Interface 結尾。

 9. 操作符重載(Operator Overloading)

 除少數特定環境外,不要重載操作符。

 定義:一個類可以定義諸如+、/等操作符,使其可以像内建類型一樣直接使用。

 優點:使代碼看上去更加直覺,就像内建類型(如int)那樣,重載操作符使那些Equals()、

 Add()等黯淡無光的函數名好玩多了。為了使一些模闆函數正确工作,你可能需要定義操作

 符。

 缺點:雖然操作符重載令代碼更加直覺,但也有一些不足

 1) 混淆直覺,讓你誤以為一些耗時的操作像内建操作那樣輕巧;

 2) 查找重載操作符的調用處更加困難,查找Equals()顯然比同等調用==容易的多;

 3) 有的操作符可以對指針進行操作,容易導緻bugs,Foo + 4 做的是一件事,而&Foo + 4

 可能做的是完全不同的另一件事,對于二者,編譯器都不會報錯,使其很難調試;

 4) 重載還有令你吃驚的副作用,比如,重載操作符&的類不能被前置聲明。

 結論:

 一般不要重載操作符,尤其是指派操作(operator=)比較陰險,應避免重載。如果需要

 的話,可以定義類似Equals()、CopyFrom()等函數。

 然而,極少數情況下需要重載操作符以便與模闆或“标準”C++類銜接(如

 operator<<(ostream&, const T&)),如果被證明是正當的尚可接受,但你要盡可能避

 免這樣做。尤其是不要僅僅為了在STL 容器中作為key 使用就重載operator==或

 operator<,取而代之,你應該在聲明容器的時候,建立相等判斷和大小比較的仿函數類

 型。

 有些STL 算法确實需要重載operator==時可以這麼做,不要忘了提供文檔說明原因。

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

10. 存取控制(Access Control)

 将資料成員私有化,并提供相關存取函數,如定義變量foo_及取值函數get_foo()、指派函數

 set_foo()。

 存取函數的定義一般内聯在頭檔案中。

 參考繼承和函數命名。

11. 聲明次序(Declaration Order)

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

 定義次序如下:public:、protected:、private:,如果那一塊沒有,直接忽略即可。

 每一塊中,聲明次序一般如下:

 1) typedefs 和enums;

 2) 常量;

 3) 構造函數;

 4) 析構函數;

 5) 成員函數,含靜态成員函數;

 6) 資料成員,含靜态資料成員。

 宏DISALLOW_COPY_AND_ASSIGN 置于private:塊之後,作為類的最後部分。參考拷

 貝構造函數。

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

 不要将大型函數内聯到類的定義中,通常,隻有那些沒有特别意義的或者性能要求高的,并

 且是比較短小的函數才被定義為内聯函數。更多細節參考譯文第一篇的内聯函數。

12. 編寫短小函數(Write Short Functions)

 傾向于選擇短小、凝練的函數。

 長函數有時是恰當的,是以對于函數長度并沒有嚴格限制。如果函數超過40 行,可以考慮

 在不影響程式結構的情況下将其分割一下。

 即使一個長函數現在工作的非常好,一旦有人對其修改,有可能出現新的問題,甚至導緻難

 以發現的bugs。使函數盡量短小、簡單,便于他人閱讀和修改代碼。

 在處理代碼時,你可能會發現複雜的長函數,不要害怕修改現有代碼:如果證明這些代碼使

 用、調試困難,或者你需要使用其中的一小塊,考慮将其分割為更加短小、易于管理的若幹

 函數。

***關于類的注意事項,總結一下:

 1. 不在構造函數中做太多邏輯相關的初始化;

 2. 編譯器提供的預設構造函數不會對變量進行初始化,如果定義了其他構造函數,編譯器

 不再提供,需要編碼者自行提供預設構造函數;

 3. 為避免隐式轉換,需将單參數構造函數聲明為explicit;

 4. 為避免拷貝構造函數、指派操作的濫用和編譯器自動生成,可目前聲明其為private

 且無需實作;

 5. 僅在作為資料集合時使用struct;

 6. 組合>實作繼承>接口繼承>私有繼承,子類重載的虛函數也要聲明virtual關鍵字,

 雖然編譯器允許不這樣做;

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

 8. 接口類類名以Interface為字尾,除提供帶實作的虛析構函數、靜态成員函數外,其

 他均為純虛函數,不定義非靜态資料成員,不提供構造函數,提供的話,聲明為protected;

 9. 為降低複雜性,盡量不重載操作符,模闆、标準類中使用時提供文檔說明;

 10. 存取函數一般内聯在頭檔案中;

 11. 聲明次序:public->protected->private;

 12. 函數體盡量短小、緊湊,功能單一。

四、Google特有的風情

1. 智能指針(Smart Pointers)

 如果确實需要使用智能指針的話,scoped_ptr 完全可以勝任。在非常特殊的情況下,例如

 對STL 容器中對象,你應該隻使用std::tr1::shared_ptr,任何情況下都不要使用

 auto_ptr。

 “智能”指針看上去是指針,其實是附加了語義的對象。以scoped_ptr 為例,scoped_ptr

 被銷毀時,删除了它所指向的對象。shared_ptr 也是如此,而且,shared_ptr 實作了引

 用計數(reference-counting),進而隻有當它所指向的最後一個對象被銷毀時,指針才

 會被删除。

五、其他C++特性

1. 引用參數(Reference Arguments)

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

 定義:在C 語言中,如果函數需要修改變量的值,形參(parameter)必須為指針,如int

 foo(int *pval)。在C++中,函數還可以聲明引用形參:int foo(int &val)。

 優點:定義形參為引用避免了像(*pval)++這樣醜陋的代碼,像拷貝構造函數這樣的應用

 也是必需的,而且不像指針那樣不接受空指針NULL。

 缺點:容易引起誤解,因為引用在文法上是值卻擁有指針的語義。

 結論:

 函數形參表中,所有引用必須是const:

 void Foo(const string &in, string *out);

 事實上這是一個硬性約定:輸入參數為值或常數引用,輸出參數為指針;輸入參數可以是常

 數指針,但不能使用非常數引用形參。

 在強調參數不是拷貝而來,在對象生命期内必須一直存在時可以使用常數指針,最好将這些

 在注釋中詳細說明。bind2nd 和mem_fun 等STL 擴充卡不接受引用形參,這種情況下也

 必須以指針形參聲明函數。

2. 函數重載(Function Overloading)

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

 預設函數參數。

 定義:可以定義一個函數參數類型為const string&,并定義其重載函數類型為const

 char*。

 class MyClass {

 public:

 void Analyze(const string &text);

 void Analyze(const char *text, size_t textlen);

 };

 優點:通過重載不同參數的同名函數,令代碼更加直覺,模闆化代碼需要重載,同時為通路

 者帶來便利。

 缺點:限制使用重載的一個原因是在特定調用處很難确定到底調用的是哪個函數,另一個原

 因是當派生類隻重載函數的部分變量會令很多人對繼承語義産生困惑。此外在閱讀庫的客戶

 端代碼時,因預設函數參數造成不必要的費解。

 結論:如果你想重載一個函數,考慮讓函數名包含參數資訊,例如,使用AppendString()、

 AppendInt()而不是Append()。

3. 預設參數(Default Arguments)

 禁止使用預設函數參數。

 優點:經常用到一個函數帶有大量預設值,偶爾會重寫一下這些值,預設參數為很少涉及的

 例外情況提供了少定義一些函數的友善。

 缺點:大家經常會通過檢視現有代碼确定如何使用API,預設參數使得複制粘貼以前的代碼

 難以呈現所有參數,當預設參數不适用于新代碼時可能導緻重大問題。

 結論:所有參數必須明确指定,強制程式員考慮API 和傳入的各參數值,避免使用可能不

 為程式員所知的預設參數。

4. 變長數組和alloca(Variable-Length Arrays and alloca())

 禁止使用變長數組和alloca()。

 優點:變長數組具有渾然天成的文法,變長數組和alloca()也都很高效。

 缺點:變長數組和alloca()不是标準C++的組成部分,更重要的是,它們在堆棧(stack)

 上根據資料配置設定大小可能導緻難以發現的記憶體洩漏:“在我的機器上運作的好好的,到了産

 品中卻莫名其妙的挂掉了”。

 結論:

 使用安全的配置設定器(allocator),如scoped_ptr/scoped_array。 

5. 友元(Friends)

 允許合理使用友元類及友元函數。

 通常将友元定義在同一檔案下,避免讀者跑到其他檔案中查找其對某個類私有成員的使用。

 經常用到友元的一個地方是将FooBuilder 聲明為Foo 的友元,FooBuilder 以便可以正确

 構造Foo 的内部狀态,而無需将該狀态暴露出來。某些情況下,将一個單元測試用類聲明

 為待測類的友元會很友善。

 友元延伸了(但沒有打破)類的封裝界線,當你希望隻允許另一個類通路某個成員時,使用

 友元通常比将其聲明為public 要好得多。當然,大多數類應該隻提供公共成員與其互動。  

6. 異常(Exceptions)

 不要使用C++異常。

 優點:

 1) 異常允許上層應用決定如何處理在底層嵌套函數中發生的“不可能發生”的失敗,不像出

 錯代碼的記錄那麼模糊費解;

 2) 應用于其他很多現代語言中,引入異常使得C++與Python、Java 及其他與C++相近

 的語言更加相容;

 3) 許多C++第三方庫使用異常,關閉異常将導緻難以與之結合;

 4) 異常是解決構造函數失敗的唯一方案,雖然可以通過工廠函數(factory function)或

 Init()方法模拟異常,但他們分别需要堆配置設定或新的“非法”狀态;

 5) 在測試架構(testing framework)中,異常确實很好用。

 缺點:

 1) 在現有函數中添加throw 語句時,必須檢查所有調用處,即使它們至少具有基本的異常

 安全保護,或者程式正常結束,永遠不可能捕獲該異常。例如:if f() calls g() calls h(),

 h 抛出被f 捕獲的異常,g 就要當心了,避免沒有完全清理;

 2) 通俗一點說,異常會導緻程式控制流(control flow)通過檢視代碼無法确定:函數有

 可能在不确定的地方傳回,進而導緻代碼管理和調試困難,當然,你可以通過規定何時何地

 如何使用異常來最小化的降低開銷,卻給開發人員帶來掌握這些規定的負擔;

 3) 異常安全需要RAII 和不同編碼實踐。輕松、正确編寫異常安全代碼需要大量支撐。允

 許使用異常;

 4) 加入異常使二進制執行代碼體積變大,增加了編譯時長(或許影響不大),還可能增加

 位址空間壓力;

 5) 異常的實用性可能會刺激開發人員在不恰當的時候抛出異常,或者在不安全的地方從異

 常中恢複,例如,非法使用者輸入可能導緻抛出異常。如果允許使用異常會使得這樣一篇程式設計

 風格指南長出很多(譯者注,這個理由有點牽強:-()!

 結論:

 從表面上看,使用異常利大于弊,尤其是在新項目中,然而,對于現有代碼,引入異常會牽

 連到所有依賴代碼。如果允許異常在新項目中使用,在跟以前沒有使用異常的代碼整合時也

 是一個麻煩。因為Google 現有的大多數C++代碼都沒有異常處理,引入帶有異常處理的

 新代碼相當困難。

 鑒于Google 現有代碼不接受異常,在現有代碼中使用異常比在新項目中使用的代價多少要

 大一點,遷移過程會比較慢,也容易出錯。我們也不相信異常的有效替代方案,如錯誤代碼、

 斷言等,都是嚴重負擔。

 我們并不是基于哲學或道德層面反對使用異常,而是在實踐的基礎上。因為我們希望使用

 Google 上的開源項目,但項目中使用異常會為此帶來不便,因為我們也建議不要在Google

 上的開源項目中使用異常,如果我們需要把這些項目推倒重來顯然不太現實。

 對于Windows 代碼來說,這一點有個例外(等到最後一篇吧:D)。

 譯者注:對于異常處理,顯然不是短短幾句話能夠說清楚的,以構造函數為例,很多C++

 書籍上都提到當構造失敗時隻有異常可以處理,Google禁止使用異常這一點,僅僅是為

 了自身的友善,說大了,無非是基于軟體管理成本上,實際使用中還是自己決定。

7. 運作時類型識别(Run-Time Type Information, RTTI)

 我們禁止使用RTTI。

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

 優點:

 RTTI 在某些單元測試中非常有用,如在進行工廠類測試時用于檢驗一個建立對象是否為期

 望的動态類型。

 除測試外,極少用到。

 缺點:運作時識别類型意味著設計本身有問題,如果你需要在運作期間确定一個對象的類型,

 這通常說明你需要重新考慮你的類的設計。

 結論:

 除單元測試外,不要使用RTTI,如果你發現需要所寫代碼因對象類型不同而動作各異的話,

 考慮換一種方式識别對象類型。

 虛函數可以實作随子類類型不同而執行不同代碼,工作都是交給對象本身去完成。

 如果工作在對象之外的代碼中完成,考慮雙重分發方案,如Visitor 模式,可以友善的在對

 象本身之外确定類的類型。

 如果你認為上面的方法你掌握不了,可以使用RTTI,但務必請三思,不要去手工實作一個

 貌似RTTI 的方案(RTTI-like workaround),我們反對使用RTTI,同樣反對貼上類型

 标簽的貌似類繼承的替代方案(譯者注,使用就使用吧,不使用也不要造輪子:D)。

8. 類型轉換(Casting)

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

 定義:C++引入了有别于C 的不同類型的類型轉換操作。

 優點:C 語言的類型轉換問題在于操作比較含糊:有時是在做強制轉換(如(int)3.5),有

 時是在做類型轉換(如(int)"hello")。另外,C++的類型轉換查找更容易、更醒目。

 缺點:文法比較惡心(nasty)。

 結論:使用C++風格而不要使用C 風格類型轉換。

 1) static_cast:和C 風格轉換相似可做值的強制轉換,或指針的父類到子類的明确的向上

 轉換;

 2) const_cast:移除const 屬性;

 3) reinterpret_cast:指針類型和整型或其他指針間不安全的互相轉換,僅在你對所做一

 切了然于心時使用;

 4) dynamic_cast:除測試外不要使用,除單元測試外,如果你需要在運作時确定類型信

 息,說明設計有缺陷(參考RTTI)。  

9. 流(Streams)

 隻在記錄日志時使用流。

 定義:流是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。

10. 前置自增和自減(Preincrement and Predecrement)

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

 定義:對于變量在自增(++i 或i++)或自減(--i 或i--)後表達式的值又沒有沒用到的情

 況下,需要确定到底是使用前置還是後置的自增自減。

 優點:不考慮傳回值的話,前置自增(++i)通常要比後置自增(i++)效率更高,因為後置

 的自增自減需要對表達式的值i 進行一次拷貝,如果i 是疊代器或其他非數值類型,拷貝的

 代價是比較大的。既然兩種自增方式動作一樣(譯者注,不考慮表達式的值,相信你知道我

 在說什麼),為什麼不直接使用前置自增呢?

 缺點:C 語言中,當表達式的值沒有使用時,傳統的做法是使用後置自增,特别是在for

 循環中,有些人覺得後置自增更加易懂,因為這很像自然語言,主語(i)在謂語動詞(++)

 前。

 結論:對簡單數值(非對象)來說,兩種都無所謂,對疊代器和模闆類型來說,要使用前置

 自增(自減)。

11. const的使用(Use of const)

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

 定義:在聲明的變量或參數前加上關鍵字const 用于指明變量值不可修改(如const int

 foo),為類中的函數加上const 限定表明該函數不會修改類成員變量的狀态(如class Foo

 { int Bar(char c) const; };)。

 優點:人們更容易了解變量是如何使用的,編輯器可以更好地進行類型檢測、更好地生成代

 碼。人們對編寫正确的代碼更加自信,因為他們知道所調用的函數被限定了能或不能修改變

 量值。即使是在無鎖的多線程程式設計中,人們也知道什麼樣的函數是安全的。

 缺點:如果你向一個函數傳入const 變量,函數原型中也必須是const 的(否則變量需要

 const_cast 類型轉換),在調用庫函數時這尤其是個麻煩。

 結論:const 變量、資料成員、函數和參數為編譯時類型檢測增加了一層保障,更好的盡早

 發現錯誤。是以,我們強烈建議在任何可以使用的情況下使用const:

 1) 如果函數不會修改傳入的引用或指針類型的參數,這樣的參數應該為const;

 2) 盡可能将函數聲明為const,通路函數應該總是const,其他函數如果不會修改任何數

 據成員也應該是const,不要調用非const 函數,不要傳回對資料成員的非const 指針或引

 用;

 3) 如果資料成員在對象構造之後不再改變,可将其定義為const。

 然而,也不要對const 過度使用,像const int * const * const x;就有些過了,即便這

 樣寫精确描述了x,其實寫成const int** x 就可以了。

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

 const 位置:

 有人喜歡int const *foo 形式不喜歡const int* foo,他們認為前者更加一緻是以可讀性

 更好:遵循了const 總位于其描述的對象(int)之後的原則。但是,一緻性原則不适用于

 此,“不要過度使用”的權威抵消了一緻性使用。将const 放在前面才更易讀,因為在自然語

 言中形容詞(const)是在名詞(int)之前的。

 這是說,我們提倡const 在前,并不是要求,但要兼顧代碼的一緻性!

12. 整型(Integer Types)

 C++内建整型中,唯一用到的是int,如果程式中需要不同大小的變量,可以使用<stdint.h>

 中的精确寬度(precise-width)的整型,如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 至少為32 位,但不要認為它會多于32 位,需要64 位整型的話,可以使用

 int64_t 或uint64_t。

 對于大整數,使用int64_t。

 不要使用uint32_t 等無符号整型,除非你是在表示一個位組(bit pattern)而不是一個數

 值。即使數值不會為負值也不要使用無符号類型,使用斷言(assertion,譯者注,這一點

 很有道理,計算機隻會根據變量、傳回值等有無符号确定數值正負,仍然無法确定對錯)來

 保護資料。

 無符号整型:

 有些人,包括一些教科書作者,推薦使用無符号類型表示非負數,類型表明了數值取值形式。

 但是,在C 語言中,這一優點被由其導緻的bugs 所淹沒。看看:

 for (unsigned int i = foo.Length()-1; i >= 0; --i) ...

 上述代碼永遠不會終止!有時gcc 會發現該bug 并報警,但通常不會。類似的bug 還會出

 現在比較有符合變量和無符号變量時,主要是C 的類型提升機制(type-promotion

 scheme,C 語言中各種内建類型之間的提升轉換關系)會緻使無符号類型的行為出乎你的

 意料。

 是以,使用斷言聲明變量為非負數,不要使用無符号型。

13. 64位下的可移植性(64-bit Portability)

 代碼在64 位和32 位的系統中,原則上應該都比較友好,尤其對于輸出、比較、結構對齊

 (structure alignment)來說:

 1) 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"

 注意宏PRI*會被編譯器擴充為獨立字元串,是以如果使用非常量的格式化字元串,需要将

 宏的值而不是宏名插入格式中,在使用宏PRI*時同樣可以在%後指定長度等資訊。例如,

 類型不要使用使用備注

 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

 printf("x = %30"PRIuS"\n", x)在32 位Linux 上将被擴充為printf("x = %30" "u"

 "\n", x),編譯器會處理為printf("x = %30u\n", x)。

 2) 記住sizeof(void *) != sizeof(int),如果需要一個指針大小的整數要使用intptr_t。

 3) 需要對結構對齊加以留心,尤其是對于存儲在磁盤上的結構體。在64 位系統中,任何

 擁有int64_t/uint64_t 成員的類/結構體将預設被處理為8 位元組對齊。如果32 位和64 位

 代碼共用磁盤上的結構體,需要確定兩種體系結構下的結構體的對齊一緻。大多數編譯器提

 供了調整結構體對齊的方案。gcc 中可使用__attribute__((packed)),MSVC 提供了

 #pragma pack()和__declspec(align())(譯者注,解決方案的項目屬性裡也可以直接設定)。

 4) 建立64 位常量時使用LL 或ULL 作為字尾,如:

 int64_t my_value = 0x123456789LL;

 uint64_t my_mask = 3ULL << 48;

 5) 如果你确實需要32 位和64 位系統具有不同代碼,可以在代碼變量前使用。(盡量不

 要這麼做,使用時盡量使修改局部化)。

14. 預處理宏(Preprocessor Macros)

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

 宏意味着你和編譯器看到的代碼是不同的,是以可能導緻異常行為,尤其是當宏存在于全局

 作用域中。

 值得慶幸的是,C++中,宏不像C 中那麼必要。宏内聯效率關鍵代碼(performance-critical

 code)可以内聯函數替代;宏存儲常量可以const 變量替代;宏“縮寫”長變量名可以引用

 替代;使用宏進行條件編譯,這個……,最好不要這麼做,會令測試更加痛苦(#define 防

 止頭檔案重包含當然是個例外)。

 宏可以做一些其他技術無法實作的事情,在一些代碼庫(尤其是底層庫中)可以看到宏的某

 些特性(如字元串化(stringifying,譯者注,使用#)、連接配接(concatenation,譯者注,

 使用##)等等)。但在使用前,仔細考慮一下能不能不使用宏實作同樣效果。

 譯者注:關于宏的進階應用,可以參考C 語言宏的進階應用。

 下面給出的用法模式可以避免一些使用宏的問題,供使用宏時參考:

 1) 不要在.h 檔案中定義宏;

 2) 使用前正确#define,使用後正确#undef;

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

 4) 不使用會導緻不穩定的C++構造(unbalanced C++ constructs,譯者注)的宏,

 至少文檔說明其行為。

15. 0和NULL(0 and NULL)

 整數用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',不僅類型正确而且可讀性好。

16. sizeof(sizeof)

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

 使用sizeof(varname)是因為當變量類型改變時代碼自動同步,有些情況下sizeof(type)

 或許有意義,還是要盡量避免,如果變量類型改變的話不能同步。

 Struct data;

 memset(&data, 0, sizeof(data));

 memset(&data, 0, sizeof(Struct));

17. Boost庫(Boost)

 隻使用Boost 中被認可的庫。

 定義:Boost 庫集是一個非常受歡迎的、同級評議的(peer-reviewed)、免費的、開源

 的C++庫。

 優點:Boost 代碼品質普遍較高、可移植性好,填補了C++标準庫很多空白,如型别特性

 (type traits)、更完善的綁定(binders)、更好的智能指針,同時還提供了TR1(标

 準庫的擴充)的實作。

 缺點:某些Boost 庫提倡的程式設計實踐可讀性差,像元程式(metaprogramming)和其他

 進階模闆技術,以及過度“函數化”("functional")的程式設計風格。

 結論:為了向閱讀和維護代碼的人員提供更好的可讀性,我們隻允許使用Boost 特性的一

 個成熟子集,目前,這些庫包括:

 1) Compressed Pair:boost/compressed_pair.hpp;

 2) Pointer Container:boost/ptr_container 不包括ptr_array.hpp 和序列化

 (serialization)。

 我們會積極考慮添加可以的Boost 特性,是以不必拘泥于該規則。 

***C++特性的注意事項,總結一下: 

 1. 對于智能指針,安全第一、友善第二,盡可能局部化(scoped_ptr);

 2. 引用形參加上const,否則使用指針形參;

 3. 函數重載的使用要清晰、易讀;

 4. 鑒于容易誤用,禁止使用預設函數參數(值得商榷);

 5. 禁止使用變長數組;

 6. 合理使用友元;

 7. 為了友善代碼管理,禁止使用異常(值得商榷);

 8. 禁止使用RTTI,否則重新設計代碼吧;

 9. 使用C++風格的類型轉換,除單元測試外不要使用dynamic_cast;

 10. 使用流還printf + read/write,it is a problem;

 11. 能用前置自增/減不用後置自增/減;

 12. const能用則用,提倡const在前;

 13. 使用确定大小的整型,除位組外不要使用無符号型;

 14. 格式化輸出及結構對齊時,注意32位和64位的系統差異;

 15. 除字元串化、連接配接外盡量避免使用宏;

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

 17. 用sizeof(varname)代替sizeof(type);

 18. 隻使用Boost中被認可的庫。

六、命名約定

1.用命名規則(General Naming Rules)

 函數命名、變量命名、檔案命名應具有描述性,不要過度縮寫,類型和變量應該是名詞,函

 數名可以用“指令性”動詞。

 如何命名:

 盡可能給出描述性名稱,不要節約空間,讓别人很快了解你的代碼更重要,好的命名選擇:

 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; // Most people know what "DNS" stands for.

 int price_count_reader; // OK, price count. Makes sense.

 // 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.

2. 檔案命名(File Names)

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

 可接受的檔案命名:

 my_useful_class.cc

 my-useful-class.cc

 myusefulclass.cc

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

 不要使用已經存在于/usr/include 下的檔案名(譯者注,對UNIX、Linux 等系統而言),

 如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檔案一節。

3. 類型命名(Type Names)

 類型命名每個單詞以大寫字母開頭,不包含下劃線:MyExcitingClass、MyExcitingEnum。

 所有類型命名——類、結構體、類型定義(typedef)、枚舉——使用相同約定,例如:

 // classes and structs

 class UrlTable { ...

 class UrlTableTester { ...

 struct UrlTableProperties { ...

 // typedefs

 typedef hash_map<UrlTableProperties *, string> PropertiesMap;

 // enums

 enum UrlTableErrors { ...

4. 變量命名(Variable Names)

 變量名一律小寫,單詞間以下劃線相連,類的成員變量以下劃線結尾,如

 my_exciting_local_variable、my_exciting_member_variable_。

 普通變量命名:

 舉例:

 string table_name; // OK - uses underscore.

 string tablename; // OK - all lowercase.

 string tableName; // Bad - mixed case.

 類資料成員:

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

 struct UrlTableProperties {

 string name;

 int num_entries;

 }

 結構體與類的讨論參考第三篇結構體vs.類一節。

 全局變量:

 對全局變量沒有特别要求,少用就好,可以以g_或其他易與局部變量區分的标志為字首。

5. 常量命名(Constant Names)

 在名稱前加k:kDaysInAWeek。

 所有編譯時常量(無論是局部的、全局的還是類中的)和其他變量保持些許差別,k 後接大

 寫字母開頭的單詞:

 const int kDaysInAWeek = 7;

6. 函數命名(Function Names)

 普通函數(regular functions,譯者注,這裡與通路函數等特殊函數相對)大小寫混合,

 存取函數(accessors and mutators)則要求與變量名比對: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_;

 };

 其他短小的内聯函數名也可以使用小寫字母,例如,在循環中調用這樣的函數甚至都不需要

 緩存其值,小寫命名就可以接受。

 譯者注:從這一點上可以看出,小寫的函數名意味着可以直接内聯使用。

7. 命名空間(Namespace Names)

 命名空間的名稱是全小寫的,其命名基于項目名稱和目錄結構:google_awesome_project。

 關于命名空間的讨論和如何命名,參考第二篇命名空間。

8. 枚舉命名(Enumerator Names)

 枚舉值應全部大寫,單詞間以下劃線相連:MY_EXCITING_ENUM_VALUE。

 枚舉名稱屬于類型,是以大小寫混合:UrlTableErrors。

 enum UrlTableErrors {

 OK = 0,

 ERROR_OUT_OF_MEMORY,

 ERROR_MALFORMED_INPUT,

 };

9. 宏命名(Macro Names)

 你并不打算使用宏,對吧?如果使用,像這樣:MY_MACRO_THAT_SCARES_SMALL_CHILDREN。

 參考第四篇預處理宏,通常是不使用宏的,如果絕對要用,其命名像枚舉命名一樣全部大寫、

 使用下劃線:

 #define ROUND(x) ...

 #define PI_ROUNDED 3.0

 MY_EXCITING_ENUM_VALUE

10. 命名規則例外(Exceptions to Naming Rules)

 當命名與現有C/C++實體相似的對象時,可參考現有命名約定:

 bigopen()

 函數名,參考open()

 uint

 typedef 類型定義

 bigpos

 struct 或class,參考pos

 sparse_hash_map

 STL 相似實體;參考STL 命名約定

 LONGLONG_MAX

 常量,類似INT_MAX

譯者:命名約定就相對輕松許多,在遵從代碼一緻性、可讀性的前提下,略顯随意:

 1. 總體規則:不要随意縮寫,如果說ChangeLocalValue寫作ChgLocVal還有情可

 原的話,把ModifyPlayerName寫作MdfPlyNm就太過分了,除函數名可适當為動

 詞外,其他命名盡量使用清晰易懂的名詞;

 2. 宏、枚舉等使用全部大寫+下劃線;

 3. 變量(含類、結構體成員變量)、檔案、命名空間、存取函數等使用全部小寫+下劃線 ,

 類成員變量以下劃線結尾,全局變量以g_開頭;

 4. 普通函數、類型(含類與結構體、枚舉類型)、常量等使用大小寫混合,不含下劃線;

 5. 參考現有或相近命名約定。

七、注釋

 注釋雖然寫起來很痛苦,但對保證代碼可讀性至為重要,下面的規則描述了應該注釋什麼、

 注釋在哪兒。當然也要記住,注釋的确很重要,但最好的代碼本身就是文檔(selfdocumenting),

 類型和變量命名意義明确要比通過注釋解釋模糊的命名好得多。

 注釋是為别人(下一個需要了解你的代碼的人)而寫的,認真點吧,那下一個人可能就是你!

1. 注釋風格(Comment Style)

 使用//或,統一就好。

 //或都可以,//隻是用的更加廣泛,在如何注釋和注釋風格上確定統一。

2. 檔案注釋(File Comments)

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

 法律公告和作者資訊:

 每一檔案包含以下項,依次是:

 1) 版權(copyright statement):如Copyright 2008 Google Inc.;

 2) 許可版本(license boilerplate):為項目選擇合适的許可證版本,如Apache 2.0、

 BSD、LGPL、GPL;

3) 作者(author line):辨別檔案的原始作者。

 如果你對其他人建立的檔案做了重大修改,将你的資訊添加到作者資訊裡,這樣當其他人對

 該檔案有疑問時可以知道該聯系誰。

 檔案内容:

 每一個檔案版權許可及作者資訊後,都要對檔案内容進行注釋說明。

 通常,.h 檔案要對所聲明的類的功能和用法作簡單說明,.cc 檔案包含了更多的實作細節或

 算法讨論,如果你感覺這些實作細節或算法讨論對于閱讀有幫助,可以把.cc 中的注釋放

 到.h 中,并在.cc 中指出文檔在.h 中。

 不要單純在.h 和.cc 間複制注釋,複制的注釋偏離了實際意義。

3. 類注釋(Class Comments)

 每個類的定義要附着描述類的功能和用法的注釋。

 // 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 {

 ...

 };

 如果你覺得已經在檔案頂部較長的描述了該類,想直接簡單的來上一句“完整描述見檔案頂部”

 的話,還是多少在類中加點注釋吧。

 如果類有任何同步前提(synchronization assumptions),文檔說明之。如果該類的實

 例可被多線程通路,使用時務必注意文檔說明。

4. 函數注釋(Function Comments)

 函數聲明處注釋描述函數功能,定義處描述函數實作。

 函數聲明:

 注釋于聲明之前,描述函數功能及用法,注釋使用描述式("Opens the file")而非指令式

 ("Open the file");注釋隻是為了描述函數而不是告訴函數做什麼。通常,注釋不會描

 述函數如何實作,那是定義部分的事情。

 函數聲明處注釋的内容:

 1) inputs(輸入)及outputs(輸出);

 2) 對類成員函數而言:函數調用期間對象是否需要保持引用參數,是否會釋放這些參數;

 3) 如果函數配置設定了空間,需要由調用者釋放;

 4) 參數是否可以為NULL;

 5) 是否存在函數使用的性能隐憂(performance implications);

 6) 如果函數是可重入的(re-entrant),其同步前提(synchronization assumptions)

 是什麼?

 舉例如下:

 // 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 檔案或其他地方的函數聲明處直接複制注釋,簡要說明函數功能是可以的,但重

 點要放在如何實作上。

5. 變量注釋(Variable Comments)

 通常變量名本身足以很好說明變量用途,特定情況下,需要額外注釋說明。

 類資料成員:

 每個類資料成員(也叫執行個體變量或成員變量)應注釋說明用途,如果變量可以接受NULL 或-1

 等警戒值(sentinel values),須說明之,如:

 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;

6. 實作注釋(Implementation Comments)

 對于實作代碼中巧妙的、晦澀的、有趣的、重要的地方加以注釋。

 代碼前注釋:

 出彩的或複雜的代碼塊前要加注釋,如:

 // 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.

 ...

 NULL、true/false、1、2、3……:

 向函數傳入、布爾值或整數時,要注釋說明含義,或使用常量讓代碼望文知意,比較一下:

 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++比你強:D:

 // Now go through the b array and make sure that if i occurs,

 // the next element is i+1.

 ... // Geez. What a useless comment.

7. 标點、拼寫和文法(Punctuation, Spelling and Grammar)

 留意标點、拼寫和文法,寫的好的注釋比差的要易讀的多。

 注釋一般是包含适當大寫和句點(.)的完整的句子,短一點的注釋(如代碼行尾的注釋)

 可以随意點,依然要注意風格的一緻性。完整的句子可讀性更好,也可以說明該注釋是完整

 的而不是一點不成熟的想法。

 雖然被别人指出該用分号(semicolon)的時候用了逗号(comma)有點尴尬。清晰易讀

 的代碼還是很重要的,适當的标點、拼寫和文法對此會有所幫助。

8. TODO注釋(TODO Comments)

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

 這樣的注釋要使用全大寫的字元串TODO,後面括号(parentheses)裡加上你的大名、郵

 件位址等,還可以加上冒号(colon):目的是可以根據統一的TODO 格式進行查找:

 // TODO([email protected]): Use a "*" here for concatenation operator.

 // TODO(Zeke) change this to use relations.

 如果加上是為了在“将來某一天做某事”,可以加上一個特定的時間("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很不錯,有時候,注釋确實是為了标記一些未完成的或完成的不盡如人意的地方 ,

 這樣一搜尋,就知道還有哪些活要幹,日志都省了。

八、格式

 代碼風格和格式确實比較随意,但一個項目中所有人遵循同一風格是非常容易的,作為個人

 未必同意下述格式規則的每一處,但整個項目服從統一的程式設計風格是很重要的,這樣做才能

 讓所有人在閱讀和了解代碼時更加容易。

1. 行長度(Line Length)

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

 我們也認識到這條規則是存有争議的,但如此多的代碼都遵照這一規則,我們感覺一緻性更

 重要。

 優點:提倡該原則的人認為強迫他們調整編輯器視窗大小很野蠻。很多人同時并排開幾個窗

 口,根本沒有多餘空間拓寬某個視窗,人們将視窗最大尺寸加以限定,一緻使用80 列寬,

 為什麼要改變呢?

 缺點:反對該原則的人則認為更寬的代碼行更易閱讀,80 列的限制是上個世紀60 年代的

 大型機的古闆缺陷;現代裝置具有更寬的顯示屏,很輕松的可以顯示更多代碼。

 結論:80 個字元是最大值。例外:

 1) 如果一行注釋包含了超過80 字元的指令或URL,出于複制粘貼的友善可以超過80 字

 符;

 2) 包含長路徑的可以超出80 列,盡量避免;

 3) 頭檔案保護(防止重複包含第一篇)可以無視該原則。

2. 非ASCII字元(Non-ASCII Characters)

 盡量不使用非ASCII 字元,使用時必須使用UTF-8 格式。

 哪怕是英文,也不應将使用者界面的文本寫死到源代碼中,是以非ASCII 字元要少用。特

 殊情況下可以适當包含此類字元,如,代碼分析外部資料檔案時,可以适當寫死資料檔案

 中作為分隔符的非ASCII 字元串;更常用的是(不需要本地化的)單元測試代碼可能包含

 非ASCII 字元串。此類情況下,應使用UTF-8 格式,因為很多工具都可以了解和處理其編

 碼,十六進制編碼也可以,尤其是在增強可讀性的情況下——如"\xEF\xBB\xBF"是Unicode

 的zero-width no-break space 字元,以UTF-8 格式包含在源檔案中是不可見的。

3. 空格還是制表位(Spaces vs. Tabs)

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

 使用空格進行縮進,不要在代碼中使用tabs,設定編輯器将tab 轉為空格。

4. 函數聲明與定義(Function Declarations and Definitions)

 傳回類型和函數名在同一行,合适的話,參數也放在同一行。

 函數看上去像這樣:

 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

 ...

 }

 注意以下幾點:

 1) 傳回值總是和函數名在同一行;

 2) 左圓括号(open parenthesis)總是和函數名在同一行;

 3) 函數名和左圓括号間沒有空格;

 4) 圓括号與參數間沒有空格;

 5) 左大括号(open curly brace)總在最後一個參數同一行的末尾處;

 6) 右大括号(close curly brace)總是單獨位于函數最後一行;

 7) 右圓括号(close parenthesis)和左大括号間總是有一個空格;

 8) 函數聲明和實作處的所有形參名稱必須保持一緻;

 9) 所有形參應盡可能對齊;

 10) 預設縮進為2 個空格;

 11) 獨立封裝的參數保持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 ) {}

 // Bad - if someone wants to implement later, it's not clear what the

 // variable means.

 void Circle::Rotate(double) {}

 譯者注:關于UNIX/Linux風格為什麼要把左大括号置于行尾(.cc檔案的函數實作處,

 左大括号位于行首),我的了解是代碼看上去比較簡約,想想行首除了函數體被一對大括

 号封在一起之外,隻有右大括号的代碼看上去确實也舒服;Windows風格将左大括号置

 于行首的優點是比對情況一目了然。

5. 函數調用(Function Calls)

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

 函數調用遵循如下形式:

 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);

 }

6. 條件語句(Conditionals)

 更提倡不在圓括号中添加空格,關鍵字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 和左圓括号間有個空格,右圓括号和左大括号(如果使用的話)間也要

 有個空格:

 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 分支是不允許的:

 // 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.

 }

 但如果語句中哪一分支使用了大括号的話,其他部分也必須使用:

 // 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;

 }

7. 循環和開關選擇語句(Loops and Switch Statements)

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

 switch 語句中的case 塊可以使用大括号也可以不用,取決于你的喜好,使用時要依下文所

 述。

 如果有不滿足case 枚舉條件的值,要總是包含一個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.

 while (condition); // Bad - looks like part of do/while loop.

8. 指針和引用表達式(Pointers and Reference Expressions)

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

 下面是指針和引用表達式的正确範例:

 x = *p;

 p = &x;

 x = r.y;

 x = r->y;

 注意:

 1) 在通路成員時,句點或箭頭前後沒有空格;

 2) 指針操作符*或&後沒有空格。

 在聲明指針變量或參數時,星号與類型或變量名緊挨都可以:

 // 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;

 char * c; // Bad - spaces on both sides of *

 const string & str; // Bad - spaces on both sides of &

 同一個檔案(建立或現有)中起碼要保持一緻。

 譯者注:個人比較習慣與變量緊挨的方式。

9. 布爾表達式(Boolean Expressions)

 如果一個布爾表達式超過标準行寬(80 字元),如果斷行要統一一下。

 下例中,邏輯與(&&)操作符總位于行尾:

 if (this_one_thing > this_other_thing &&

 a_third_thing == a_fourth_thing &&

 yet_another & last_one) {

 ...

 }

 兩個邏輯與(&&)操作符都位于行尾,可以考慮額外插入圓括号,合理使用的話對增強可讀

 性是很有幫助的。

 譯者注:個人比較習慣邏輯運算符位于行首,邏輯關系一目了然,各人喜好而已,至于加

 不加圓括号的問題,如果你對優先級了然于胸的話可以不加,但可讀性總是差了些。

10. 函數傳回值(Return Values)

 return 表達式中不要使用圓括号。

 函數傳回時不要使用圓括号:

 return x; // not return(x);

11. 變量及數組初始化(Variable and Array Initialization)

 選擇=還是()。

 需要做二者之間做出選擇,下面的形式都是正确的:

 int x = 3;

 int x(3);

 string name("Some Name");

 string name = "Some Name";

12. 預處理指令(Preprocessor Directives)

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

 即使預處理指令位于縮進代碼塊中,指令也應從行首開始。

 // Good - directives at beginning of line

 if (lopsided_score) {

 #if DISASTER_PENDING // Correct -- Starts at beginning of line

 DropEverything();

 #endif

 BackToNormal();

 }

 // 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();

 }

13. 類格式(Class Format)

 聲明屬性依次序是public:、protected:、private:,每次縮進1 個空格(譯者注,為什麼

 不是兩個呢?也有人提倡private 在前,對于聲明了哪些資料成員一目了然,還有人提倡依

 邏輯關系将變量與操作放在一起,都有道理:-))。

 類聲明(對類注釋不了解的話,參考第六篇中的類注釋一節)的基本格式如下:

 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);

 };

 注意:

 1) 是以基類名應在80 列限制下盡量與子類名放在同一行;

 2) 關鍵詞public:、protected:、private:要縮進1 個空格(譯者注,MSVC 多使用tab

 縮進,且這三個關鍵詞沒有縮進);

 3) 除第一個關鍵詞(一般是public)外,其他關鍵詞前空一行,如果類比較小的話也可以

 不空;

 4) 這些關鍵詞後不要空行;

 5) public 放在最前面,然後是protected 和private;

 6) 關于聲明次序參考第三篇聲明次序一節。

14. 初始化清單(Initializer Lists)

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

 兩種可以接受的初始化清單格式:

 // 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();

 ...

 }

15. 命名空間格式化(Namespace Formatting)

 命名空間内容不縮進。

 命名空間不添加額外縮進層次,例如:

 namespace {

 void foo() { // Correct. No extra indentation within namespace.

 ...

 }

 } // namespace

 不要縮進:

 namespace {

 // Wrong. Indented when it should not be.

 void foo() {

 ...

 }

 } // namespace

16. 水準留白(Horizontal Whitespace)

 水準留白的使用因地制宜。不要在行尾添加無謂的留白。

 普通:

 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.

 ...

 添加備援的留白會給其他人編輯時造成額外負擔,是以,不要加入多餘的空格。如果确定一

 行代碼已經修改完畢,将多餘的空格去掉;或者在專門清理空格時去掉(确信沒有其他人在

 使用)。

 循環和條件語句:

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

17. 垂直留白(Vertical Whitespace)

 垂直留白越少越好。

 這不僅僅是規則而是原則問題了:不是非常有必要的話就不要使用空行。尤其是:不要在兩

 個函數定義之間空超過2 行,函數體頭、尾不要有空行,函數體中也不要随意添加空行。

 基本原則是:同一屏可以顯示越多的代碼,程式的控制流就越容易了解。當然,過于密集的

 代碼塊和過于疏松的代碼塊同樣難看,取決于你的判斷,但通常是越少越好。

 函數頭、尾不要有空行:

 void Function() {

 // Unnecessary blank lines before and after

 }

 代碼塊頭、尾不要有空行:

 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. 行寬原則上不超過80列,把22寸的顯示屏都占完,怎麼也說不過去;

 2. 盡量不使用非ASCII字元,如果使用的話,參考UTF-8格式(尤其是UNIX/Linux

 下,Windows下可以考慮寬字元),盡量不将字元串常量耦合到代碼中,比如獨立出資

 源檔案,這不僅僅是風格問題了;

 3. UNIX/Linux下無條件使用空格,MSVC的話使用Tab也無可厚非;

 4. 函數參數、邏輯條件、初始化清單:要麼所有參數和函數名放在同一行,要麼所有參數

 并排分行;

 5. 除函數定義的左大括号可以置于行首外,包括函數/類/結構體/枚舉聲明、各種語句的

 左大括号置于行尾,所有右大括号獨立成行;

 6. ./->操作符前後不留白格,*/&不要前後都留,一個就可,靠左靠右依各人喜好;

 7. 預處理指令/命名空間不使用額外縮進,類/結構體/枚舉/函數/語句使用縮進;

 8. 初始化用=還是()依個人喜好,統一就好;

 9. return不要加();

 10. 水準/垂直留白不要濫用,怎麼易讀怎麼來。

九、規則之例外

 前面說明的編碼習慣基本是強制性的,但所有優秀的規則都允許例外。

1. 現有不統一代碼(Existing Non-conformant Code)

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

 當你修改使用其他風格的代碼時,為了與代碼原有風格保持一緻可以不使用本指南約定。如

 果不放心可以與代碼原作者或現在的負責人員商讨,記住,一緻性包括原有的一緻性。

2. Windows代碼(Windows Code)

 Windows 程式員有自己的編碼習慣,主要源于Windows 的一些頭檔案和其他Microsoft

 代碼。我們希望任何人都可以順利讀懂你的代碼,是以針對所有平台的C++編碼給出一個

 單獨的指導方案。

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

 者注,我怎麼感覺像在被洗腦:D):

 1) 不要使用匈牙利命名法(Hungarian notation,如定義整型變量為iNum),使用Google

 命名約定,包括對源檔案使用.cc 擴充名;

 2) Windows 定義了很多原有内建類型的同義詞(譯者注,這一點,我也很反感),如DWORD、

 HANDLE 等等,在調用Windows API 時這是完全可以接受甚至鼓勵的,但還是盡量使用原

 來的C++類型,例如,使用const TCHAR *而不是LPCTSTR;

 3) 使用Microsoft Visual C++進行編譯時,将警告級别設定為3 或更高,并将所有

 warnings當作errors處理;

 4) 不要使用#pragma once;作為包含保護,使用C++标準包含保護,包含保護的檔案路徑

 包含到項目樹頂層(譯者注,#include<prj_name/public/tools.h>);

 5) 除非萬不得已,否則不使用任何不标準的擴充,如#pragma 和__declspec,允許使用

 __declspec(dllimport)和__declspec(dllexport),但必須通過DLLIMPORT 和DLLEXPORT

 等宏,以便其他人在共享使用這些代碼時容易放棄這些擴充。

在Windows 上,隻有很少一些偶爾可以不遵守的規則:

 1) 通常我們禁止使用多重繼承,但在使用COM 和ATL/WTL 類時可以使用多重繼承,為

 了執行COM 或ATL/WTL 類及其接口時可以使用多重實作繼承;

 2) 雖然代碼中不應使用異常,但在ATL 和部分STL(包括Visual C++的STL)中異常

 被廣泛使用,使用ATL 時,應定義_ATL_NO_EXCEPTIONS 以屏蔽異常,你要研究一下是否也

 屏蔽掉STL 的異常,如果不屏蔽,開啟編譯器異常也可以,注意這隻是為了編譯STL,自

 己仍然不要寫含異常處理的代碼;

 3) 通常每個項目的每個源檔案中都包含一個名為StdAfx.h 或precompile.h 的頭檔案友善

 頭檔案預編譯,為了使代碼友善與其他項目共享,避免顯式包含此檔案(precompile.cc 除

 外),使用編譯器選項/FI 以自動包含;

 4) 通常名為resource.h、且隻包含宏的資源頭檔案,不必拘泥于此風格指南。

繼續閱讀