1. 程式人生 > >Google's C++ coding style

Google's C++ coding style

v0.2 - Last updated November 8, 2013

目錄 由 DocToc生成
     標頭檔案
        #define用法
        前向宣告
        行內函數
        -inl.h檔案
        函式引數順序
        include的命名和順序
    作用域
        名稱空間
            未名稱空間
            名稱空間
        巢狀類
        非成員函式、靜態成員函式、全域性函式
        區域性變數
        靜態變數和全域性變數

    類
        在建構函式裡面完成工作
        初始化
        顯式建構函式
        拷貝建構函式
        委派和繼承建構函式
        結構體 vs 類
        繼承
        多重繼承
        介面
        操作符過載
        訪問控制
        宣告順序
        編寫短函式
    其它C++特性
        所有權和智慧指標
        引用引數
        右值引用
        函式過載
        預設引數
        變長陣列和alloca

        友元
        異常
        執行時型別識別
        轉換
        流
        前置自增和自減
        const用法
        constexpr用法
        Integer型別
            Unsigned Integers型別
        64位移植性
        預處理巨集
        0和nullptr/NULL
        sizeof
        auto
        大括號初始化
        Lambda表示式
        Boost

        C++11
        一般命名規則
        檔名
        型別名
        變數名
            普通變數名
            類資料成員
            結構體變數
            全域性變數
        常量名
        函式名稱
            一般函式
            訪問器和儲存器
        名稱空間的名稱
        列舉器名稱
        巨集命名
        異常的命名規則
            bigopen()
            uint
            bigpos
            sparse_hash_map
            LONGLONG_MAX
    註釋
        Doxygen
        註釋規範
        檔案註釋
            法律宣告和作者
            檔案內容
        類註釋
        函式註釋
            函式宣告
            函式定義
        變數註釋
            類成員
            全域性變數
        實現註釋
            類資料成員
            單行註釋
            nullptr/NULL, true/false, 1, 2, 3...
            Don'ts
        標點,拼寫和語法
        TODO註釋
        棄用註釋
    格式化
        行長度
        非ASCII字元
        空格還是製表位
        函式宣告與定義
        函式呼叫
        大括號初始化列表
        條件語句
        迴圈和選擇語句
        指標和引用表示式
        布林表示式
        返回值
        變數和陣列初始化
        前處理器指令
        類格式
        建構函式初始化列表
        名稱空間格式化
        水平空白
            一般
            迴圈和條件
            操作符
            模版和型別轉換
        垂直空白
    例外的規則
        現存的不符合標準的程式碼
        Windows程式碼

標頭檔案

一般情況下,每.CPP檔案應該有一個相關的·h檔案。有一些常見的例外,如單元測試程式碼和只包含一個main函式的cpp檔案。

正確使用標頭檔案在可讀性,檔案大小和效能上有很大差異。

下面的規則將指導您繞過標頭檔案使用中的各種陷阱。

define用法

所有標頭檔案應該由#define防護,以避免多重包含。符號名稱的格式應該是<PROJECT>_<PATH>_<FILE>_H_

為了保證唯一性,它們應根據在專案的原始碼樹的完整路徑。例如,在檔案中FOO專案cocos2dx/sprites_nodes/CCSprite.h應具有以下防護:

1 2 3 4 5 6 #ifndef COCOS2DX_SPRITE_NODES_CCSPRITE_H_ #define COCOS2DX_SPRITE_NODES_CCSPRITE_H_ ... #endif  // COCOS2DX_SPRITE_NODES_CCSPRITE_H_
1 2 // Pragma once is still open for debate #pragma once

我們在考慮是是否使用#pragma once,我們不確定他能支援所有平臺。

前向宣告

前向宣告普通類可以避免不必要的#includes

定義:“前向宣告”是類、函式或者模版的宣告,沒有定義。用前向宣告來替代#include通常應用在客戶端程式碼中。

優點:

  • 不必要的#includes會強制編譯器開啟更多的檔案並處理更多的輸入。
  • 不必要的#includes也會導致程式碼被更經常重新編譯,因為標頭檔案修改。

缺點:

  • 不容易確定模版、typedefs、預設引數等的前向宣告以及使用宣告。
  • 不容易判斷對給定的程式碼該用前向宣告還是#include,尤其是當有隱式轉換時。極端情況下,用#include代替前向宣告會悄悄的改變程式碼的含義。
  • 在標頭檔案中多個前向宣告比#include囉嗦。
  • 前向宣告函式或者模版會阻止標頭檔案對APIs做“否則相容”(otherwise-compatible)修改;例如,擴充套件引數型別或者新增帶有預設值的模版引數。
  • 前向宣告std名稱空間的符號通常會產生不確定的行為。
  • 為了前向宣告而結構化程式碼(例如,適用指標成員,而不是物件成員)會使程式碼更慢更復雜。
  • 前向宣告的實際效率提升未經證實。

結論:

  • 使用標頭檔案中宣告的函式,總是#include該標頭檔案。
  • 使用類模版,優先使用#include
  • 使用普通類,可以用前向宣告,但是注意前向宣告可能不夠或者不正確的情況;如果有疑問,就用#include
  • 不應只是為了避免#include而用指標成員代替資料成員。

總是#include實際宣告/定義的檔案;不依賴非直接包含的標頭檔案中間接引入的符號。例外是,Myfile.cpp可以依賴Myfile.h中的#include和前向宣告。

行內函數

只在函式體很小——10行程式碼以內——的時候將其定義為行內函數。

定義:你可以在宣告函式時允許編譯器將其擴充套件內聯,而不是通過常見的函式呼叫機制呼叫。

優點: 內聯短小精悍的函式可以生成更高效的物件碼。推薦內聯取值函式、設值函式以及其餘效能關鍵的短函式。

缺點: 濫用內聯可能導致程式更慢。內聯可能讓程式碼尺寸增加或者減少,這取決於函式的尺寸。內聯一個非常小的取值函式通常會減少程式碼尺寸,而內聯一個非常大的函式會顯著增加程式碼尺寸。在現代處理器架構下,更小尺寸的程式碼因為可以更好的利用指令快取,通常跑得更快。

結論:一個黃金法則是不要內聯超過10行的函式。要小心解構函式,因為隱含成員和基類的解構函式,它們通常比看上去的要長。

另一個黃金法則:通常不建議內聯帶迴圈或者switch語句的函式(除非,大部分情況下,迴圈或者switch語句不會被執行)

需要注意的是,即便函式被宣告為內聯他們也不一定會真的內聯;例如虛擬函式以及遞迴函式一般都不會被內聯。通常遞迴函式不應該被內聯。將虛擬函式內聯的主要原因是為了方便或者文件需要,將其定義放在類中,例如取值函式以及設值函式。

-inl.h檔案

如果有需要,可以用帶-inl.h字尾的檔案來定義複雜行內函數。

行內函數的定義必須放在標頭檔案中,這樣編譯器在函式呼叫處內聯展開時才有函式定義可用。但實現程式碼通常還是放在.cpp檔案比較合適,因為除非會帶來可讀性或者效能上的好處,否則我們不希望在.h檔案裡堆放太多具體的程式碼。

如果一個行內函數的定義非常短,只含有少量邏輯,你可以把程式碼放在你的.h檔案裡。例如取值函式與設值函式都毫無疑問的應該放在類定義中。更復雜的行內函數為了實現者和呼叫者的方便,也要放在.h檔案裡,但是如果這樣會讓.h檔案過於臃腫,你也可以將其放在一個單獨的-inl.h檔案裡。這樣可以將具體實現與類定義分開,同時又確保了實現在需要用到的時候是被包含的。

-inl.h檔案還有一個用途是存放函式模板的定義。這樣可以讓你的模板定義更加易讀。

不要忘記,就像其他的標頭檔案一樣,一個-inl.h檔案也是需要#define防護的。

函式引數順序

定義函式時,引數順序應該為:輸入,然後是輸出。

C/C++函式的引數要麼是對函式的輸入,要麼是函式給出的輸出,要麼兩者兼是。輸入引數通常是值或者常引用,而輸出以及輸入/輸出引數是非const指標。在給函式引數排序時,將所有僅輸入用的引數放在一切輸出引數的前面。特別需要注意的是,在加新引數時不要因為它們是新的就直接加到最後去;新的僅輸入用引數仍然要放到輸出引數前。

這不是一條不可動搖的鐵律。那些既用於輸入又用於輸出的引數(通常是類/結構體)通常會把水攪渾,同時,為了保持相關函式的一致性,有時也會使你違背這條原則。

include的命名和順序

使用以下標準順序以增加可讀性,同時避免隱藏的依賴關係:C庫,C++庫,其他庫的.h檔案,你自己專案的.h檔案。

所有本專案的標頭檔案都應該包含從原始碼根目錄開始的完整路徑,而不要使用UNIX的目錄快捷方式.(當前目錄)或者..(上層目錄)。例如google-awesome-project/src/base/logging.h應寫為以下方式

1 #include "base/logging.h"

例如有檔案dir/foo.cppdir/foo_test.cpp,他們的主要用途是實現或者測試dir2/foo2.h標頭檔案裡的內容,那麼include的順序應該如下:

  • dir2/foo2.h (推薦位置——理由見後)
  • C system files.
  • C++ system files.
  • Other libraries' .h files.
  • Your project's .h files.

按照這個推薦順序,如果dir2/foo2.h漏掉了什麼必須的包含檔案,dir/foo.cpp或者dir/foo_test.cpp就會編譯失敗。這樣的規則就保證了工作在這些檔案的人而不是在其他包工作的無辜的人最先發現問題。

dir/foo.cppdir2/foo2.h通常位於同一個目錄(例如base/basictypes_test.cppbase/basictypes.h),但是在不同目錄也沒問題。

在同一部分中包含檔案應該按照字母順序排列。注意如果老程式碼不符合這條規則,那就在方便的時候改過來。

例如cocos2dx/sprite_nodes/CCSprite.cpp的include部分可能如下:

1 2 3 4 5 6 7 8 9 10 #include "sprite_nodes/CCSprite.h"  // Preferred location. #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 5 6 7 8 #include "foo/public/fooserver.h" #include "base/port.h" // For LANG_CXX11. #ifdef LANG_CXX11 #include <initializer_list> #endif  // LANG_CXX11

作用域

名稱空間

在.cpp檔案中,提倡使用未命名的名稱空間(unnamed namespaces,注:未命名的名稱空間就像未命名的類一樣,似乎被介紹的很少:-()。使用命名的名稱空間時,其名稱可基於專案的路徑名稱。不要使用using指示符。不要使用內聯名稱空間。

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

優點:名稱空間提供了(層次化的)命名軸(name axis,注:將命名分割在不同名稱空間內),當然,類也提供了(層次化的)的命名軸。

舉例來說,兩個不同專案的全域性作用域都有一個類Foo,這樣在編譯或執行時造成衝突。如果每個專案將程式碼置於不同名稱空間中,project1::Foo和project2::Foo作為不同符號自然不會衝突。

內聯命令空間自動地將名字置於封閉作用域。例子如下:

1 2 3 4 5 namespace X { inline namespace Y { void foo(); } }

X::Y::foo()X::foo()是一樣的。內聯名稱空間是為了相容不同版本的ABI而做的擴充套件。

缺點:名稱空間具有迷惑性,因為它們和類一樣提供了額外的(層次化的)命名軸。

特別是內聯名稱空間,因為命名實際上並不侷限於他們宣告的名稱空間。只有作為較大的版本控制策略的一部分時才有用。

在標頭檔案中使用未命名的空間容易違背C++的唯一定義原則(One Definition Rule, ODR)。

結論:根據下文將要提到的策略合理使用名稱空間。如例子中那樣結束名稱空間時進行註釋。

未名稱空間

允許甚至鼓勵在.cpp中使用未名稱空間,以避免執行時的命名衝突:

1 2 3 4 5 6 7 namespace {                           // This is in a .cpp file. // The content of a namespace is not indented enum { UNUSED, EOF, ERROR };         // Commonly used tokens. bool atEof() { return _pos == EOF; }  // Uses our namespace's EOF. }  // namespace

然而,與特定類關聯的檔案作用域宣告在該類中被宣告為型別、靜態資料成員戒靜態成員函式,而不是未命名名稱空間的成員。不能在.h檔案中使用未名稱空間。

名稱空間

命名的名稱空間使用規則如下:

名稱空間將除檔案包含、全域性標識的宣告/定義以及類的前置宣告外的整個原始檔封裝起來,以同其他名稱空間相區分。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 // .h檔案 // 使用cocos2d名稱空間 NS_CC_BEGIN // 所有宣告均在名稱空間作用域內。 // 注意不用縮排。 class MyClass { public: ... void foo(); }; NS_CC_END
1 2 3 4 5 6 7 8 9 10 11 12 13 14 // .h檔案 // 不使用cocos2d名稱空間 namespace mynamespace { // 所有宣告均在名稱空間作用域中。 // 注意不用縮排。 class MyClass { public: ... void foo(); }; }  // namespace mynamespace
1 2 3 4 5 6 7 8 9 10 // .cpp檔案 namespace mynamespace { // 函式定義在名稱空間作用域中。 void MyClass::foo() { ... } }  // namespace mynamespace

通常.cpp檔案會包含更多、更復雜的細節,包括引用其他名稱空間中的類等。

1 2 3 4 5 6 7 8 9 10 11 12 #include "a.h" DEFINE_bool(someflag, false"dummy flag"); class C;  // 前向宣告全域性作用域中的類C。 namespace a { class A; }  // 前向宣告a::A。 namespace b { ...code for b...         // 程式碼無縮排。 }  // namespace b
  • 不要宣告std名稱空間裡的任何內容,包括標準庫類的前置宣告。宣告std裡的實體會導致不明確的行為,例如,不可移植。包含對應的標頭檔案來宣告標準庫裡的實體。最好不要使用using指示符,以保證名稱空間下的所有名稱都可以正常使用。
1 2 // 禁止--汙染了名稱空間。 using namespace foo;
  • .h的函式、方法、類,.cpp的任何地方都可以使用using宣告。
1 2 3 // 在.cpp中沒有問題。 // 在.h中必須在函式、方法或者累中。 using ::foo::bar;
  • .h的函式、方法或包含整個.h的命名的名稱空間中以及.cpp中,可以使用命名空間別名。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // .cpp中一些常用名的縮寫 namespace fbz = ::foo::bar::baz; // .h中一些常用名的縮寫 namespace librarian { // 包括該標頭檔案(在librarian名稱空間中)在內的所有檔案都可以使用下面的別名: // 因此同一個專案中的別名應該保持一致。 namespace pd_s = ::pipeline_diagnostics::sidetable; inline void myInlineFunction() { // 函式或者方法中的本地命名空間別名。 namespace fbz = ::foo::bar::baz; ... } }  // namespace librarian

注意,.h檔案中的別名對所有包含該檔案的所有檔案都可見,因此公共的標頭檔案(在專案外仍可用)以及通過他們間接辦好的標頭檔案應避免定義別名,為了保持公共的APIs儘可能小。

  • 不要用內聯名稱空間。

巢狀類

當公開巢狀類作為介面的一部分時,雖然可以直接將他們保持在全域性作用域中,但將巢狀類的宣告置於名稱空間中是更好的選擇。

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

1 2 3 4 5 6 7 8 9 class Foo { private: // Bar是巢狀在Foo中的成員類 class Bar { ... }; };

優點:當巢狀(成員)類只在被巢狀類(enclosing class)中使用時很有用,將其置於被巢狀類作用域作為被巢狀類的成員不會汙染其他作用域同名類。可在被巢狀類中前置宣告巢狀類,在.cpp檔案中定義巢狀類,避免在被巢狀類宣告中包含巢狀類的定義,因為巢狀類的定義通常只與實現相關。

缺點:只能在被巢狀類的定義中才能前置宣告巢狀類。因此,任何使用Foo::Bar*指標的標頭檔案必須包含整個Foo類的宣告。