NO IMAGE

原文:http://www.cnblogs.com/flysnail/archive/2012/07/11/2586716.html

背景

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

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

風格,也被視為可讀性,主要指稱管理C++程式碼的習慣。使用術語風格有點用詞不當,因為這些習慣遠不止原始碼檔案格式這麼簡單。

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

本指南的另一個觀點是

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

由Google開發的開源專案將遵照本指南約定。

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

標頭檔案

通常,每一個.cc檔案(C++的原始檔)都有一個對應的.h檔案(標頭檔案),也有一些例外,如單元測試程式碼和只包含main()的.cc檔案。

正確使用標頭檔案可令程式碼在可讀性、檔案大小和效能上大為改觀。

下面的規則將引導你規避使用標頭檔案時的各種麻煩。

1. #define的保護

所有標頭檔案都應該使用#define防止標頭檔案被多重包含(multipleinclusion),命名格式當是:<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. 標頭檔案依賴

使用前置宣告(forwarddeclarations)儘量減少.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++函式引數分為輸入引數和輸出引數兩種,有時輸入引數也會輸出(譯者注:值被修改時)。輸入引數一般傳值或常數引用(constreferences),輸出引數或輸入/輸出引數為非常數指標(non-const
pointers)。對引數排序時,將所有輸入引數置於輸出引數之前。不要僅僅因為是新新增的引數,就將其置於最後,而應該依然置於輸出引數之前。

這一點並不是必須遵循的規則,輸入/輸出兩用引數(通常是類/結構體變數)混在其中,會使得規則難以遵循。

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

將包含次序標準化可增強可讀性、避免隱藏依賴(hiddendependencies,譯者注:隱藏依賴主要是指包含的檔案中編譯時),次序如下: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.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. 避免多重包含是學程式設計時最基本的要求;

2. 前置宣告是為了降低編譯依賴,防止修改一個標頭檔案引發多米諾效應;

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

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

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

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

Google C++程式設計風格指南(二)

作用域

1. 名稱空間(Namespaces)

在.cc檔案中,提倡使用不具名的名稱空間(unnamednamespaces,譯者注:不具名的名稱空間就像不具名的類一樣,似乎被介紹的很少:-()。使用具名名稱空間時,其名稱可基於專案或路徑名稱,不要使用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 forb...               // 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;
// 允許:.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型別!

______________________________________

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

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

2. 巢狀類符合區域性使用原則,只是不能在其他標頭檔案中前置宣告,儘量不要public;

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

4. 多執行緒中的全域性變數(含靜態成員變數)不要使用class型別(含STL容器),避免不明確行為導致的bugs。

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

Google C++程式設計風格指南(三)

這一篇主要提到的是類,Lippman在《Inside
The C++ Object Model
》第二章中對建構函式作了詳盡說明,本文中提到的幾個單詞基本仿該書中譯本侯捷先生的翻譯:

explicit:明確的

implicit:隱含的

trivial:沒有意義的

non-trivial:有意義的

 

原文地址:Google
C++ Style Guide

類是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的根源。拷貝建構函式降低了程式碼可讀性,相比按引用傳遞,跟蹤按值傳遞的物件更加困難,物件修改的地方變得難以捉摸。

結論:

大量的類並不需要可拷貝,也不需要一個拷貝建構函式或賦值操作(assignmentoperator)。不幸的是,如果你不主動宣告它們,編譯器會為你自動生成,而且是public的。

可以考慮在類的private中新增空的(dummy)拷貝建構函式和賦值操作,只有宣告,沒有定義。由於這些空程式宣告為private,當其他程式碼試圖使用它們的時候,編譯器將報錯。為了方便,可以使用巨集DISALLOW_COPY_AND_ASSIGN:

複製程式碼
// 禁止使用拷貝建構函式和賦值操作的巨集
// 應在類的private:中使用
#defineDISALLOW_COPY_AND_ASSIGN(TypeName) \
TypeName(constTypeName&); \ 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被用在僅包含資料的消極物件(passiveobjects)上,可能包括有關聯的常量,但沒有存取資料成員之外的函式功能,而存取功能通過直接訪問實現而無需方法呼叫,這兒提到的方法是指只用於處理資料成員的,如建構函式、解構函式、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)

真正需要用到多重實現繼承(multipleimplementation 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) 有的操作符可以對指標進行操作,容易導致bugs,Foo + 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) 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 C++程式設計風格指南(四)

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(conststring &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-LengthArrays 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) 異常是解決建構函式失敗的唯一方案,雖然可以通過工廠函式(factoryfunction)或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"
複製程式碼

型別

不要使用

使用

備註

void *(或其他指標型別)

%lx

%p

 

int64_t

%qd, %lld

%"PRId64"

 

uint64_t

%qu, %llu, %llx

%"PRIu64", %"PRIx64"

 

size_t

%u

%"PRIuS", %"PRIxS"

C99指定%zu

ptrdiff_t

%d

%"PRIdS"

C99指定%zd

 

注意巨集PRI*會被編譯器擴充套件為獨立字串,因此如果使用非常量的格式化字串,需要將巨集的值而不是巨集名插入格式中,在使用巨集PRI*時同樣可以在%後指定長度等資訊。例如,printf("x = %30"PRIuS"\n", x)在32位Linux上將被擴充套件為printf("x = %30" "u" "\n",x),編譯器會處理為printf("x = %30u\n", x)。

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) PointerContainer: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中被認可的庫。

Google C++程式設計風格指南(五)

命名約定

最重要的一致性規則是命名管理,命名風格直接可以直接確定命名實體是:型別、變數、函式、常量、巨集等等,無需查詢實體宣告,我們大腦中的模式匹配引擎依賴於這些命名規則。

命名規則具有一定隨意性,但相比按個人喜好命名,一致性更重要,所以不管你怎麼想,規則總歸是規則。

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

函式命名、變數命名、檔案命名應具有描述性,不要過度縮寫,型別和變數應該是名詞,函式名可以用“命令性”動詞。

如何命名:

儘可能給出描述性名稱,不要節約空間,讓別人很快理解你的程式碼更重要,好的命名選擇:

int num_errors;                  // Good.
int num_completed_connections;   // Good.

醜陋的命名使用模糊的縮寫或隨意的字元:

int n;                           // Bad - meaningless.
int nerr;                        // Bad - ambiguousabbreviation.
int n_comp_conns;                // Bad - ambiguousabbreviation.

型別和變數名一般為名詞:如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 asmall 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
andmutators)則要求與變數名匹配: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_; }
voidset_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. 參考現有或相近命名約定。

Google C++程式設計風格指南(六)

註釋

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

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

1. 註釋風格(Comment Style)

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

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

2. 檔案註釋(File Comments)

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

法律公告和作者資訊:

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

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

2) 許可版本(license boilerplate):為專案選擇合適的許可證版本,如Apache
2.0
BSDLGPLGPL

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
{
 
  ...
 
};

如果你覺得已經在檔案頂部詳細描述了該類,想直接簡單的來上一句“完整描述見檔案頂部”的話,還是多少在類中加點註釋吧。

如果類有任何同步前提(synchronizationassumptions),文件說明之。如果該類的例項可被多執行緒訪問,使用時務必注意文件說明。

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 donewith it,
// and it must not use the iterator once theGargantuanTable object
// on which the iterator was created has been deleted.
//
// The iterator is initially positioned at the beginningof the table.
//
// This method is equivalent to:
//    Iterator*iter = table->NewIterator();
//   iter->Seek("");
//    return iter;
// If you are going to immediately seek to another placein the
// returned iterator, it will be faster to useNewIterator()
// and avoid the extra seek.
Iterator* GetIterator() const;
複製程式碼

但不要有無謂冗餘或顯而易見的註釋,下面的註釋就沒有必要加上“returns false otherwise”,因為已經暗含其中了:

// Returns true if the table cannot hold any moreentries.
bool IsTableFull();

註釋構造/解構函式時,記住,讀程式碼的人知道構造/解構函式是什麼,所以“destroysthis object”這樣的註釋是沒有意義的。說明建構函式對引數做了什麼(例如,是否是指標的所有者)以及解構函式清理了什麼,如果都是無關緊要的內容,直接省掉註釋,解構函式前沒有註釋是很正常的。

函式定義:

每個函式定義時要以註釋說明函式功能和實現要點,如使用的漂亮程式碼、實現的簡要步驟、如此實現的理由、為什麼前半部分要加鎖而後半部分不需要。

不要從.h檔案或其他地方的函式宣告處直接複製註釋,簡要說明函式功能是可以的,但重點要放在如何實現上。

5. 變數註釋(Variable Comments)

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

類資料成員:

每個類資料成員(也叫例項變數或成員變數)應註釋說明用途,如果變數可以接受NULL或-1等警戒值(sentinel values),須說明之,如:

複製程式碼
private:
// Keeps track ofthe total number of entries in the table.
// Used to ensurewe do not go over the limit. -1 means
// that we don'tyet know how many entries the table has.
intnum_total_entries_; 
複製程式碼

全域性變數(常量):

和資料成員相似,所有全域性變數(常量)也應註釋說明含義及用途,如:

// The total number of tests cases that we run through inthis 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 thecomments line up.
DoSomethingElseThatIsLonger();  // Comment here so there are two spacesbetween
// the code andthe 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 ioccurs,
// 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 forconcatenation operator.
// TODO(Zeke) change this to use relations.

如果加上是為了在“將來某一天做某事”,可以加上一個特定的時間("Fix by November2005")或事件("Remove this code whenall 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很不錯,有時候,註釋確實是為了標記一些未完成的或完成的不盡如人意的地方,這樣一搜尋,就知道還有哪些活要幹,日誌都省了。

Google C++程式設計風格指南(七)

格式

程式碼風格和格式確實比較隨意,但一個專案中所有人遵循同一風格是非常容易的,作為個人未必同意下述格式規則的每一處,但整個專案服從統一的程式設計風格是很重要的,這樣做才能讓所有人在閱讀和理解程式碼時更加容易。

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轉為空格。

譯者注:在前段時間的關於Debian開發學習日記一文中,曾給出針對C/C++編碼使用的vim配置。

4. 函式宣告與定義(Function Declarations and Definitions)

返回型別和函式名在同一行,合適的話,引數也放在同一行。

函式看上去像這樣:

複製程式碼
ReturnType ClassName::FunctionName(Type par_name1, Typepar_name2) {
DoSomething();
...
}
複製程式碼

如果同一行文字較多,容不下所有引數:

複製程式碼
ReturnType ClassName::ReallyLongFunctionName(Typepar_name1,
Type par_name2,
Type par_name3) {
DoSomething();
...
}
複製程式碼

 甚至連第一個引數都放不下:

複製程式碼
ReturnTypeLongClassName::ReallyReallyReallyLongFunctionName(
Typepar_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 singleline
ReturnType FunctionName(Type par) const {
...
}
// This function signature requires multiple lines, but
// the const keyword is on the line with the lastparameter.
ReturnType ReallyLongFunctionName(Type par1,
Type par2)const {
...
}
複製程式碼

如果有些引數沒有用到,在函式定義處將引數名註釋起來:

複製程式碼
// Always have named parameters in interfaces.
class Shape {
public:
virtual voidRotate(double radians) = 0;
}
// Always have named parameters in the declaration.
class Circle : public Shape {
public:
virtual voidRotate(double radians);
}
// Comment out unused named parameters in definitions.
void Circle::Rotate(double /*radians*/) {}
// Bad - if someone wants to implement later, it's notclear 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 {  // Theelse goes on the same line as the closing brace.
...
}
複製程式碼

如果你傾向於在圓括號內部加空格:

複製程式碼
if ( condition ) { // spaces inside parentheses - rare
...  // 2 space indent.
} else {  // Theelse 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 isan 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 testuntil 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;    // butremember 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;  // notreturn(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) {
#ifDISASTER_PENDING  // Wrong!  The "#if" should be at beginning ofline
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.
explicitMyClass(int var);
~MyClass() {}
voidSomeFunction();
void SomeFunctionThatDoesNothing(){
}
voidset_some_var(int var) { some_var_ = var; }
int some_var()const { return some_var_; }
private:
boolSomeInternalFunction();
int some_var_;
intsome_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 spaceindent some_other_var_(var + 1) { //lined up ... DoSomething(); ... }
複製程式碼

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

名稱空間內容不縮排。

名稱空間不新增額外縮排層次,例如:

複製程式碼
namespace {
void foo() {  //Correct.  No extra indentation withinnamespace.
...
}
}  // 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, putthem on both sides!
// Spaces around the colon in inheritance and initializerlists.
class Foo : public Bar {
public:
// For inlinefunction implementations, put spaces between the braces
// and theimplementation itself.
Foo(int b) :Bar(), baz_(b) {}  // No spaces insideempty braces.
void Reset() {baz_ = 0; }  // Spaces separating bracesfrom 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 aspace before the
// semicolon.
switch (i) {
case 1:         // No space before colon in a switchcase.
...
case 2:break;  // Use a space after a colon if there'scode 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 pointerare
// okay, but beconsistent.
set<list<string> > x;       // C++ requires a space in > >.
set< list<string> > x;      // You may optionally make use
// symmetricspacing in < <. 
複製程式碼

17. 垂直留白(Vertical Whitespace)

垂直留白越少越好。

這不僅僅是規則而是原則問題了:不是非常有必要的話就不要使用空行。尤其是:不要在兩個函式定義之間空超過2行,函式體頭、尾不要有空行,函式體中也不要隨意新增空行。

基本原則是:同一屏可以顯示越多的程式碼,程式的控制流就越容易理解。當然,過於密集的程式碼塊和過於疏鬆的程式碼塊同樣難看,取決於你的判斷,但通常是越少越好。

函式頭、尾不要有空行:

void Function() {
// Unnecessaryblank lines before and after
}

程式碼塊頭、尾不要有空行:

複製程式碼
while (condition) {
// Unnecessaryblank line after
}
if (condition) {
// Unnecessaryblank line before
}
if-else塊之間空一行還可以接受:
if (condition) {
// Some lines ofcode too small to move to another function
// followed by ablank line.
} else {
// Another blockof code
}
複製程式碼

 

______________________________________

譯者:首先說明,對於程式碼格式,因人、因系統各有優缺點,但同一個專案中遵循同一標準還是有必要的:

1. 行寬原則上不超過80列,把22寸的顯示屏都佔完,怎麼也說不過去;

2. 儘量不使用非ASCII字元,如果使用的話,參考UTF-8格式(尤其是UNIX/Linux下,Windows下可以考慮寬字元),儘量不將字串常量耦合到程式碼中,比如獨立出資原始檔,這不僅僅是風格問題了;

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

4. 函式引數、邏輯條件、初始化列表:要麼所有引數和函式名放在同一行,要麼所有引數並排分行;

5. 除函式定義的左大括號可以置於行首外,包括函式/類/結構體/列舉宣告、各種語句的左大括號置於行尾,所有右大括號獨立成行;

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

7. 預處理指令/名稱空間不使用額外縮排,類/結構體/列舉/函式/語句使用縮排;

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

9. return不要加();

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

Google C++程式設計風格指南(八)[完]

規則之例外

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

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

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

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

1. 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) 通常我們禁止使用多重繼承,但在使用COMATL/WTL類時可以使用多重繼承,為了執行COMATL/WTL類及其介面時可以使用多重實現繼承;

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

3) 通常每個專案的每個原始檔中都包含一個名為StdAfx.h或precompile.h的標頭檔案方便標頭檔案預編譯,為了使程式碼方便與其他專案共享,避免顯式包含此檔案(precompile.cc除外),使用編譯器選項/FI以自動包含;

4) 通常名為resource.h、且只包含巨集的資源標頭檔案,不必拘泥於此風格指南。

團隊合作

參考常識,保持一致。

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

複製程式碼
/**********************************
* Some comments are here.
* There may be many lines.
**********************************/
複製程式碼

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

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