代碼規範



· 背景

Google的開源項目大多使用C++開發。每一個C++程序員也都知道,C++具有很多強大的語言特性,但這種強大不可避免的導致它的複雜,這種複雜會使得代碼更易於出現bug、難於閱讀和維護。

本指南的目的是通過詳細闡述在C++編碼時要怎樣寫、不要怎樣寫來規避其複雜性。這些規則可在允許代碼有效使用C++語言特性的同時使其易於管理。

風格,也被視爲可讀性,主要指稱管理C++代碼的習慣。使用術語風格有點用詞不當,因爲這些習慣遠不止源代碼文件格式這麼簡單。

使代碼易於管理的方法之一是增強代碼一致性,讓別人可以讀懂你的代碼是很重要的,保持統一編程風格意味着可以輕鬆根據模式匹配規則推斷各種符號的含義。創建通用的、必需的習慣用語和模式可以使代碼更加容易理解,在某些情況下改變一些編程風格可能會是好的選擇,但我們還是應該遵循一致性原則,儘量不這樣去做。

本指南的另一個觀點是C++特性的臃腫。C++是一門包含大量高級特性的巨型語言,某些情況下,我們會限制甚至禁止使用某些特性使代碼簡化,避免可能導致的各種問題,指南中列舉了這類特性,並解釋說爲什麼這些特性是被限制使用的。

Google開發的開源項目將遵照本指南約定。

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

· 頭文件

通常,每一個.cc文件(C++的源文件)都有一個對應的.h文件(頭文件),也有一些例外,如單元測試代碼和只包含main().cc文件。

正確使用頭文件可令代碼在可讀性、文件大小和性能上大爲改觀。

下面的規則將引導你規避使用頭文件時的各種麻煩。

1. #define的保護

所有頭文件都應該使用#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):當函數被聲明爲內聯函數之後,編譯器可能會將其內聯展開,無需按通常的函數調用機制調用內聯函數。

優點:當函數體比較小的時候,內聯該函數可以令目標代碼更加高效。對於存取函數(accessormutator)以及其他一些比較短的關鍵執行函數。

缺點:濫用內聯將導致程序變慢,內聯有可能是目標代碼量或增或減,這取決於被內聯的函數的大小。內聯較短小的存取函數通常會減少代碼量,但內聯一個很大的函數(譯者注:如果編譯器允許的話)將戲劇性的增加代碼量。在現代處理器上,由於更好的利用指令緩存(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中。

dir/foo.ccdir2/foo2.h通常位於相同目錄下(像base/basictypes_unittest.ccbase/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. 避免多重包含是學編程時最基本的要求;

2. 前置聲明是爲了降低編譯依賴,防止修改一個頭文件引發多米諾效應;

3. 內聯函數的合理使用可提高代碼執行效率;

4. -inl.h可提高代碼可讀性(一般用不到吧:D);

5. 標準化函數參數順序可以提高可讀性和易維護性(對函數參數的堆棧空間有輕微影響,我以前大多是相同類型放在一起);

6. 包含文件的名稱使用...雖然方便卻易混亂,使用比較完整的項目路徑看上去很清晰、很條理,包含文件的次序除了美觀之外,最重要的是可以減少隱藏依賴,使每個頭文件在最需要編譯(對應源文件處:D)的地方編譯,有人提出庫文件放在最後,這樣出錯先是項目內的文件,頭文件都放在對應源文件的最前面,這一點足以保證內部錯誤的及時發現了。

· 作用域

1. 命名空間(Namespaces

.cc文件中,提倡使用不具名的命名空間(unnamed namespaces,譯者注:不具名的命名空間就像不具名的類一樣,似乎被介紹的很少:-()。使用具名命名空間時,其名稱可基於項目或路徑名稱,不要使用using指示符。

定義:命名空間將全局作用域細分爲不同的、具名的作用域,可有效防止全局作用域的命名衝突。

優點:命名空間提供了(可嵌套)命名軸線(name axis,譯者注:將命名分割在不同命名空間內),當然,類也提供了(可嵌套)的命名軸線(譯者注:將命名分割在不同類的作用域內)。

舉例來說,兩個不同項目的全局作用域都有一個類Foo,這樣在編譯或運行時造成衝突。如果每個項目將代碼置於不同命名空間中,project1::Fooproject2::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)和全局函數(Global Functions

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

優點:某些情況下,非成員函數和靜態成員函數是非常有用的,將非成員函數置於命名空間中可避免對全局作用域的污染。

缺點:將非成員函數和靜態成員函數作爲新類的成員或許更有意義,當它們需要訪問外部資源或具有重要依賴時更是如此。

結論:

有時,不把函數限定在類的實體中是有益的,甚至需要這麼做,要麼作爲靜態成員,要麼作爲非成員函數。非成員函數不應依賴於外部變量,並儘量置於某個命名空間中。相比單純爲了封裝若干不共享任何靜態數據的靜態成員函數而創建類,不如使用命名空間。

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

如果你確實需要定義非成員函數,又只是在.cc文件中使用它,可使用不具名命名空間或static關聯(如static int Foo() {...})限定其作用域。

4. 局部變量(Local Variables

將函數變量儘可能置於最小作用域內,在聲明變量時將其初始化。

C++允許在函數的任何位置聲明變量。我們提倡在儘可能小的作用域中聲明變量,離第一次使用越近越好。這使得代碼易於閱讀,易於定位變量的聲明位置、變量類型和初始值。特別是,應使用初始化代替聲明+賦值的方式。

int i;
i = f();        // ——初始化和聲明分離
nt j = g();   // ——初始化時聲明

注意:gcc可正確執行for (int i = 0; i < 10; ++i)i的作用域僅限for循環),因此其他for循環中可重用iifwhile等語句中,作用域聲明(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類型的全局變量(包括STLstring, vector等等),因爲它們的初始化順序有可能導致構造出現問題。內建類型和由內建類型構成的沒有構造函數的結構體可以使用,如果你一定要使用class類型的全局變量,請使用單件模式(singleton pattern)。

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

const char kFrogSays[] = "ribbet";

雖然允許在全局作用域中使用全局變量,使用時務必三思。大多數全局變量應該是類的靜態數據成員,或者當其只在.cc文件中使用時,將其定義到不具名命名空間中,或者使用靜態關聯以限制變量的作用域。

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

______________________________________

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

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++中,關鍵字structclass幾乎含義等同,我們爲其人爲添加語義,以便爲定義的數據類型合理選擇使用哪個關鍵字。

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,才令BarFoo的子類。

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

譯者注:至於子類沒有額外數據成員,甚至父類也沒有任何數據成員的特殊情況下,析構函數的調用是否必要是語義爭論,從編程設計規範的角度看,在含有虛函數的父類中,定義虛析構函數絕對必要。

限定僅在子類訪問的成員函數爲protected,需要注意的是數據成員應始終爲私有。

當重定義派生的虛函數時,在派生類中明確聲明其爲virtual。根本原因:如果遺漏virtual,閱讀者需要檢索類的所有祖先以確定該函數是否爲虛函數(譯者注,雖然不影響其爲虛函數的本質)。

7. 多重繼承(Multiple Inheritance

真正需要用到多重實現繼承(multiple implementation inheritance)的時候非常少,只有當最多一個基類中含有實現,其他基類都是以Interface爲後綴的純接口類時纔會使用多重繼承。

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

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

缺點:真正需要用到多重實現繼承的時候非常少,多重實現繼承看上去是不錯的解決方案,通常可以找到更加明確、清晰的、不同的解決方案。

結論:只有當所有超類(superclass)除第一個外都是純接口時才能使用多重繼承。爲確保它們是純接口,這些類必須以Interface爲後綴。

注意:關於此規則,Windows下有種例外情況(譯者注,將在本譯文最後一篇的規則例外中闡述)。

8. 接口(Interface

接口是指滿足特定條件的類,這些類以Interface爲後綴(非必需)。

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

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

2) 沒有非靜態數據成員;

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

4) 如果是子類,也只能繼承滿足上述條件並以Interface爲後綴的類。

接口類不能被直接實例化,因爲它聲明瞭純虛函數。爲確保接口類的所有實現可被正確銷燬,必須爲之聲明虛析構函數(作爲第1條規則的例外,析構函數不能是純虛函數)。具體細節可參考Stroustrup的《The C++ Programming Language, 3rd edition》第12.4節。

優點:以Interface爲後綴可令他人知道不能爲該接口類增加實現函數或非靜態數據成員,這一點對於多重繼承尤其重要。另外,對於Java程序員來說,接口的概念已經深入人心。

缺點:Interface後綴增加了類名長度,爲閱讀和理解帶來不便,同時,接口特性作爲實現細節不應暴露給客戶。

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

9. 操作符重載(Operator Overloading

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

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

優點:使代碼看上去更加直觀,就像內建類型(如int)那樣,重載操作符使那些Equals()Add()等黯淡無光的函數名好玩多了。爲了使一些模板函數正確工作,你可能需要定義操作符。

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

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

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

3) 有的操作符可以對指針進行操作,容易導致bugsFoo + 4做的是一件事,而&Foo + 4可能做的是完全不同的另一件事,對於二者,編譯器都不會報錯,使其很難調試;

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

結論:

一般不要重載操作符,尤其是賦值操作(operator=)比較陰險,應避免重載。如果需要的話,可以定義類似Equals()CopyFrom()等函數。

然而,極少數情況下需要重載操作符以便與模板或標準”C++類銜接(如operator<<(ostream&, const T&)),如果被證明是正當的尚可接受,但你要儘可能避免這樣做。尤其是不要僅僅爲了在STL容器中作爲key使用就重載operator==operator<,取而代之,你應該在聲明容器的時候,創建相等判斷和大小比較的仿函數類型。

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

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

10. 存取控制(Access Control

將數據成員私有化,並提供相關存取函數,如定義變量foo_及取值函數foo()、賦值函數set_foo()

存取函數的定義一般內聯在頭文件中。

參考繼承和函數命名。

11. 聲明次序(Declaration Order

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

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

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

1) typedefsenums

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特有的風情

Google有很多自己實現的使C++代碼更加健壯的技巧、功能,以及有異於別處的C++的使用方式。

1. 智能指針(Smart Pointers

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

智能指針看上去是指針,其實是附加了語義的對象。以scoped_ptr爲例,scoped_ptr被銷燬時,刪除了它所指向的對象。shared_ptr也是如此,而且,shared_ptr實現了引用計數(reference-counting),從而只有當它所指向的最後一個對象被銷燬時,指針纔會被刪除。

一般來說,我們傾向於設計對象隸屬明確的代碼,最明確的對象隸屬是根本不使用指針,直接將對象作爲一個域(field)或局部變量使用。另一種極端是引用計數指針不屬於任何對象,這樣設計的問題是容易導致循環引用或其他導致對象無法刪除的詭異條件,而且在每一次拷貝或賦值時連原子操作都會很慢。

雖然不推薦這麼做,但有些時候,引用計數指針是最簡單有效的解決方案。

譯者注:看來,Google所謂的不同之處,在於儘量避免使用智能指針:D,使用時也儘量局部化,並且,安全第一。

· 其他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);

事實上這是一個硬性約定:輸入參數爲值或常數引用,輸出參數爲指針;輸入參數可以是常數指針,但不能使用非常數引用形參。

在強調參數不是拷貝而來,在對象生命期內必須一直存在時可以使用常數指針,最好將這些在註釋中詳細說明。bind2ndmem_funSTL適配器不接受引用形參,這種情況下也必須以指針形參聲明函數。

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. 變長數組和allocaVariable-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++PythonJava及其他與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)xint 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)的自增、自減運算符。

定義:對於變量在自增(++ii++)或自減(--ii--)後表達式的值又沒有沒用到的情況下,需要確定到底是使用前置還是後置的自增自減。

優點:不考慮返回值的話,前置自增(++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++沒有指定整型的大小,通常人們認爲short16位,int32位,long32位,long long64位。

優點:保持聲明統一。

缺點:C++中整型大小因編譯器和體系結構的不同而不同。

結論:

<stdint.h>定義了int16_tuint32_tint64_t等整型,在需要確定大小的整型時可以使用它們代替shortunsigned long long等,在C整型中,只使用int。適當情況下,推薦使用標準類型如size_tptrdiff_t

最常使用的是,對整數來說,通常不會用到太大,如循環計數等,可以使用普通的int。你可以認爲int至少爲32位,但不要認爲它會多於32位,需要64位整型的話,可以使用int64_tuint64_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 schemeC語言中各種內建類型之間的提升轉換關係)會致使無符號類型的行爲出乎你的意料。

因此,使用斷言聲明變量爲非負數,不要使用無符號型。

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"

 

類型

不要使用

使用

備註

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)32Linux上將被擴展爲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位常量時使用LLULL作爲後綴,如:

 

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. 0NULL0 and NULL

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

整數用0,實數用0.0,這一點是毫無爭議的。

對於指針(地址值),到底是用0還是NULLBjarne Stroustrup建議使用最原始的0,我們建議使用看上去像是指針的NULL,事實上一些C++編譯器(如gcc 4.1.0)專門提供了NULL的定義,可以給出有用的警告,尤其是sizeof(NULL)和sizeof(0)不相等的情況。

字符(串)用'\0',不僅類型正確而且可讀性好。

16. sizeofsizeof

儘可能用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 Pairboost/compressed_pair.hpp

2) Pointer Containerboost/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/writeit 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.

 

類型和變量名一般爲名詞:如FileOpenernum_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下的文件名(譯者注,對UNIXLinux等系統而言),如db.h

通常,儘量讓文件名更加明確,http_server_logs.h就比logs.h要好,定義類時文件名一般成對出現,如foo_bar.hfoo_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

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

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

在名稱前加kkDaysInAWeek

所有編譯時常量(無論是局部的、全局的還是類中的)和其他變量保持些許區別,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

structclass,參考pos

sparse_hash_map

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

LONGLONG_MAX

常量,類似INT_MAX

______________________________________

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

1. 總體規則:不要隨意縮寫,如果說ChangeLocalValue寫作ChgLocVal還有情可原的話,把ModifyPlayerName寫作MdfPlyNm就太過分了,除函數名可適當爲動詞外,其他命名儘量使用清晰易懂的名詞;

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

3. 變量(含類、結構體成員變量)、文件、命名空間、存取函數等使用全部小寫+下劃線,類成員變量以下劃線結尾,全局變量以g_開頭;

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

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

· 註釋

註釋雖然寫起來很痛苦,但對保證代碼可讀性至爲重要,下面的規則描述了應該註釋什麼、註釋在哪兒。當然也要記住,註釋的確很重要,但最好的代碼本身就是文檔(self-documenting),類型和變量命名意義明確要比通過註釋解釋模糊的命名好得多。

註釋是爲別人(下一個需要理解你的代碼的人)而寫的,認真點吧,那下一個人可能就是你!

1. 註釋風格(Comment Style

使用//或/* */,統一就好。

//或/* */都可以,//只是用的更加廣泛,在如何註釋和註釋風格上確保統一。

 

2. 文件註釋(File Comments

在每一個文件開頭加入版權公告,然後是文件內容描述。

法律公告和作者信息:

每一文件包含以下項,依次是:

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

2) 許可版本(license boilerplate):爲項目選擇合適的許可證版本,如Apache 2.0BSDLGPLGPL

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.

...

NULLtrue/false123……

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

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"Unicodezero-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 /*radians*/) {}

// 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放在最前面,然後是protectedprivate

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

對於現有不符合既定編程風格的代碼可以網開一面。

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

1. Windows代碼(Windows Code

Windows程序員有自己的編碼習慣,主要源於Windows的一些頭文件和其他Microsoft代碼。我們希望任何人都可以順利讀懂你的代碼,所以針對所有平臺的C++編碼給出一個單獨的指導方案。

如果你一直使用Windows編碼風格的,這兒有必要重申一下某些你可能會忘記的指南(譯者注,我怎麼感覺像在被洗腦:D):

1) 不要使用匈牙利命名法(Hungarian notation,如定義整型變量爲iNum),使用Google命名約定,包括對源文件使用.cc擴展名;

2) Windows定義了很多原有內建類型的同義詞(譯者注,這一點,我也很反感),如DWORDHANDLE等等,在調用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),但必須通過DLLIMPORTDLLEXPORT等宏,以便其他人在共享使用這些代碼時容易放棄這些擴展。

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

1) 通常我們禁止使用多重繼承,但在使用COMATL/WTL類時可以使用多重繼承,爲了執行COMATL/WTL類及其接口時可以使用多重實現繼承;

2) 雖然代碼中不應使用異常,但在ATL和部分STL(包括Visual C++STL)中異常被廣泛使用,使用ATL時,應定義_ATL_NO_EXCEPTIONS以屏蔽異常,你要研究一下是否也屏蔽掉STL的異常,如果不屏蔽,開啓編譯器異常也可以,注意這只是爲了編譯STL,自己仍然不要寫含異常處理的代碼;

3) 通常每個項目的每個源文件中都包含一個名爲StdAfx.hprecompile.h的頭文件方便頭文件預編譯,爲了使代碼方便與其他項目共享,避免顯式包含此文件(precompile.cc除外),使用編譯器選項/FI以自動包含;

4) 通常名爲resource.h、且只包含宏的資源頭文件,不必拘泥於此風格指南。

· 團隊合作

參考常識,保持一致。

編輯代碼時,花點時間看看項目中的其他代碼並確定其風格,如果其他代碼if語句中使用空格,那麼你也要使用。如果其中的註釋用星號(*)圍成一個盒子狀,你也這樣做:

/**********************************

* Some comments are here.

* There may be many lines.

**********************************/

編程風格指南的使用要點在於提供一個公共的編碼規範,所有人可以把精力集中在實現內容而不是表現形式上。我們給出了全局的風格規範,但局部的風格也很重要,如果你在一個文件中新加的代碼和原有代碼風格相去甚遠的話,這就破壞了文件本身的整體美觀也影響閱讀,所以要儘量避免。

好了,關於編碼風格寫的差不多了,代碼本身才是更有趣的,盡情享受吧!

Benjy Weinberger
Craig Silverstein
Gregory Eitzmann
Mark Mentovai
Tashana Landray

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