c++開發規範

目錄html

c++代碼規範

前言c++

做者:孟賽git

版本:v1.1程序員

本規範參照 google-cpp-styleguidegithub

修訂web

V1.0 2019/04/15 初始版本算法

V1.1 2019/04/27 添加章節5.9 指針 5.10 newx 修正部份內容。shell

1. 頭文件

一般每個 .cc文件都有一個對應的 .h 文件. 也有一些常見例外, 如單元測試代碼和只包含 main() 函數的 .cc 文件.編程

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

下面的規則將引導你規避使用頭文件時的各類陷阱.

1.1. Self-contained 頭文件

Tip

頭文件應該可以自給自足(self-contained,也就是能夠做爲第一個頭文件被引入),以 .h 結尾。至於用來插入文本的文件,說到底它們並非頭文件,因此應以 .inc 結尾。不容許分離出 -inl.h 頭文件的作法.

全部頭文件要可以自給自足。換言之,用戶和重構工具不須要爲特別場合而包含額外的頭文件。詳言之,一個頭文件要有 1.2. #define 保護 ,通通包含它所須要的其它頭文件,也不要求定義任何特別符號 (symbols) .

不過有一個例外,即一個文件並非 self-contained 的,而是做爲文本插入到代碼某處。或者,文件內容其實是其它頭文件的特定平臺(platform-specific)擴展部分。這些文件就要用 .inc 文件擴展名。

若是 .h 文件聲明瞭一個模板或內聯函數,同時也在該文件加以定義。凡有用到這些的 .cc 文件,就得通通包含該頭文件,不然程序可能會在構建中連接失敗。

有個例外:若是某函數模板爲全部相關模板參數顯式實例化,或自己就是某類的一個私有成員,那麼它就只能定義在實例化該模板的 .cc 文件裏。

1.2. #define 保護

Tip

全部頭文件都應該使用 #define 來防止頭文件被多重包含, 命名格式當是: <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_

1.3. 前置聲明

Tip

儘量地避免使用前置聲明。使用 #include 包含須要的頭文件便可。

定義:

所謂「前置聲明」(forward declaration)是類、函數和模板的純粹聲明,沒伴隨着其定義.

在兩個類互相包含時會用到前置聲明:

//a.h
class B; //前置聲明
class A
{
...
private:
  B* b;
}
//b.h
#include "a.h"
class B
{
...
private:
  A a;
}

優勢:**

  • 前置聲明可以節省編譯時間,多餘的 #include 會迫使編譯器展開更多的文件,處理更多的輸入。
  • 前置聲明可以節省沒必要要的從新編譯的時間。 #include 使代碼由於頭文件中無關的改動而被從新編譯屢次。

缺點:

  • 前置聲明隱藏了依賴關係,頭文件改動時,用戶的代碼會跳過必要的從新編譯過程。

  • 前置聲明可能會被庫的後續更改所破壞。前置聲明函數或模板有時會妨礙頭文件開發者變更其 API. 例如擴大形參類型,加個自帶默認參數的模板形參等等。

  • 前置聲明來自命名空間 std:: 的符號 (symbols) 時,其行爲未定義。

  • 很難判斷何時該用前置聲明,何時該用 #include 。極端狀況下,用前置聲明代替 includes 甚至都會暗暗地改變代碼的含義:

    // b.h:
    struct B {};
    struct D : B {};
// good_user.cc:
#include "b.h"
void f(B*);
void f(void*);
void test(D* x) { f(x); }  // calls f(B*)

若是 #includeBD 的前置聲明替代, test() 就會調用 f(void*) .

  • 前置聲明瞭很多來自頭文件的 symbol 時,就會比單單一行的 include 冗長。
  • 僅僅爲了能前置聲明而重構代碼(好比用指針成員代替對象成員)會使代碼變得更慢更復雜.

結論:

  • 儘可能避免前置聲明那些定義在其餘項目中的實體.
  • 函數:老是使用 #include.
  • 類模板:優先使用 #include.

至於何時包含頭文件,參見 1.5. #include 的路徑及順序

1.4. 內聯函數

Tip

只有當函數只有 10 行甚至更少時纔將其定義爲內聯函數.

定義:

當函數被聲明爲內聯函數以後, 編譯器會將其內聯展開, 而不是按一般的函數調用機制進行調用.

//內聯函數
    inline int Max (int a, int b)
    {
        if(a >b)
            return a;
        return b;
    }

優勢:

只要內聯的函數體較小, 內聯該函數能夠令目標代碼更加高效. 對於存取函數以及其它函數體比較短, 性能關鍵的函數, 鼓勵使用內聯.

缺點:

濫用內聯將致使程序變得更慢. 內聯可能使目標代碼量或增或減, 這取決於內聯函數的大小. 內聯很是短小的存取函數一般會減小代碼大小, 但內聯一個至關大的函數將戲劇性的增長代碼大小. 現代處理器因爲更好的利用了指令緩存, 小巧的代碼每每執行更快。

結論:

一個較爲合理的經驗準則是, 不要內聯超過 10 行的函數. 謹慎對待析構函數, 析構函數每每比其表面看起來要更長, 由於有隱含的成員和基類析構函數被調用!

另外一個實用的經驗準則: 內聯那些包含循環或 switch 語句的函數經常是得不償失 (除非在大多數狀況下, 這些循環或 switch 語句從不被執行).

有些函數即便聲明爲內聯的也不必定會被編譯器內聯, 這點很重要; 好比虛函數和遞歸函數就不會被正常內聯. 一般, 遞歸函數不該該聲明成內聯函數.(注: 遞歸調用堆棧的展開並不像循環那麼簡單, 好比遞歸層數在編譯時多是未知的, 大多數編譯器都不支持內聯遞歸函數). 虛函數內聯的主要緣由則是想把它的函數體放在類定義內, 爲了圖個方便, 抑或是看成文檔描述其行爲, 好比精短的存取函數.

1.5. #include 的路徑及順序

Tip

使用標準的頭文件包含順序可加強可讀性, 避免隱藏依賴: 相關頭文件, C 庫, C++ 庫, 其餘庫的 .h, 本項目內的 .h.

項目內頭文件應按照項目源代碼目錄樹結構排列, 避免使用 UNIX 特殊的快捷目錄 . (當前目錄) 或 .. (上級目錄). 例如, google-awesome-project/src/base/logging.h 應該按以下方式包含:

#include "base/logging.h"

又如, dir/foo.ccdir/foo_test.cc 的主要做用是實現或測試 dir2/foo2.h 的功能, foo.cc 中包含頭文件的次序以下:

  1. dir2/foo2.h (優先位置, 詳情以下)
  2. C 系統文件
  3. C++ 系統文件
  4. 其餘庫的 .h 文件
  5. 本項目內 .h 文件

這種優先的順序排序保證當 dir2/foo2.h 遺漏某些必要的庫時, dir/foo.ccdir/foo_test.cc 的構建會馬上停止。所以這一條規則保證維護這些文件的人們首先看到構建停止的消息而不是維護其餘包的人們。

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

dir/foo.ccdir2/foo2.h 一般位於同一目錄下 (如 base/basictypes_unittest.ccbase/basictypes.h), 但也能夠放在不一樣目錄下.

按字母順序分別對每種類型的頭文件進行二次排序是不錯的主意。注意較老的代碼可不符合這條規則,要在方便的時候改正它們。

您所依賴的符號 (symbols) 被哪些頭文件所定義,您就應該包含(include)哪些頭文件前置聲明 (forward declarations) 狀況除外。好比您要用到 bar.h 中的某個符號, 哪怕您所包含的 foo.h 已經包含了 bar.h, 也照樣得包含 bar.h, 除非 foo.h 有明確說明它會自動向您提供 bar.h 中的符號 (symbols) . 不過,凡是 cc 文件所對應的「相關頭文件」已經包含的,就不用再重複包含進其 cc 文件裏面了,就像 foo.cc 只包含 foo.h 就夠了,不用再管後者所包含的其它內容。

舉例來講, fooserver.cc 用到 bar.h 中的某個符號,foo.h 已經包含了 bar.h,fooserver.cc 的包含次序以下:

#include "fooserver.h"

#include <sys/types.h>
#include <unistd.h>

#include <hash_map>
#include <vector>

#include "bar.h"
#include "foo.h"

fooserver.cc 用到 bar.h 中的某個符號,fooserver.h 已經包含了 bar.h,fooserver.cc 的包含次序以下:

#include "fooserver.h" 

#include <sys/types.h>
#include <unistd.h>

#include <hash_map>
#include <vector>

#include "foo.h"

例外:

有時,平臺特定(system-specific)代碼須要條件編譯(conditional includes),這些代碼能夠放到其它 includes 以後。固然,您的平臺特定代碼也要夠簡練且獨立,好比:

#include "foo/public/fooserver.h"

#include "base/port.h"  // For LANG_CXX11.

#ifdef LANG_CXX11
#include <initializer_list>
#endif  // LANG_CXX11

2. 做用域

2.1. 命名空間

Tip

鼓勵在 .cc 文件內使用匿名命名空間或 static 聲明. 使用具名的命名空間時, 其名稱可基於項目名或相對路徑. 禁止使用 using 指示(using-directive)。禁止使用內聯命名空間(inline namespace)。

定義:

命名空間將全局做用域細分爲獨立的, 具名的做用域, 可有效防止全局做用域的命名衝突.

優勢:

雖然類已經提供了(可嵌套的)命名軸線 (注: 將命名分割在不一樣類的做用域內), 命名空間在這基礎上又封裝了一層.

舉例來講, 兩個不一樣項目的全局做用域都有一個類 Foo, 這樣在編譯或運行時形成衝突. 若是每一個項目將代碼置於不一樣命名空間中, project1::Fooproject2::Foo 做爲不一樣符號天然不會衝突.

內聯命名空間會自動把內部的標識符放到外層做用域,好比:

namespace X {
inline namespace Y {
void foo();
}  // namespace Y
}  // namespace X

X::Y::foo()X::foo() 彼此可代替。內聯命名空間主要用來保持跨版本的 ABI 兼容性。

缺點:

命名空間具備迷惑性, 由於它們使得區分兩個相同命名所指代的定義更加困難。

內聯命名空間很容易使人迷惑,畢竟其內部的成員再也不受其聲明所在命名空間的限制。內聯命名空間只在大型版本控制裏有用。

有時候不得很少次引用某個定義在許多嵌套命名空間裏的實體,使用完整的命名空間會致使代碼的冗長。

在頭文件中使用匿名空間致使違背 C++ 的惟必定義原則 (One Definition Rule (ODR)).

結論:

根據下文將要提到的策略合理使用命名空間.

  • 遵照 命名空間命名中的規則。

  • 像以前的幾個例子中同樣,在命名空間的最後註釋出命名空間的名字。

  • 用命名空間把文件包含, 以及類的前置聲明之外的整個源文件封裝起來, 以區別於其它命名空間:

    // .h 文件
    namespace mynamespace 
    {
    // 全部聲明都置於命名空間中
    // 注意不要使用縮進
    class CMyClass 
    
    {
    public:
    ...
    void Foo();
    };
    
    } // namespace mynamespace
// .cc 文件
namespace mynamespace 
{

// 函數定義都置於命名空間中
void CMyClass::Foo() 
{
...
}

} // namespace mynamespace

更復雜的 .cc 文件包含更多, 更復雜的細節, 好比 gflags 或 using 聲明。

#include "a.h"

DEFINE_FLAG(bool, someflag, false, "dummy flag");

namespace a 
{

...code for a...                // 左對齊

} // namespace a
  • 不要在命名空間 std 內聲明任何東西, 包括標準庫的類前置聲明. 在 std 命名空間聲明實體是未定義的行爲, 會致使如不可移植. 聲明標準庫下的實體, 須要包含對應的頭文件.

  • 不該該使用 using 指示 引入整個命名空間的標識符號。

    // 禁止 —— 污染命名空間
    using namespace foo;
  • 不要在頭文件中使用 命名空間別名 除非顯式標記內部命名空間使用。由於任何在頭文件中引入的命名空間都會成爲公開API的一部分。

    // 在 .cc 中使用別名縮短經常使用的命名空間
    namespace baz = ::foo::bar::baz;
// 在 .h 中使用別名縮短經常使用的命名空間
namespace librarian {
namespace impl {  // 僅限內部使用
namespace sidetable = ::pipeline_diagnostics::sidetable;
}  // namespace impl

inline void my_inline_function() {
// 限制在一個函數中的命名空間別名
namespace baz = ::foo::bar::baz;
...
}
}  // namespace librarian
  • 禁止用內聯命名空間

2.2. 匿名命名空間和靜態變量

Tip

.cc 文件中定義一個不須要被外部引用的變量時,能夠將它們放在匿名命名空間或聲明爲 static 。可是不要在 .h 文件中這麼作。

定義:

全部置於匿名命名空間的聲明都具備內部連接性,函數和變量能夠經由聲明爲 static 擁有內部連接性,這意味着你在這個文件中聲明的這些標識符都不能在另外一個文件中被訪問。即便兩個文件聲明瞭徹底同樣名字的標識符,它們所指向的實體其實是徹底不一樣的。

結論:

推薦、鼓勵在 .cc 中對於不須要在其餘地方引用的標識符使用內部連接性聲明,可是不要在 .h 中使用。

匿名命名空間的聲明和具名的格式相同,在最後註釋上 namespace :

namespace 
{
...
}  // namespace

2.3. 非成員函數、靜態成員函數和全局函數

Tip

使用靜態成員函數或命名空間內的非成員函數, 儘可能不要用裸的全局函數. 將一系列函數直接置於命名空間中,不要用類的靜態方法模擬出命名空間的效果,類的靜態方法應當和類的實例或靜態數據緊密相關.

優勢:

某些狀況下, 非成員函數和靜態成員函數是很是有用的, 將非成員函數放在命名空間內可避免污染全局做用域.

缺點:

將非成員函數和靜態成員函數做爲新類的成員或許更有意義, 當它們須要訪問外部資源或具備重要的依賴關係時更是如此.

結論:

有時, 把函數的定義同類的實例脫鉤是有益的, 甚至是必要的. 這樣的函數能夠被定義成靜態成員, 或是非成員函數. 非成員函數不該依賴於外部變量, 應儘可能置於某個命名空間內. 相比單純爲了封裝若干不共享任何靜態數據的靜態成員函數而建立類, 不如使用 2.1.命名空間 。舉例而言,對於頭文件 myproject/foo_bar.h , 應當使用

namespace myproject 
{
namespace foo_bar 
{
void Function1();
void Function2();
}  // namespace foo_bar
}  // namespace myproject

而非

namespace myproject 
{
class FooBar 
{
public:
  static void Function1();
  static void Function2();
};
}  // namespace myproject

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

若是你必須定義非成員函數, 又只是在 .cc 文件中使用它, 可以使用匿名 2.1.命名空間static 連接關鍵字 (如 static int Foo() {...}) 限定其做用域.

2.4. 局部變量

Tip

將函數變量儘量置於最小做用域內, 並在變量聲明時進行初始化.

C++ 容許在函數的任何位置聲明變量. 咱們提倡在儘量小的做用域中聲明變量, 離第一次使用越近越好. 這使得代碼瀏覽者更容易定位變量聲明的位置, 瞭解變量的類型和初始值. 特別是,應使用初始化的方式替代聲明再賦值, 好比:

int i;
i = f(); // 壞——初始化和聲明分離
int j = g(); // 好——初始化時聲明
vector<int> v;
v.push_back(1); // 用花括號初始化更好
v.push_back(2);
vector<int> v = {1, 2}; // 好——v 一開始就初始化

爲了程序可讀性if, whilefor 括號內語句變量應在外面聲明 (for循環迭代變量 i 除外):

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

//不推薦
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);
}

非原生變量要在循環外定義可不遵循最小做用域原則,除非設計上須要變量在循環中構造。

2.5. 靜態和全局變量

Tip

禁止定義靜態儲存週期非POD變量,禁止使用含有反作用的函數初始化POD全局變量,由於多編譯單元中的靜態變量執行時的構造和析構順序是未明確的,這將致使代碼的不可移植。

禁止使用類的 靜態儲存週期 變量:因爲構造和析構函數調用順序的不肯定性,它們會致使難以發現的 bug 。不過 constexp 變量除外,畢竟它們又不涉及動態初始化或析構。

靜態生存週期的對象,即包括了全局變量,靜態變量,靜態類成員變量和函數靜態變量,都必須是原生數據類型 (POD : Plain Old Data): 即 int, char 和 float, 以及 POD 類型的指針、數組和結構體。

靜態變量的構造函數、析構函數和初始化的順序在 C++ 中是隻有部分明確的,甚至隨着構建變化而變化,致使難以發現的 bug. 因此除了禁用類類型的全局變量,咱們也不容許用函數返回值來初始化 POD 變量,除非該函數(好比 getenv()getpid() )不涉及任何全局變量。函數做用域裏的靜態變量除外,畢竟它的初始化順序是有明肯定義的,並且只會在指令執行到它的聲明那裏纔會發生。

同理,全局和靜態變量在程序中斷時會被析構,不管所謂中斷是從 main() 返回仍是對 exit() 的調用。析構順序正好與構造函數調用的順序相反。但既然構造順序未定義,那麼析構順序固然也就不定了。好比,在程序結束時某靜態變量已經被析構了,但代碼還在跑------好比其它線程------並試圖訪問它且失敗;再好比,一個靜態 string 變量也許會在一個引用了前者的其它變量析構以前被析構掉。

改善以上析構問題的辦法之一是用 quick_exit() 來代替 exit() 並中斷程序。它們的不一樣之處是前者不會執行任何析構,也不會執行 atexit() 所綁定的任何 handlers. 若是您想在執行 quick_exit() 來中斷時執行某 handler(好比刷新 log),您能夠把它綁定到 _at_quick_exit(). 若是您想在 exit()quick_exit() 都用上該 handler, 都綁定上去。

綜上所述,咱們只容許 POD 類型的靜態變量,即徹底禁用 vector (使用 C 數組替代) 和 string (使用 const char [])。

若是您確實須要一個 class 類型的靜態或全局變量,能夠考慮在 main() 函數或 pthread_once() 內初始化一個指針且永不回收。注意只能用 raw 指針,別用智能指針,畢竟後者的析構函數涉及到上文指出的不定順序問題。

3. 類

類是 C++ 中代碼的基本單元. 顯然, 它們被普遍使用. 本節列舉了在寫一個類時的主要注意事項.

3.1. 構造函數的職責

總述

不要在構造函數中調用虛函數, 也不要在沒法報出錯誤時進行可能失敗的初始化.不要拋出異常。

定義

在構造函數中能夠進行各類初始化操做.

優勢

  • 無需考慮類是否被初始化.
  • 通過構造函數徹底初始化後的對象能夠爲 const 類型, 也能更方便地被標準容器或算法使用.

缺點

  • 若是在構造函數內調用了自身的虛函數, 這類調用是不會重定向到子類的虛函數實現. 即便當前沒有子類化實現, 未來還是隱患.
  • 在沒有使程序崩潰 (由於並非一個始終合適的方法) 或者使用異常 (由於已經被 禁用 了) 等方法的條件下, 構造函數很難上報錯誤
  • 若是執行失敗, 會獲得一個初始化失敗的對象, 這個對象有可能進入不正常的狀態, 必須使用 bool IsValid() 或相似這樣的機制才能檢查出來, 然而這是一個十分容易被疏忽的方法.
  • 構造函數的地址是沒法被取得的, 所以, 舉例來講, 由構造函數完成的工做是沒法以簡單的方式交給其餘線程的.

結論

構造函數不容許調用虛函數. 若是代碼容許, 直接終止程序是一個合適的處理錯誤的方式. 不然, 考慮用 Init() 方法或工廠函數.

構造函數不得調用虛函數, 或嘗試報告一個非致命錯誤. 若是對象須要進行有意義的 (non-trivial) 初始化, 考慮使用明確的 Init() 方法或使用工廠模式.

3.2. 隱式類型轉換

總述

不要定義隱式類型轉換. (對於轉換運算符和單參數構造函數,在C++11項目中使用 explicit 關鍵字).

定義

隱式類型轉換容許一個某種類型 (稱做 源類型) 的對象被用於須要另外一種類型 (稱做 目的類型) 的位置, 例如, 將一個 int 類型的參數傳遞給須要 double 類型的函數.

除了語言所定義的隱式類型轉換, 用戶還能夠經過在類定義中添加合適的成員定義本身須要的轉換. 在源類型中定義隱式類型轉換, 能夠經過目的類型名的類型轉換運算符實現 (例如 operator bool()). 在目的類型中定義隱式類型轉換, 則經過以源類型做爲其惟一參數 (或惟一無默認值的參數) 的構造函數實現.

explicit 關鍵字能夠用於構造函數或 (在 C++11 引入) 類型轉換運算符, 以保證只有當目的類型在調用點被顯式寫明時才能進行類型轉換, 例如使用 cast. 這不只做用於隱式類型轉換, 還能做用於 C++11 的列表初始化語法:

class CFoo {
  explicit CFoo(int x, double y);
  ...
};

void Func(CFoo f);

此時下面的代碼是不容許的:

Func({42, 3.14});  // Error

這一代碼從技術上說並不是隱式類型轉換, 可是語言標準認爲這是 explicit 應當限制的行爲.

優勢

  • 有時目的類型名是一目瞭然的, 經過避免顯式地寫出類型名, 隱式類型轉換可讓一個類型的可用性和表達性更強.
  • 隱式類型轉換能夠簡單地取代函數重載.
  • 在初始化對象時, 列表初始化語法是一種簡潔明瞭的寫法.

缺點

  • 隱式類型轉換會隱藏類型不匹配的錯誤. 有時, 目的類型並不符合用戶的指望, 甚至用戶根本沒有意識到發生了類型轉換.
  • 隱式類型轉換會讓代碼難以閱讀, 尤爲是在有函數重載的時候, 由於這時很難判斷究竟是哪一個函數被調用.
  • 單參數構造函數有可能會被無心地用做隱式類型轉換.
  • 若是單參數構造函數沒有加上 explicit 關鍵字, 讀者沒法判斷這一函數到底是要做爲隱式類型轉換, 仍是做者忘了加上 explicit 標記.
  • 並無明確的方法用來判斷哪一個類應該提供類型轉換, 這會使得代碼變得含糊不清.
  • 若是目的類型是隱式指定的, 那麼列表初始化會出現和隱式類型轉換同樣的問題, 尤爲是在列表中只有一個元素的時候.

結論

在類型定義中, 類型轉換運算符和單參數構造函數都應當用 explicit 進行標記. 一個例外是, 拷貝和移動構造函數不該當被標記爲 explicit, 由於它們並不執行類型轉換. 對於設計目的就是用於對其餘類型進行透明包裝的類來講, 隱式類型轉換有時是必要且合適的. 這時應當聯繫項目組長並說明特殊狀況.

不能以一個參數進行調用的構造函數不該當加上 explicit. 接受一個 std::initializer_list 做爲參數的構造函數也應當省略 explicit, 以便支持拷貝初始化 (例如 MyType m = {1, 2};) .

3.3. 可拷貝類型

總述

若是你的類型須要, 就讓它們支持拷貝. 不然, 就把隱式產生的拷貝構造函數和賦值運算符禁用.

定義

可拷貝類型容許對象在初始化時獲得來自相同類型的另外一對象的值, 或在賦值時被賦予相同類型的另外一對象的值, 同時不改變源對象的值. 對於用戶定義的類型, 拷貝操做通常經過拷貝構造函數與拷貝賦值操做符定義. string 類型就是一個可拷貝類型的例子.

拷貝構造函數在某些狀況下會被編譯器隱式調用. 例如, 經過傳值的方式傳遞對象.

優勢

可拷貝類型的對象能夠經過傳值的方式進行傳遞或者返回, 這使得 API 更簡單, 更安全也更通用. 與傳指針和引用不一樣, 這樣的傳遞不會形成全部權, 生命週期, 可變性等方面的混亂, 也就不必在協議中予以明確. 這同時也防止了客戶端與實如今非做用域內的交互, 使得它們更容易被理解與維護. 這樣的對象能夠和須要傳值操做的通用 API 一塊兒使用, 例如大多數容器.

拷貝構造函數與賦值操做通常來講要比它們的各類替代方案, 好比 Clone(), CopyFrom() or Swap(), 更容易定義, 由於它們能經過編譯器產生, 不管是隱式的仍是經過 = default. 這種方式很簡潔, 也保證全部數據成員都會被複制. 拷貝構造函數通常也更高效, 由於它們不須要堆的分配或者是單獨的初始化和賦值步驟, 同時, 對於相似 省略沒必要要的拷貝 這樣的優化它們也更加合適.

缺點

許多類型都不須要拷貝, 爲它們提供拷貝操做會讓人迷惑, 也顯得荒謬而不合理. 單件類型 (Registerer), 與特定的做用域相關的類型 (Cleanup), 與其餘對象實體緊耦合的類型 (Mutex) 從邏輯上來講都不該該提供拷貝操做. 爲基類提供拷貝 / 賦值操做是有害的, 由於在使用它們時會形成 對象切割 . 默認的或者隨意的拷貝操做實現多是不正確的, 這每每致使使人困惑而且難以診斷出的錯誤.

拷貝構造函數是隱式調用的, 也就是說, 這些調用很容易被忽略. 這會讓人迷惑, 尤爲是對那些所用的語言約定或強制要求傳引用的程序員來講更是如此. 同時, 這從必定程度上說會鼓勵過分拷貝, 從而致使性能上的問題.

結論

若是須要就讓你的類型可拷貝. 做爲一個經驗法則, 若是對於你的用戶來講這個拷貝操做不是一眼就能看出來的, 那就不要把類型設置爲可拷貝. 若是讓類型可拷貝, 必定要同時給出拷貝構造函數和賦值操做的定義, 反之亦然.

若是定義了拷貝操做, 則要保證這些操做的默認實現是正確的. 記得時刻檢查默認操做的正確性, 而且在文檔中說明類是可拷貝的.

class CFoo 
{
public:
  CFoo(const CFoo& other) : field_(other.field) {}
  // 差, 只定義了拷貝構造函數, 而沒有定義對應的賦值運算符.

private:
  Field field_;
};

因爲存在對象切割的風險, 不要爲任何有可能有派生類的對象提供賦值操做或者拷貝構造函數 (固然也不要繼承有這樣的成員函數的類). 若是你的基類須要可複製屬性, 請提供一個 public virtual Clone() 和一個 protected 的拷貝構造函數以供派生類實現(設計模式中的原型模式).

class BCase 
{
public:
  public virtual CBase* Clone() {return new CBase(*this);}

protected:
  CBase(const CBase& other);
};
class CFoo : public CBase 
{
public:
  public virtual CFoo* Clone() {return new CFoo(*this);}
  //注意 Clone() 返回值與基類不一樣
protected:
  CFoo(const CFoo& other) {...};
};

若是你的類不須要拷貝操做, 請顯式地經過在 private 域中定義拷貝構造函數和賦值運算符並不予實現.

class CMyClass
{
private:
  // CMyClass is neither copyable nor movable.
  CMyClass(const CMyClass&);
  CMyClass& operator=(const CMyClass&);
}

3.4. 結構體 VS. 類

總述

僅當只有數據成員時使用 struct, 其它一律使用 class.

說明

在 C++ 中 structclass 關鍵字幾乎含義同樣. 咱們爲這兩個關鍵字添加咱們本身的語義理解, 以便爲定義的數據類型選擇合適的關鍵字.

struct 用來定義包含數據的被動式對象, 也能夠包含相關的常量, 但除了存取數據成員以外, 沒有別的函數功能. 而且存取功能是經過直接訪問位域, 而非函數調用. 除了構造函數, 析構函數, Initialize(), Reset(), Validate() 等相似的用於設定數據成員的函數外, 不能提供其它功能的函數.

若是須要更多的函數功能, class 更適合. 若是拿不許, 就用 class.

爲了和 STL 保持一致, 對於仿函數等特性能夠不用 class 而是使用 struct.

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

3.5. 繼承

總述

使用組合經常比使用繼承更合理. 若是使用繼承的話, 定義爲 public 繼承.

定義

當子類繼承基類時, 子類包含了父基類全部數據及操做的定義. C++ 實踐中, 繼承主要用於兩種場合: 實現繼承, 子類繼承父類的實現代碼; 接口繼承, 子類僅繼承父類的方法名稱.

優勢

實現繼承經過原封不動的複用基類代碼減小了代碼量. 因爲繼承是在編譯時聲明, 程序員和編譯器均可以理解相應操做並發現錯誤. 從編程角度而言, 接口繼承是用來強制類輸出特定的 API. 在類沒有實現 API 中某個必須的方法時, 編譯器一樣會發現並報告錯誤.

缺點

對於實現繼承, 因爲子類的實現代碼散佈在父類和子類間之間, 要理解其實現變得更加困難. 子類不能重寫父類的非虛函數, 固然也就不能修改其實現. 基類也可能定義了一些數據成員, 所以還必須區分基類的實際佈局.

結論

全部繼承必須是 public 的. 若是你想使用私有繼承, 你應該替換成把基類的實例做爲成員對象的方式.

不要過分使用實現繼承. 組合經常更合適一些. 儘可能作到只在 "是一個" ("is-a", 注: 其餘 "has-a" 狀況下請使用組合) 的狀況下使用繼承: 若是 Bar 的確 "是一種" Foo, Bar 才能繼承 Foo.

必要的話, 析構函數聲明爲 virtual. 若是你的類有虛函數, 則析構函數也應該爲虛函數.

對於可能被子類訪問的成員函數, 不要過分使用 protected 關鍵字. 注意, 數據成員都必須是 私有的.

對於重載的虛函數或虛析構函數, 使用 override, 或 (較不經常使用的) final 關鍵字顯式地進行標記. 較早 (早於 C++11) 的代碼可能會使用 virtual 關鍵字做爲不得已的選項. 所以, 在聲明重載時, 請使用 override, finalvirtual 的其中之一進行標記. 標記爲 overridefinal 的析構函數若是不是對基類虛函數的重載的話, 編譯會報錯, 這有助於捕獲常見的錯誤. 這些標記起到了文檔的做用, 由於若是省略這些關鍵字, 代碼閱讀者不得不檢查全部父類, 以判斷該函數是不是虛函數.

例子

(1)若在邏輯上B 是A 的「一種」(a kind of ),則容許B 繼承A 的功能。如男人(Man)是人(Human)的一種,男孩(Boy)是男人的一種。那麼類Man 能夠從類Human 派生,類Boy 能夠從類Man 派生。示例程序以下:

class CHuman
{
  …
};
class CMan : public CHuman
{
  …
};
class CBoy : public CMan
{
  …
};

(2)若在邏輯上A 是B 的「一部分」(a part of),則不容許B 繼承A 的功能,而是要用A和其它東西組合出B。例如眼(Eye)、鼻(Nose)、口(Mouth)、耳(Ear)是頭(Head)的一部分,因此類Head 應該由類Eye、Nose、Mouth、Ear 組合而成,不是派生而成。示例程序以下:

class CEye
{
public:
  void Look(void);
};
class CNose
{
public:
  void Smell(void);
};
class CMouth
{
public:
  void Eat(void);
};
class CEar
{
public:
  void Listen(void);
};
// 正確的設計
class CHead
{
public:
  void Look(void) { m_eye.Look(); }
  void Smell(void) { m_nose.Smell(); }
  void Eat(void) { m_mouth.Eat(); }
  void Listen(void) { m_ear.Listen(); }
private:
  CEye m_eye;
  CNose m_nose;
  CMouth m_mouth;
Ear m_ear;
};
// 錯誤的設計
class CHead : public CEye, public CNose, public CMouth, public CEar
{
};

3.6. 多重繼承

總述

真正須要用到多重實現繼承的狀況少之又少. 只在如下狀況咱們才容許多重繼承: 最多隻有一個基類是非抽象類; 其它基類都是 純接口類.

定義

多重繼承容許子類擁有多個基類. 要將做爲 純接口 的基類和具備 實現 的基類區別開來.

優勢

相比單繼承 (見 繼承), 多重實現繼承能夠複用更多的代碼.

缺點

真正須要用到多重 實現 繼承的狀況少之又少. 有時多重實現繼承看上去是不錯的解決方案, 但這時你一般也能夠找到一個更明確, 更清晰的不一樣解決方案.

結論

只有當全部父類除第一個外都是 純接口類 時, 才容許使用多重繼承.

注意

關於該規則, Windows 下有個 特例.

3.7. 接口

總述

接口是指知足特定條件的類.

定義

當一個類知足如下要求時, 稱之爲純接口:

  • 只有純虛函數 ("=0") 和靜態函數 (除了下文提到的析構函數).
  • 沒有非靜態數據成員.
  • 沒有定義任何構造函數. 若是有, 也不能帶有參數, 而且必須爲 protected.
  • 若是它是一個子類, 也只能從知足上述條件的類繼承.
class MCDInterface
{
public:
  inline virtual ~MCDInterface(){};
  virtual void connect()=0;
  virtual void disconnect()=0;
protected:
  MCDInterface(const MCDInterface&);
  MCDInterface& operator=(const MCDInterface&);
}

接口類不能被直接實例化, 由於它聲明瞭純虛函數. 爲確保接口類的全部實現可被正確銷燬, 必須爲之聲明虛析構函數 (做爲上述第 1 條規則的特例, 析構函數不能是純虛函數). 具體細節可參考 Stroustrup 的 The C++ Programming Language, 3rd edition 第 12.4 節.

3.8. 運算符重載

總述

除少數特定環境外, 不要重載運算符. 也不要建立用戶定義字面量.

定義

C++ 容許用戶經過使用 operator 關鍵字 對內建運算符進行重載定義 , 只要其中一個參數是用戶定義的類型. operator 關鍵字還容許用戶使用 operator"" 定義新的字面運算符, 而且定義類型轉換函數, 例如 operator bool().

優勢

重載運算符可讓代碼更簡潔易懂, 也使得用戶定義的類型和內建類型擁有類似的行爲. 重載運算符對於某些運算來講是符合符合語言習慣的名稱 (例如 ==, <, =, <<), 遵循這些語言約定可讓用戶定義的類型更易讀, 也能更好地和須要這些重載運算符的函數庫進行交互操做.

對於建立用戶定義的類型的對象來講, 用戶定義字面量是一種很是簡潔的標記.

缺點

  • 要提供正確, 一致, 不出現異常行爲的操做符運算須要花費很多精力, 並且若是達不到這些要求的話, 會致使使人迷惑的 Bug.
  • 過分使用運算符會帶來難以理解的代碼, 尤爲是在重載的操做符的語義與一般的約定不符合時.
  • 函數重載有多少弊端, 運算符重載就至少有多少.
  • 運算符重載會混淆視聽, 讓你誤覺得一些耗時的操做和操做內建類型同樣輕巧.
  • 對重載運算符的調用點的查找須要的可就不只僅是像 grep 那樣的程序了, 這時須要可以理解 C++ 語法的搜索工具.
  • 若是重載運算符的參數寫錯, 此時獲得的多是一個徹底不一樣的重載而非編譯錯誤. 例如: foo < bar 執行的是一個行爲, 而 &foo < &bar 執行的就是徹底不一樣的另外一個行爲了.
  • 重載某些運算符自己就是有害的. 例如, 重載一元運算符 & 會致使一樣的代碼有徹底不一樣的含義, 這取決於重載的聲明對某段代碼而言是不是可見的. 重載諸如 &&, ||, 會致使運算順序和內建運算的順序不一致.
  • 運算符從一般定義在類的外部, 因此對於同一運算, 可能出現不一樣的文件引入了不一樣的定義的風險. 若是兩種定義都連接到同一二進制文件, 就會致使未定義的行爲, 有可能表現爲難以發現的運行時錯誤.
  • 用戶定義字面量所建立的語義形式對於某些有經驗的 C++ 程序員來講都是很陌生的.

結論

只有在乎義明顯, 不會出現奇怪的行爲而且與對應的內建運算符的行爲一致時才定義重載運算符. 例如, | 要做爲位或或邏輯或來使用, 而不是做爲 shell 中的管道.

只有對用戶本身定義的類型重載運算符. 更準確地說, 將它們和它們所操做的類型定義在同一個頭文件中, .cc 中和命名空間中. 這樣作不管類型在哪裏都可以使用定義的運算符, 而且最大程度上避免了多重定義的風險. 若是可能的話, 請避免將運算符定義爲模板, 由於此時它們必須對任何模板參數都可以做用. 若是你定義了一個運算符, 請將其相關且有意義的運算符都進行定義, 而且保證這些定義的語義是一致的. 例如, 若是你重載了 <, 那麼請將全部的比較運算符都進行重載, 而且保證對於同一組參數, <> 不會同時返回 true.

建議不要將不進行修改的二元運算符定義爲成員函數. 若是一個二元運算符被定義爲類成員, 這時隱式轉換會做用域右側的參數卻不會做用於左側. 這時會出現 a < b 可以經過編譯而 b < a 不能的狀況, 這是很讓人迷惑的.

不要爲了不重載操做符而走極端. 好比說, 應當定義 ==, =, 和 << 而不是 Equals(), CopyFrom()PrintTo(). 反過來講, 不要只是爲了知足函數庫須要而去定義運算符重載. 好比說, 若是你的類型沒有天然順序, 而你要將它們存入 std::set 中, 最好仍是定義一個自定義的比較運算符而不是重載 <.

不要重載 &&, ||, , 或一元運算符 &. 不要重載 operator"", 也就是說, 不要引入用戶定義字面量.

類型轉換運算符在 隱式類型轉換 一節有說起. = 運算符在 可拷貝類型 一節有說起. 運算符 << 一節有說起. 同時請參見 函數重載 一節, 其中提到的的規則對運算符重載一樣適用.

3.9. 存取控制

總述

全部 數據成員聲明爲非 public, 除非是 static const 類型成員 (遵循 常量命名規則). 處於技術上的緣由, 在使用 Google Test 時咱們容許測試固件類中的數據成員爲 protected.

3.10. 聲明順序

總述

將類似的聲明放在一塊兒, 將 public 部分放在最前.

說明

類定義通常應以 public: 開始, 後跟 protected:, 最後是 private:. 省略空部分.

在各個部分中, 建議將相似的聲明放在一塊兒, 而且建議以以下的順序: 類型 (包括 typedef, using 和嵌套的結構體與類), 常量, 工廠函數, 構造函數, 賦值運算符, 析構函數, 其它函數, 數據成員.

不要將大段的函數定義內聯在類定義中. 一般,只有那些普通的, 或性能關鍵且短小的函數能夠內聯在類定義中. 參見 內聯函數 一節.

4. 函數

4.1. 參數順序

總述

函數的參數順序爲: 輸入參數在先, 後跟輸出參數.

說明

C/C++ 中的函數參數或者是函數的輸入, 或者是函數的輸出, 或兼而有之. 輸入參數一般是值參或 const 引用, 輸出參數或輸入/輸出參數則通常爲非 const 指針. 在排列參數順序時, 將全部的輸入參數置於輸出參數以前. 特別要注意, 在加入新參數時不要由於它們是新參數就置於參數列表最後, 而是仍然要按照前述的規則, 即將新的輸入參數也置於輸出參數以前.

這並不是一個硬性規定. 輸入/輸出參數 (一般是類或結構體) 讓這個問題變得複雜. 而且, 有時候爲了其餘函數保持一致, 你可能不得不有所變通.

4.2. 編寫簡短函數

總述

咱們傾向於編寫簡短, 凝練的函數.

說明

咱們認可長函數有時是合理的, 所以並不硬性限制函數的長度. 若是函數超過 40 行, 能夠思索一下能不能在不影響程序結構的前提下對其進行分割.

即便一個長函數如今工做的很是好, 一旦有人對其修改, 有可能出現新的問題, 甚至致使難以發現的 bug. 使函數儘可能簡短, 以便於他人閱讀和修改代碼.

在處理代碼時, 你可能會發現複雜的長函數. 不要懼怕修改現有代碼: 若是證明這些代碼使用 / 調試起來很困難, 或者你只須要使用其中的一小段代碼, 考慮將其分割爲更加簡短並易於管理的若干函數.

4.3. 引用參數

總述

全部按引用傳遞的參數必須加上 const.

定義

在 C 語言中, 若是函數須要修改變量的值, 參數必須爲指針, 如 int foo(int *pval). 在 C++ 中, 函數還能夠聲明爲引用參數: int foo(int &val).

優勢

定義引用參數能夠防止出現 (*pval)++ 這樣醜陋的代碼. 引用參數對於拷貝構造函數這樣的應用也是必需的. 同時也更明確地不接受空指針.

缺點

容易引發誤解, 由於引用在語法上是值變量卻擁有指針的語義.

結論

函數參數列表中, 全部引用參數都必須是 const,輸出參數不能使用引用.:

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

事實上這在 Google Code 是一個硬性約定: 輸入參數是值參或 const 引用, 輸出參數爲指針. 輸入參數能夠是 const 指針, 但決不能是非 const 的引用參數, 除非特殊要求, 好比 swap().

有時候, 在輸入形參中用 const T* 指針比 const T& 更明智. 好比:

  • 可能會傳遞空指針.
  • 函數要把指針或對地址的引用賦值給輸入形參.

總而言之, 大多時候輸入形參每每是 const T&. 若用 const T* 則說明輸入另有處理. 因此若要使用 const T*, 則應給出相應的理由, 不然會使得讀者感到迷惑.

參數若是是指針使用前必定要對指針有效性進行判斷。

例子

4.4. 函數重載

總述

若要使用函數重載, 則必須能讓讀者一看調用點就成竹在胸, 而不用花心思猜想調用的重載函數究竟是哪種. 這一規則也適用於構造函數.

定義

你能夠編寫一個參數類型爲 const string& 的函數, 而後用另外一個參數類型爲 const char* 的函數對其進行重載:

class CMyClass 
{
    public:
    void Analyze(const string &text);
    void Analyze(const char *text, size_t textlen);
};

優勢

經過重載參數不一樣的同名函數, 能夠令代碼更加直觀. 模板化代碼須要重載, 這同時也能爲使用者帶來便利.

缺點

若是函數單靠不一樣的參數類型而重載 (acgtyrant 注:這意味着參數數量不變), 讀者就得十分熟悉 C++ 五花八門的匹配規則, 以瞭解匹配過程具體到底如何. 另外, 若是派生類只重載了某個函數的部分變體, 繼承語義就容易使人困惑.

結論

若是打算重載一個函數, 能夠試試改在函數名里加上參數信息. 例如, 用 AppendString()AppendInt() 等, 而不是一口氣重載多個 Append(). 若是重載函數的目的是爲了支持不一樣數量的同一類型參數, 則優先考慮使用 std::vector 以便使用者能夠用 列表初始化 指定參數.

4.5. 缺省參數

總述

只容許在非虛函數中使用缺省參數, 且必須保證缺省參數的值始終一致. 缺省參數與 函數重載 遵循一樣的規則. 通常狀況下建議使用函數重載, 尤爲是在缺省參數帶來的可讀性提高不能彌補下文中所提到的缺點的狀況下.

優勢

有些函數通常狀況下使用默認參數, 但有時須要又使用非默認的參數. 缺省參數爲這樣的情形提供了便利, 使程序員不須要爲了極少的例外狀況編寫大量的函數. 和函數重載相比, 缺省參數的語法更簡潔明瞭, 減小了大量的樣板代碼, 也更好地區別了 "必要參數" 和 "可選參數".

缺點

缺省參數其實是函數重載語義的另外一種實現方式, 所以全部 不該當使用函數重載的理由 也都適用於缺省參數.

虛函數調用的缺省參數取決於目標對象的靜態類型, 此時沒法保證給定函數的全部重載聲明的都是一樣的缺省參數.

缺省參數是在每一個調用點都要進行從新求值的, 這會形成生成的代碼迅速膨脹. 做爲讀者, 通常來講也更但願缺省的參數在聲明時就已經被固定了, 而不是在每次調用時均可能會有不一樣的取值.

缺省參數會干擾函數指針, 致使函數簽名與調用點的簽名不一致. 而函數重載不會致使這樣的問題.

結論

對於虛函數, 不容許使用缺省參數, 由於在虛函數中缺省參數不必定能正常工做. 若是在每一個調用點缺省參數的值都有可能不一樣, 在這種狀況下缺省函數也不容許使用. (例如, 不要寫像 void f(int n = counter++); 這樣的代碼.)

在其餘狀況下, 若是缺省參數對可讀性的提高遠遠超過了以上說起的缺點的話, 能夠使用缺省參數. 若是仍有疑惑, 就使用函數重載.

4.6. 函數指針類型參數檢查

總述

參數若是是指針使用前必定要對指針有效性進行檢查.

例如:

int foo(int *x)
{
    if (nullptr == x)
    {
        ...
        return -1;
    }
    ...
}

5. 其餘 C++ 特性

5.1. 友元

Tip

咱們容許合理的使用友元類及友元函數.

一般友元應該定義在同一文件內, 避免代碼讀者跑到其它文件查找使用該私有成員的類. 常常用到友元的一個地方是將 FooBuilder 聲明爲 Foo 的友元, 以便 FooBuilder 正確構造 Foo 的內部狀態, 而無需將該狀態暴露出來. 某些狀況下, 將一個單元測試類聲明成待測類的友元會很方便.

友元擴大了 (但沒有打破) 類的封裝邊界. 某些狀況下, 相對於將類成員聲明爲 public, 使用友元是更好的選擇, 尤爲是若是你只容許另外一個類訪問該類的私有成員時. 固然, 大多數類都只應該經過其提供的公有成員進行互操做.

5.2. 異常

Tip

咱們不使用 C++ 異常.但第三方庫使用到異常仍是須要抓取異常.

優勢:

  • 異常容許應用高層決定如何處理在底層嵌套函數中「不可能發生」的失敗(failures),不用管那些含糊且容易出錯的錯誤代碼(注:error code, C語言函數返回的非零 int 值)。
  • 不少現代語言都用異常。引入異常使得 C++ 與 Python, Java 以及其它類 C++ 的語言更一脈相承。
  • 有些第三方 C++ 庫依賴異常,禁用異常就很差用了。
  • 異常是處理構造函數失敗的惟一途徑。雖然能夠用工廠函數(注:factory function, 出自 C++ 的一種設計模式,即「簡單工廠模式」)或 Init() 方法代替異常, 可是前者要求在堆棧分配內存,後者會致使剛建立的實例處於 "無效" 狀態。
  • 在測試框架裏很好用。

缺點:

  • 在現有函數中添加 throw 語句時,您必須檢查全部調用點。要麼讓全部調用點通通具有最低限度的異常安全保證,要麼眼睜睜地看異常一路歡快地往上跑,最終中斷掉整個程序。舉例,f() 調用 g(), g() 又調用 h(), 且 h 拋出的異常被 f 捕獲。小心 g, 不然會沒妥善清理好。
  • 還有更常見的,異常會完全擾亂程序的執行流程並難以判斷,函數也許會在您意料不到的地方返回。您或許會加一大堆什麼時候何到處理異常的規定來下降風險,然而開發者的記憶負擔更重了。
  • 異常安全須要RAII和不一樣的編碼實踐. 要輕鬆編寫出正確的異常安全代碼須要大量的支持機制. 更進一步地說, 爲了不讀者理解整個調用表, 異常安全必須隔絕從持續狀態寫到 "提交" 狀態的邏輯. 這一點有利有弊 (由於你也許不得不爲了隔離提交而混淆代碼). 若是容許使用異常, 咱們就不得不時刻關注這樣的弊端, 即便有時它們並不值得.
  • 啓用異常會增長二進制文件數據,延長編譯時間(或許影響小),還可能加大地址空間的壓力。
  • 濫用異常會變相鼓勵開發者去捕捉不合時宜,或原本就已經無法恢復的「僞異常」。好比,用戶的輸入不符合格式要求時,也用不着拋異常。如此之類的僞異常列都列不完。

結論:

咱們本身的代碼中不要拋出異常,可是須要抓取第三方庫的異常。捕獲的異常須要處理,錯誤異常要輸出日誌。

對於 Windows 代碼來講, 有個 特例.

5.3. 運行時類型識別

Tip

除了項目中第三方庫須要,咱們禁止使用 RTTI.

定義:

RTTI 容許程序員在運行時識別 C++ 類對象的類型. 它經過使用 typeid 或者 dynamic_cast 完成.

優勢:

RTTI 的標準替代 (下面將描述) 須要對有問題的類層級進行修改或重構. 有時這樣的修改並非咱們所想要的, 甚至是不可取的, 尤爲是在一個已經普遍使用的或者成熟的代碼中.

RTTI 在某些單元測試中很是有用. 好比進行工廠類測試時, 用來驗證一個新建對象是否爲指望的動態類型. RTTI 對於管理對象和派生對象的關係也頗有用.

在考慮多個抽象對象時 RTTI 也很好用. 例如:

bool Base::Equal(Base* other) = 0;
bool Derived::Equal(Base* other) {
Derived* that = dynamic_cast<Derived*>(other);
if (that == nullptr)
 return false;
...
}

缺點:

在運行時判斷類型一般意味着設計問題. 若是你須要在運行期間肯定一個對象的類型, 這一般說明你須要考慮從新設計你的類.

隨意地使用 RTTI 會使你的代碼難以維護. 它使得基於類型的判斷樹或者 switch 語句散佈在代碼各處. 若是之後要進行修改, 你就必須檢查它們.

結論:

RTTI 有合理的用途可是容易被濫用, 所以在使用時請務必注意. 在單元測試中能夠使用 RTTI, 可是在其餘代碼中請儘可能避免. 尤爲是在新代碼中, 使用 RTTI 前務必三思. 若是你的代碼須要根據不一樣的對象類型執行不一樣的行爲的話, 請考慮用如下的兩種替代方案之一查詢類型:

虛函數能夠根據子類類型的不一樣而執行不一樣代碼. 這是把工做交給了對象自己去處理.

若是這一工做須要在對象以外完成, 能夠考慮使用雙重分發的方案, 例如使用訪問者設計模式. 這就可以在對象以外進行類型判斷.

若是程序可以保證給定的基類實例實際上都是某個派生類的實例, 那麼就能夠自由使用 dynamic_cast. 在這種狀況下, 使用 dynamic_cast 也是一種替代方案.

基於類型的判斷樹是一個很強的暗示, 它說明你的代碼已經偏離正軌了. 不要像下面這樣:

if (typeid(*data) == typeid(D1)) {
...
} else if (typeid(*data) == typeid(D2)) {
...
} else if (typeid(*data) == typeid(D3)) {
...

一旦在類層級中加入新的子類, 像這樣的代碼每每會崩潰. 並且, 一旦某個子類的屬性改變了, 你很難找到並修改全部受影響的代碼塊.

不要去手工實現一個相似 RTTI 的方案. 反對 RTTI 的理由一樣適用於這些方案, 好比帶類型標籤的類繼承體系. 並且, 這些方案會掩蓋你的真實意圖.

5.4. 流

Tip

只在記錄日誌時使用流.

定義:

流用來替代 printf()scanf().

優勢:

有了流, 在打印時不須要關心對象的類型. 不用擔憂格式化字符串與參數列表不匹配 (雖然在 gcc 中使用 printf 也不存在這個問題). 流的構造和析構函數會自動打開和關閉對應的文件.

缺點:

流使得 pread() 等功能函數很難執行. 若是不使用 printf 風格的格式化字符串, 某些格式化操做 (尤爲是經常使用的格式字符串 %.*s) 用流處理性能是很低的. 流不支持字符串操做符從新排序 (%1s), 而這一點對於軟件國際化頗有用.

結論:

不要使用流, 除非是日誌接口須要. 使用 printf 之類的代替.

使用流還有不少利弊, 但代碼一致性賽過一切. 不要在代碼中使用流.

拓展討論:

對這一條規則存在一些爭論, 這兒給出點深層次緣由. 回想一下惟一性原則 (Only One Way): 咱們但願在任什麼時候候都只使用一種肯定的 I/O 類型, 使代碼在全部 I/O 處都保持一致. 所以, 咱們不但願用戶來決定是使用流仍是 printf + read/write. 相反, 咱們應該決定到底用哪種方式. 把日誌做爲特例是由於日誌是一個很是獨特的應用, 還有一些是歷史緣由.

流的支持者們主張流是不二之選, 但觀點並非那麼清晰有力. 他們指出的流的每一個優點也都是其劣勢. 流最大的優點是在輸出時不須要關心打印對象的類型. 這是一個亮點. 同時, 也是一個不足: 你很容易用錯類型, 而編譯器不會報警. 使用流時容易形成的這類錯誤:

cout << this;   // 輸出地址
cout << *this;  // 輸出值

因爲 << 被重載, 編譯器不會報錯. 就由於這一點咱們反對使用操做符重載.

有人說 printf 的格式化醜陋不堪, 易讀性差, 但流也好不到哪兒去. 看看下面兩段代碼吧, 實現相同的功能, 哪一個更清晰?

cerr << "Error connecting to '" << foo->bar()->hostname.first
  << ":" << foo->bar()->hostname.second << ": " << strerror(errno);

fprintf(stderr, "Error connecting to '%s:%u: %s",
     foo->bar()->hostname.first, foo->bar()->hostname.second,
     strerror(errno));

你可能會說, "把流封裝一下就會比較好了", 這兒能夠, 其餘地方呢? 並且不要忘了, 咱們的目標是使語言更緊湊, 而不是添加一些別人須要學習的新裝備.

每一種方式都是各有利弊, "沒有最好, 只有更適合". 簡單性原則告誡咱們必須從中選擇其一, 最後大多數決定採用 printf + read/write.

5.5. 前置自增和自減

Tip

使用前綴形式 (++i) 的自增, 自減運算符.

定義:

對於變量在自增 (++ii++) 或自減 (--ii--) 後表達式的值又沒有沒用到的狀況下, 須要肯定究竟是使用前置仍是後置的自增 (自減).

優勢:

不考慮返回值的話, 前置自增 (++i) 一般要比後置自增 (i++) 效率更高. 由於後置自增 (或自減) 須要對錶達式的值 i 進行一次拷貝. 若是 i 是迭代器或其餘非數值類型, 拷貝的代價是比較大的. 既然兩種自增方式實現的功能同樣, 爲何不老是使用前置自增呢?

缺點:

在 C 開發中, 當表達式的值未被使用時, 傳統的作法是使用後置自增, 特別是在 for 循環中. 有些人以爲後置自增更加易懂, 由於這很像天然語言, 主語 (i) 在謂語動詞 (++) 前.

結論:

統一使用前置自增 (自減).

5.6. const 用法

Tip

咱們強烈建議你在任何可能的狀況下都要使用 const.

定義:

在聲明的變量或參數前加上關鍵字 const 用於指明變量值不可被篡改 (如 const int foo ). 爲類中的函數加上 const 限定符代表該函數不會修改類成員變量的狀態 (如 class Foo { int Bar(char c) const; };).

優勢:

你們更容易理解如何使用變量. 編譯器能夠更好地進行類型檢測, 相應地, 也能生成更好的代碼. 人們對編寫正確的代碼更加自信, 由於他們知道所調用的函數被限定了能或不能修改變量值. 即便是在無鎖的多線程編程中, 人們也知道什麼樣的函數是安全的.

缺點:

const 是入侵性的: 若是你向一個函數傳入 const 變量, 函數原型聲明中也必須對應 const 參數 (不然變量須要 const_cast 類型轉換), 在調用庫函數時顯得尤爲麻煩.

結論:

const 變量, 數據成員, 函數和參數爲編譯時類型檢測增長了一層保障; 便於儘早發現錯誤. 所以, 咱們強烈建議在任何可能的狀況下使用 const:

  • 若是函數不會修改傳你入的引用或指針類型參數, 該參數應聲明爲 const.
  • 儘量將函數聲明爲 const. 訪問函數應該老是 const. 其餘不會修改任何數據成員, 未調用非 const 函數, 不會返回數據成員非 const 指針或引用的函數也應該聲明成 const.
  • 若是數據成員在對象構造以後再也不發生變化, 可將其定義爲 const.

然而, 也不要發了瘋似的使用 const. 像 const int * const * const x; 就有些過了, 雖然它很是精確的描述了常量 x. 關注真正有幫助意義的信息: 前面的例子寫成 const int** x 就夠了.

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

const 的位置:

有人喜歡 int const *foo 形式, 不喜歡 const int* foo, 他們認爲前者更一致所以可讀性也更好: 遵循了 const 總位於其描述的對象以後的原則. 可是一致性原則不適用於此, "不要過分使用" 的聲明能夠取消大部分你本來想保持的一致性. 將 const 放在前面才更易讀, 由於在天然語言中形容詞 (const) 是在名詞 (int) 以前.

這是說, 咱們強制 const 在前.

5.7. 整型

Tip

C++ 內建整型中, 僅使用 int. 若是程序中須要不一樣大小的變量, 能夠使用 <stdint.h> 中長度精確的整型, 如 int16_t.若是您的變量可能不小於 2^31 (2GiB), 就用 64 位變量好比 int64_t. 此外要留意,哪怕您的值並不會超出 int 所可以表示的範圍,在計算過程當中也可能會溢出。因此拿不許時,乾脆用更大的類型。

定義:

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

若是已知整數不會太大, 咱們經常會使用 int, 如循環計數. 在相似的狀況下使用原生類型 int. 你能夠認爲 int 至少爲 32 位, 但不要認爲它會多於 32 位. 若是須要 64 位整型, 用 int64_tuint64_t.

對於大整數, 使用 int64_t.

不要使用 uint32_t 等無符號整型, 除非你是在表示一個位組而不是一個數值, 或是你須要定義二進制補碼溢出. 尤爲是不要爲了指出數值永不會爲負, 而使用無符號類型. 相反, 你應該使用斷言來保護數據.

若是您的代碼涉及容器返回的大小(size),確保其類型足以應付容器各類可能的用法。拿不許時,類型越大越好。

當心整型類型轉換和整型提高(acgtyrant 注:integer promotions, 好比 intunsigned int 運算時,前者被提高爲 unsigned int 而有可能溢出),總有意想不到的後果。

關於無符號整數:

有些人, 包括一些教科書做者, 推薦使用無符號類型表示非負數. 這種作法試圖達到自我文檔化. 可是, 在 C 語言中, 這一優勢被由其致使的 bug 所淹沒. 看看下面的例子:

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

上述循環永遠不會退出! 有時 gcc 會發現該 bug 並報警, 但大部分狀況下都不會. 相似的 bug 還會出如今比較有符合變量和無符號變量時. 主要是 C 的類型提高機制會導致無符號類型的行爲出乎你的意料.

所以, 使用斷言來指出變量爲非負數, 而不是使用無符號型!

5.8. 預處理宏

Tip

使用宏時要很是謹慎, 儘可能之內聯函數, 枚舉和常量代替之.程序中不要使用「魔數」,要使用宏、常量枚舉替換.

宏意味着你和編譯器看到的代碼是不一樣的. 這可能會致使異常行爲, 尤爲由於宏具備全局做用域.

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

宏能夠作一些其餘技術沒法實現的事情, 在一些代碼庫 (尤爲是底層庫中) 能夠看到宏的某些特性 (如用 # 字符串化, 用 ## 鏈接等等). 但在使用前, 仔細考慮一下能不能不使用宏達到一樣的目的.

下面給出的用法模式能夠避免使用宏帶來的問題; 若是你要宏, 儘量遵照:

  • 只在一個文件中使用的宏要在.cc文件中定義不要定義在頭文件中.
  • 不要試圖使用展開後會致使 C++ 構造不穩定的宏, 否則也至少要附上文檔說明其行爲.

5.9. 指針

Tip

指針變量定義時要初始化,指針銷燬後要置`nullptr`,指針使用前要進行有效性判斷.

指針變量定義時要初始化,指針被free或者delete以後,要置爲nullptr,避免訪問'野指針'。指針使用前要使用if (p == nullptr)if (p != nullptr)進行防錯處理.

//正例
char *p = nullptr;                  //指針初始化
try
{
    p = new char;
}
catch (std::bad_alloc &ba)
{
    ...
}

if (nullptr != p)                   //使用前檢查有效性
{
    *p = 'a';
}

delete p;
p = nullptr;                        //釋放後置空
//反例
char *p;            // 指針沒有初始化.
p = new char;

*p = 'a';           // 使用前沒有判斷有效性.

delete p;           // 釋放後沒有置空.

5.10. new

Tip

使用new分配內存時必定要考慮分配失敗的狀況.

目前標準版本的new失敗後會拋出一個異常的類型std::bad_alloc,在早期C++編譯器中new失敗後將返回NULL,和malloc()很是類似。因此要對兩種狀況都作處理。

char *p = nullptr;
try
{
    p = new char;
}
catch (std::bad_alloc &ba)
{
    p = nullptr;
    ...
    printf("new err\n");
}

if (nullptr != p)
{
    ...
}
else
{
    printf("new err\n");
}

5.11. 0, nullptrNULL

Tip

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

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

對於指針 (地址值), 究竟是用0NULL 仍是nullptr。實際上,一些 C++ 編譯器對NULL的定義比較特殊,特別是sizeof(NULL)就和sizeof(0)不同。(若是編譯器不支持nullptr能夠用宏定義一個。)

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

5.12. sizeof

Tip

儘量用 sizeof(varname) 代替 sizeof(type).

使用 sizeof(varname) 是由於當代碼中變量類型改變時會自動更新. 您或許會用 sizeof(type) 處理不涉及任何變量的代碼,好比處理來自外部或內部的數據格式,這時用變量就不合適了。

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

Warning

memset(&data, 0, sizeof(Struct));
if (raw_size < sizeof(int)) {
    LOG(ERROR) << "compressed record not big enough for count: " << raw_size;
    return false;
}

5.13. 列表初始化

Tip

你能夠用列表初始化。

早在 C++03 裏,聚合類型(aggregate types)就已經能夠被列表初始化了,好比數組和不自帶構造函數的結構體:

struct Point { int x; int y; };
Point p = {1, 2};

C++11 中,該特性獲得進一步的推廣,任何對象類型均可以被列表初始化。示範以下:

// Vector 接收了一個初始化列表。
vector<string> v{"foo", "bar"};

// 不考慮細節上的微妙差異,大體上相同。
// 您能夠任選其一。
vector<string> v = {"foo", "bar"};

// map 接收了一些 pair, 列表初始化大顯神威。
map<int, string> m = {{1, "one"}, {2, "2"}};

// 初始化列表也能夠用在返回類型上的隱式轉換。
vector<int> test_function() { return {1, 2, 3}; }

// 初始化列表可迭代。
for (int i : {-1, -2, -3}) {}

// 在函數調用裏用列表初始化。
void TestFunction2(vector<int> v) {}
TestFunction2({1, 2, 3});

用戶自定義類型也能夠定義接收 std::initializer_list<T> 的構造函數和賦值運算符,以自動列表初始化:

class CMyType {
 public:
  // std::initializer_list 專門接收 init 列表。
  // 得以值傳遞。
  CMyType(std::initializer_list<int> init_list) {
    for (int i : init_list) append(i);
  }
  CMyType& operator=(std::initializer_list<int> init_list) {
    clear();
    for (int i : init_list) append(i);
  }
};
CMyType m{2, 3, 5, 7};

最後,列表初始化也適用於常規數據類型的構造,哪怕沒有接收 std::initializer_list<T> 的構造函數。

double d{1.23};
// CMyOtherType 沒有 std::initializer_list 構造函數,
 // 直接上接收常規類型的構造函數。
class CMyOtherType {
 public:
  explicit CMyOtherType(string);
  CMyOtherType(int, string);
};
CMyOtherType m = {1, "b"};
// 不過若是構造函數是顯式的(explict),您就不能用 `= {}` 了。
CMyOtherType m{"b"};

至於格式化,參見 8.6. 列表初始化格式.

5.14. 模板編程

Tip

不要使用複雜的模板編程,參數不要超過3個.

定義:

模板編程指的是利用c++ 模板實例化機制是圖靈完備性, 能夠被用來實現編譯時刻的類型判斷的一系列編程技巧

優勢:

模板編程可以實現很是靈活的類型安全的接口和極好的性能, 一些常見的工具好比Google Test, std::tuple, std::function 和 Boost.Spirit. 這些工具若是沒有模板是實現不了的

缺點:

  • 模板編程所使用的技巧對於使用c++不是很熟練的人是比較晦澀, 難懂的. 在複雜的地方使用模板的代碼讓人更不容易讀懂, 而且debug 和 維護起來都很麻煩
  • 模板編程常常會致使編譯出錯的信息很是不友好: 在代碼出錯的時候, 即便這個接口很是的簡單, 模板內部複雜的實現細節也會在出錯信息顯示. 致使這個編譯出錯信息看起來很是難以理解.
  • 大量的使用模板編程接口會讓重構工具(Visual Assist X, Refactor for C++等等)更難發揮用途. 首先模板的代碼會在不少上下文裏面擴展開來, 因此很難確認重構對全部的這些展開的代碼有用, 其次有些重構工具只對已經作過模板類型替換的代碼的AST 有用. 所以重構工具對這些模板實現的原始代碼並不有效, 很難找出哪些須要重構.

結論:

  • 模板編程有時候可以實現更簡潔更易用的接口, 可是更多的時候卻拔苗助長. 所以模板編程最好只用在少許的基礎組件, 基礎數據結構上, 由於模板帶來的額外的維護成本會被大量的使用給分擔掉
  • 在使用模板編程或者其餘複雜的模板技巧的時候, 你必定要再三考慮一下. 考慮一下大家團隊成員的平均水平是否可以讀懂而且可以維護你寫的模板代碼.或者一個非c++ 程序員和一些只是在出錯的時候偶爾看一下代碼的人可以讀懂這些錯誤信息或者可以跟蹤函數的調用流程. 若是你使用遞歸的模板實例化, 或者類型列表, 或者元函數, 又或者表達式模板, 或者依賴SFINAE, 或者sizeof 的trick 手段來檢查函數是否重載, 那麼這說明你模板用的太多了, 這些模板太複雜了, 咱們不推薦使用
  • 若是你使用模板編程, 你必須考慮儘量的把複雜度最小化, 而且儘可能不要讓模板對外暴漏. 你最好只在實現裏面使用模板, 而後給用戶暴露的接口裏面並不使用模板, 這樣能提升你的接口的可讀性. 而且你應該在這些使用模板的代碼上寫儘量詳細的註釋. 你的註釋裏面應該詳細的包含這些代碼是怎麼用的, 這些模板生成出來的代碼大概是什麼樣子的. 還須要額外注意在用戶錯誤使用你的模板代碼的時候須要輸出更人性化的出錯信息. 由於這些出錯信息也是你的接口的一部分, 因此你的代碼必須調整到這些錯誤信息在用戶看起來應該是很是容易理解, 而且用戶很容易知道如何修改這些錯誤

5.15. Boost 庫

Tip

只使用 Boost 中被承認的庫.

定義:

Boost 庫集 是一個廣受歡迎, 通過同行鑑定, 免費開源的 C++ 庫集.

優勢:

Boost代碼質量廣泛較高, 可移植性好, 填補了 C++ 標準庫不少空白, 如型別的特性, 更完善的綁定器, 更好的智能指針。

缺點:

某些 Boost 庫提倡的編程實踐可讀性差, 好比元編程和其餘高級模板技術, 以及過分 "函數化" 的編程風格.

結論:

爲了向閱讀和維護代碼的人員提供更好的可讀性, 咱們只容許使用 Boost 一部分經承認的特性子集. 目前容許使用如下庫:

咱們正在積極考慮增長其它 Boost 特性, 因此列表中的規則將不斷變化.

如下庫能夠用,但因爲現在已經被 C++ 11 標準庫取代,再也不鼓勵:

5.16. C++11

Tip

不要使用C++11,除非項目中第三方庫須要.

定義:

C++11 有衆多語言和庫上的變革

優勢:

在二〇一四年八月以前,C++11 一度是官方標準,被大多 C++ 編譯器支持。它標準化不少咱們早先就在用的 C++ 擴展,簡化了很多操做,大大改善了性能和安全。

缺點:

C++11 相對於前身,複雜極了:1300 頁 vs 800 頁!不少開發者也不怎麼熟悉它。因而從長遠來看,前者特性對代碼可讀性以及維護代價難以預估。咱們說不許何時採納其特性,特別是在被迫依賴老實工具的項目上。

5.15. Boost 庫 同樣,有些 C++11 擴展提倡實則對可讀性有害的編程實踐------就像去除冗餘檢查(好比類型名)以幫助讀者,或是鼓勵模板元編程等等。有些擴展在功能上與原有機制衝突,容易招致困惑以及遷移代價。

總結:

出於編譯器支持和移植方面考慮不要使用C++11,除非項目中第三方庫須要.

6. 命名約定

最重要的一致性規則是命名管理. 命名的風格能讓咱們在不須要去查找類型聲明的條件下快速地瞭解某個名字表明的含義: 類型, 變量, 函數, 常量, 宏, 等等, 甚至. 咱們大腦中的模式匹配引擎很是依賴這些命名規則.

命名規則具備必定隨意性, 但相比按我的喜愛命名, 一致性更重要, 因此不管你認爲它們是否重要, 規則總歸是規則.

6.1. 通用命名規則

總述

函數命名, 變量命名, 文件命名要有描述性; 少用縮寫.

說明

儘量使用描述性的命名, 別心疼空間, 畢竟相比之下讓代碼易於新讀者理解更重要. 不要用只有項目開發者能理解的縮寫, 也不要經過砍掉幾個字母來縮寫單詞.

int price_count_reader;    // 無縮寫
int num_errors;            // "num" 是一個常見的寫法
int num_dns_connections;   // 人人都知道 "DNS" 是什麼
int n;                     // 毫無心義.
int nerr;                  // 含糊不清的縮寫.
int n_comp_conns;          // 含糊不清的縮寫.
int wgc_connections;       // 只有貴團隊知道是什麼意思.
int pc_reader;             // "pc" 有太多可能的解釋了.
int cstmr_id;              // 刪減了若干字母.

注意, 一些特定的廣爲人知的縮寫是容許的, 例如用 i 表示迭代變量和用 T 表示模板參數.

模板參數的命名應當遵循對應的分類: 類型模板參數應當遵循 類型命名 的規則, 而非類型模板應當遵循 變量命名 的規則.

6.2. 文件命名

總述

文件名要所有小寫, 能夠包含下劃線 (_) ,.

說明

可接受的文件命名示例:

  • my_useful_class.cc
  • myusefulclass.cc

C++ 文件要以 .cc/.cpp 結尾, 頭文件以 .h 結尾. 專門插入文本的文件則以 .inc 結尾, 參見 頭文件自足.

不要使用已經存在於 /usr/include 下的文件名 ( 注: 即編譯器搜索系統頭文件的路徑), 如 db.h.

一般應儘可能讓文件名更加明確. http_server_logs.h 就比 logs.h 要好. 定義類時文件名通常成對出現, 如 foo_bar.hfoo_bar.cc, 對應於類 FooBar.

內聯函數必須放在 .h 文件中. 若是內聯函數比較短, 就直接放在 .h 中.

對於MFC項目中自動生成的文件名可不遵循本規則。

6.3. 類型命名

總述

類型名稱的每一個單詞首字母均大寫, 不包含下劃線,命名規則以下:

命名規則 類型+描述
類型
C :類
Struct : 結構體
P : typedef/using 指針類型
En: 枚舉

說明

全部類型命名 ------ 類, 結構體, 類型定義 (typedef), 枚舉 均使用相同約定, 例如:

// 類和結構體
class CUrlTable { ...
class CUrlTableTester { ...
struct StructUrlTableProperties { ...

// 類型定義
typedef CUrlTable *PCUrlTable;

// using 別名
using *PCUrlTable = CUrlTable;

// 枚舉
enum EnUrlTableErrors { ...

6.4. 變量命名

匈牙利命名法

總述

變量 (包括函數參數) 和數據成員名採用匈牙利命名法.

說明

匈牙利命名法的規則是:

屬性+類型+描述
屬性通常是小寫字母+_:
g_:全局變量
m_:類成員變量
s_:靜態變量
類型就多了:
b:bool
sz:以零結束的字符串
p:指針
n:整型
dw:雙字
l:長整型
無符號:u
函數:fn

6.5. 常量命名

總述

聲明爲 const 的變量, 或在程序運行期間其值始終保持不變的, 命名時以 "const" 開頭, 大小寫混合. 例如:

const int constDaysInAWeek = 7;

說明

全部具備靜態存儲類型的變量 (例如靜態變量或全局變量, 參見 存儲類型) 都應當以此方式命名. 對於其餘存儲類型的變量, 如自動變量等, 這條規則是可選的. 若是不採用這條規則, 就按照通常的變量命名規則.

6.6. 函數命名

總述

常規函數使用大小寫混合: MyExcitingFunction(), MyExcitingMethod()

說明

通常來講, 函數名的每一個單詞首字母大寫 (即 "駝峯變量名" 或 "帕斯卡變量名"), 沒有下劃線. 對於首字母縮寫的單詞, 更傾向於將它們視做一個單詞進行首字母大寫 (例如, 寫做 StartRpc() 而非 StartRPC()).

AddTableEntry()
DeleteUrl()
OpenFileOrDie()

(一樣的命名規則同時適用於類做用域與命名空間做用域的常量, 由於它們是做爲 API 的一部分暴露對外的, 所以應當讓它們看起來像是一個函數, 由於在這時, 它們其實是一個對象而非函數的這一事實對外不過是一個可有可無的實現細節.)

6.7. 命名空間命名

總述

命名空間以小寫字母命名. 最高級命名空間的名字取決於項目名稱. 要注意避免嵌套命名空間的名字之間和常見的頂級命名空間的名字之間發生衝突.

頂級命名空間的名稱應當是項目名或者是該命名空間中的代碼所屬的團隊的名字. 命名空間中的代碼, 應當存放於和命名空間的名字匹配的文件夾或其子文件夾中.

注意 不使用縮寫做爲名稱 的規則一樣適用於命名空間. 命名空間中的代碼極少須要涉及命名空間的名稱, 所以沒有必要在命名空間中使用縮寫.

要避免嵌套的命名空間與常見的頂級命名空間發生名稱衝突. 因爲名稱查找規則的存在, 命名空間之間的衝突徹底有可能致使編譯失敗. 尤爲是, 不要建立嵌套的 std 命名空間. 建議使用更獨特的項目標識符 (websearch::index, websearch::index_util) 而很是見的極易發生衝突的名稱 (好比 websearch::util).

對於 internal 命名空間, 要小心加入到同一 internal 命名空間的代碼之間發生衝突 (因爲內部維護人員一般來自同一團隊, 所以常有可能致使衝突). 在這種狀況下, 請使用文件名以使得內部名稱獨一無二 (例如對於 frobber.h, 使用 websearch::index::frobber_internal).

6.8. 枚舉命名

總述

枚舉命名和 類型命名 相同,枚舉值的命名採用所有字母大寫單詞之間用 _ 分隔 ENUM_ 開頭後跟名字: ENUM_NAME.

enum EnUrlTableErrors {
    ENUM_OK = 0,
    ENUM_ERROR_OUT_OF_MEMORY,
    ENUM_ERROR_MALFORMED_INPUT,
};

6.9. 宏命名

總述

宏像這樣命名: MY_MACRO_THAT_SCARES_SMALL_CHILDREN.

說明

參考 預處理宏; 宏命名像枚舉命名同樣所有大寫, 使用下劃線:

#define ROUND(x) ...
#define PI_ROUNDED 3.0

6.10. 命名規則的特例

總述

若是你命名的實體與已有 C/C++ 實體類似, 可參考現有命名策略.

bigopen(): 函數名, 參照 open() 的形式

uint: typedef

bigpos: structclass, 參照 pos 的形式

sparse_hash_map: STL 型實體; 參照 STL 命名約定

LONGLONG_MAX: 常量, 如同 INT_MAX

7. 註釋

註釋雖然寫起來很痛苦, 但對保證代碼可讀性相當重要. 下面的規則描述瞭如何註釋以及在哪兒註釋. 固然也要記住: 註釋當然很重要, 但最好的代碼應當自己就是文檔. 有意義的類型名和變量名, 要遠賽過要用註釋解釋的含糊不清的名字.

你寫的註釋是給代碼讀者看的, 也就是下一個須要理解你的代碼的人. 因此慷慨些吧, 下一個讀者可能就是你!

7.1. 註釋風格

總述

文件頭,函數/類說明使用/* */

其餘使用 ///* */, 統一就好.

說明

///* */ 均可以; 但 // 經常使用. 要在如何註釋及註釋風格上確保統一.

7.2. 文件註釋

總述

在每個文件開頭加入版權公告.

文件註釋描述了該文件的內容. 若是一個文件只聲明, 或實現, 或測試了一個對象, 而且這個對象已經在它的聲明處進行了詳細的註釋, 那麼就不必再加上文件註釋. 除此以外的其餘文件都須要文件註釋.

/***********************************************
*  Copyright DPIN                              
*                                              
*  @file     Example.h                         
*  @brief    對文件的簡述,文件的功能,規範文檔,引用開源代碼版權信息.                       
*  @author   name                           
*  @date     2018/04/10                        
*                                              
*  Change History :                            
*  2018/04/10 | 1.0.0.1 | name | 描述修改項  
*                                              
***********************************************/

說明

法律公告和做者信息

每一個文件都應該包含許可證引用. 爲項目選擇合適的許可證版本.(好比, Apache 2.0, BSD, LGPL, GPL)

若是你對原始做者的文件作了重大修改, 請考慮刪除原做者信息.

做者使用簡稱,日期,修改記錄

文件內容

若是一個 .h 文件聲明瞭多個概念, 則文件註釋應當對文件的內容作一個大體的說明, 同時說明各概念之間的聯繫. 一個一到兩行的文件註釋就足夠了, 對於每一個概念的詳細文檔應當放在各個概念中, 而不是文件註釋中.

不要在 .h.cc 之間複製註釋, 這樣的註釋偏離了註釋的實際意義.

7.3. 類註釋

總述

每一個類的定義都要附帶一份註釋, 描述類的功能和用法, 除非它的功能至關明顯.

/* 
  Iterates over the contents of a GargantuanTable.
  Example:
    GargantuanTableIterator* iter = table->NewIterator();
    for (iter->Seek("foo"); !iter->done(); iter->Next()) {
      process(iter->key(), iter->value());
    }
    delete iter;
*/
class GargantuanTableIterator {
  ...
};

說明

類註釋應當爲讀者理解如何使用與什麼時候使用類提供足夠的信息, 同時應當提醒讀者在正確使用此類時應當考慮的因素. 若是類有任何同步前提, 請用文檔說明. 若是該類的實例可被多線程訪問, 要特別注意文檔說明多線程環境下相關的規則和常量使用.

若是你想用一小段代碼演示這個類的基本用法或一般用法, 放在類註釋裏也很是合適.

若是類的聲明和定義分開了(例如分別放在了 .h.cc 文件中), 此時, 描述類用法的註釋應當和接口定義放在一塊兒, 描述類的操做和實現的註釋應當和實現放在一塊兒.

7.4. 函數註釋

總述

函數聲明處的註釋描述函數功能; 定義處的註釋描述函數實現.

說明

函數聲明

基本上每一個函數聲明處前都應當加上註釋, 描述函數的功能和用途. 只有在函數的功能簡單而明顯時才能省略這些註釋(例如, 簡單的取值和設值函數). 註釋使用敘述式 ("Opens the file") 而非指令式 ("Open the file"); 註釋只是爲了描述函數, 而不是命令函數作什麼. 一般, 註釋不會描述函數如何工做. 那是函數定義部分的事情.

函數聲明處註釋的內容:

  • 函數的輸入輸出.
  • 對類成員函數而言: 函數調用期間對象是否須要保持引用參數, 是否會釋放這些參數.
  • 函數是否分配了必須由調用者釋放的空間.
  • 參數是否能夠爲空指針.
  • 是否存在函數使用上的性能隱患.
  • 若是函數是可重入的, 其同步前提是什麼?

舉例以下:

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

但也要避免羅羅嗦嗦, 或者對顯而易見的內容進行說明. 下面的註釋就沒有必要加上 "不然返回 false", 由於已經暗含其中了:

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

註釋函數重載時, 註釋的重點應該是函數中被重載的部分, 而不是簡單的重複被重載的函數的註釋. 多數狀況下, 函數重載不須要額外的文檔, 所以也沒有必要加上註釋.

註釋構造/析構函數時, 切記讀代碼的人知道構造/析構函數的功能, 因此 "銷燬這一對象" 這樣的註釋是沒有意義的. 你應當註明的是註明構造函數對參數作了什麼 (例如, 是否取得指針全部權) 以及析構函數清理了什麼. 若是都是些可有可無的內容, 直接省掉註釋. 析構函數前沒有註釋是很正常的.

函數定義

若是函數的實現過程當中用到了很巧妙的方式, 那麼在函數定義處應當加上解釋性的註釋. 例如, 你所使用的編程技巧, 實現的大體步驟, 或解釋如此實現的理由. 舉個例子, 你能夠說明爲何函數的前半部分要加鎖然後半部分不須要.

不要.h 文件或其餘地方的函數聲明處直接複製註釋. 簡要重述函數功能是能夠的, 但註釋重點要放在如何實現上.

7.5. 變量註釋

總述

一般變量名自己足以很好說明變量用途. 某些狀況下, 也須要額外的註釋說明.

說明

類數據成員

每一個類數據成員 (也叫實例變量或成員變量) 都應該用註釋說明用途. 若是有非變量的參數(例如特殊值, 數據成員之間的關係, 生命週期等)不可以用類型與變量名明確表達, 則應當加上註釋. 然而, 若是變量類型與變量名已經足以描述一個變量, 那麼就再也不須要加上註釋.

特別地, 若是變量能夠接受 NULL-1 等警惕值, 須加以說明. 好比:

private:
 // Used to bounds-check table accesses. -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;

7.6. 實現註釋

總述

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

說明

代碼前註釋

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

// 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();  // Two spaces between the code and the comment.
{ // One space before comment when opening a new scope is allowed,
  // thus the comment lines up with the following comments and code.
  DoSomethingElse();  // Two spaces before line comments normally.
}
std::vector<string> list{
                    // Comments in braced lists describe the next element...
                    "First item",
                    // .. and should be aligned appropriately.
"Second item"};
DoSomething(); /* For trailing block comments, one space is fine. */

函數參數註釋

若是函數參數的意義不明顯, 考慮用下面的方式進行彌補:

  • 若是參數是一個字面常量, 而且這一常量在多處函數調用中被使用, 用以推斷它們一致, 你應當用一個常量名讓這一約定變得更明顯, 而且保證這一約定不會被打破.
  • 考慮更改函數的簽名, 讓某個 bool 類型的參數變爲 enum 類型, 這樣可讓這個參數的值表達其意義.
  • 若是某個函數有多個配置選項, 你能夠考慮定義一個類或結構體以保存全部的選項, 並傳入類或結構體的實例. 這樣的方法有許多優勢, 例如這樣的選項能夠在調用處用變量名引用, 這樣就能清晰地代表其意義. 同時也減小了函數參數的數量, 使得函數調用更易讀也易寫. 除此以外, 以這樣的方式, 若是你使用其餘的選項, 就無需對調用點進行更改.
  • 用具名變量代替大段而複雜的嵌套表達式.
  • 萬不得已時, 才考慮在調用點用註釋闡明參數的意義.

好比下面的示例的對比:

// What are these arguments?
const DecimalNumber product = CalculateProduct(values, 7, false, nullptr);

ProductOptions options;
options.set_precision_decimals(7);
options.set_use_cache(ProductOptions::kDontUseCache);
const DecimalNumber product =
    CalculateProduct(values, options, /*completion_callback=*/nullptr);

哪一個更清晰一目瞭然.

不容許的行爲

不要描述顯而易見的現象, 永遠不要 用天然語言翻譯代碼做爲註釋, 除非即便對深刻理解 C++ 的讀者來講代碼的行爲都是不明顯的. 要假設讀代碼的人 C++ 水平比你高, 即使他/她可能不知道你的用意:

你所提供的註釋應當解釋代碼 爲何 要這麼作和代碼的目的, 或者最好是讓代碼自文檔化.

比較這樣的註釋:

// Find the element in the vector.  <-- 差: 這太明顯了!
iter = std::find(v.begin(), v.end(), element);
if (iter != v.end()) 
{
  Process(element);
}

和這樣的註釋:

// Process "element" unless it was already processed.
iter = std::find(v.begin(), v.end(), element);
if (iter != v.end()) 
{
  Process(element);
}

自文檔化的代碼根本就不須要註釋. 上面例子中的註釋對下面的代碼來講就是毫無必要的:

if (!IsAlreadyProcessed(element)) 
{
  Process(element);
}

7.7. 標點, 拼寫和語法

總述

注意標點, 拼寫和語法; 寫的好的註釋比差的要易讀的多.

說明

註釋的一般寫法是包含正確大小寫和結尾句號的完整敘述性語句. 大多數狀況下, 完整的句子比句子片斷可讀性更高. 短一點的註釋, 好比代碼行尾註釋, 能夠隨意點, 但依然要注意風格的一致性.

雖然被別人指出該用分號時卻用了逗號多少有些尷尬, 但清晰易讀的代碼仍是很重要的. 正確的標點, 拼寫和語法對此會有很大幫助.

代碼中的常量字符只能使用英文(包括日誌內容),註釋能夠用中文。

7.8. TODO 註釋

總述

對那些臨時的, 短時間的解決方案, 或已經夠好但仍不完美的代碼使用 TODO 註釋.

TODO 註釋要使用全大寫的字符串 TODO, 在隨後的圓括號裏寫上你的名字, 郵件地址, bug ID, 或其它身份標識和與這一 TODO 相關的 issue. 主要目的是讓添加註釋的人 (也是能夠請求提供更多細節的人) 可根據規範的 TODO 格式進行查找. 添加 TODO 註釋並不意味着你要本身來修正, 所以當你加上帶有姓名的 TODO 時, 通常都是寫上本身的名字.

// TODO(kl@gmail.com): Use a "*" here for concatenation operator.
// TODO(Zeke) change this to use relations.
// TODO(bug 12345): remove the "Last visitors" feature

若是加 TODO 是爲了在 "未來某一天作某事", 能夠附上一個很是明確的時間 "Fix by November 2005"), 或者一個明確的事項 ("Remove this code when all clients can handle XML responses.").

7.9. 棄用註釋

總述

經過棄用註釋(DEPRECATED comments)以標記某接口點已棄用.

您能夠寫上包含全大寫的 DEPRECATED 的註釋, 以標記某接口爲棄用狀態. 註釋能夠放在接口聲明前, 或者同一行.

DEPRECATED 一詞後, 在括號中留下您的名字, 郵箱地址以及其餘身份標識.

棄用註釋應當包涵簡短而清晰的指引, 以幫助其餘人修復其調用點. 在 C++ 中, 你能夠將一個棄用函數改形成一個內聯函數, 這一函數將調用新的接口.

僅僅標記接口爲 DEPRECATED 並不會讓你們不約而同地棄用, 您還得親自主動修正調用點(callsites), 或是找個幫手.

修正好的代碼應該不會再涉及棄用接口點了, 着實改用新接口點. 若是您不知從何下手, 能夠找標記棄用註釋的當事人一塊兒商量.

7.10. 修改他人代碼註釋

須要留下您的名字或其餘身份標識,和修改時間以及修改緣由.

//name 日期,修改緣由

8. 格式

每一個人均可能有本身的代碼風格和格式, 但若是一個項目中的全部人都遵循同一風格的話, 這個項目就能更順利地進行. 每一個人未必能贊成下述的每一處格式規則, 並且其中的很多規則須要必定時間的適應, 但整個項目服從統一的編程風格是很重要的, 只有這樣才能讓全部人輕鬆地閱讀和理解代碼.

8.1. 行長度

總述

每一行代碼字符數不超過 80.

咱們也認識到這條規則是有爭議的, 但不少已有代碼都遵守這一規則, 所以咱們感受一致性更重要.

優勢

提倡該原則的人認爲強迫他們調整編輯器窗口大小是很野蠻的行爲. 不少人同時並排開幾個代碼窗口, 根本沒有多餘的空間拉伸窗口. 你們都把窗口最大尺寸加以限定, 而且 80 列寬是傳統標準. 那麼爲何要改變呢?

缺點

反對該原則的人則認爲更寬的代碼行更易閱讀. 80 列的限制是上個世紀 60 年代的大型機的古板缺陷; 現代設備具備更寬的顯示屏, 能夠很輕鬆地顯示更多代碼.

結論

80 個字符是最大值.

若是沒法在不傷害易讀性的條件下進行斷行, 那麼註釋行能夠超過 80 個字符, 這樣能夠方便複製粘貼. 例如, 帶有命令示例或 URL 的行能夠超過 80 個字符.

包含長路徑的 #include 語句能夠超出80列.

頭文件保護 能夠無視該原則.

8.2. 空格仍是製表位

總述

只使用空格, 每次縮進 2 個空格.

說明

咱們使用空格縮進. 不要在代碼中使用製表符. 你應該設置編輯器將製表符轉爲空格.(注:Visual Studio 2010 工具->選項->文本編輯器->C/C++->製表符->插入空格)

8.3. 函數聲明與定義

總述

返回類型和函數名在同一行, 參數也儘可能放在同一行, 若是放不下就對形參分行, 分行方式與 函數調用 一致.

說明

函數看上去像這樣:

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

注意如下幾點:

  • 使用好的參數名.
  • 只有在參數未被使用或者其用途很是明顯時, 才能省略參數名.
  • 若是返回類型和函數名在一行放不下, 分行.
  • 若是返回類型與函數聲明或定義分行了, 不要縮進.
  • 左圓括號老是和函數名在同一行.
  • 函數名和左圓括號間永遠沒有空格.
  • 圓括號與參數間沒有空格.
  • 左大括號總在最後一個參數同一行的末尾處, 不另起新行.
  • 右大括號老是單獨位於函數最後一行, 或者與左大括號同一行.
  • 右圓括號和左大括號間老是有一個空格.
  • 全部形參應儘量對齊.
  • 缺省縮進爲 2 個空格.
  • 換行後的參數保持 4 個空格的縮進.

未被使用的參數, 或者根據上下文很容易看出其用途的參數, 能夠省略參數名:

class CFoo 
{
 public:
  CFoo(CFoo&&);
  CFoo(const CFoo&);
  CFoo& operator=(CFoo&&);
  CFoo& operator=(const CFoo&);
};

未被使用的參數若是其用途不明顯的話, 在函數定義處將參數名註釋起來:

class CShape 
{
 public:
  virtual void Rotate(double radians) = 0;
};

class CCircle : public CShape 
{
 public:
  void Rotate(double radians) override;
};

void CCircle::Rotate(double /*radians*/) {}
// 差 - 若是未來有人要實現, 很難猜出變量的做用.
void CCircle::Rotate(double) {}

屬性, 和展開爲屬性的宏, 寫在函數聲明或定義的最前面, 即返回類型以前:

MUST_USE_RESULT bool IsOK();

8.4. Lambda 表達式

總述

Lambda 表達式對形參和函數體的格式化和其餘函數一致; 捕獲列表同理, 表項用逗號隔開.

說明

若用引用捕獲, 在變量名和 & 之間不留空格.

int x = 0;
auto add_to_x = [&x](int n) { x += n; };

短 lambda 就寫得和內聯函數同樣.

std::set<int> blacklist = {7, 8, 9};
std::vector<int> digits = {3, 9, 1, 8, 4, 7, 1};
digits.erase(std::remove_if(digits.begin(), digits.end(), [&blacklist](int i) {
               return blacklist.find(i) != blacklist.end();
             }),
             digits.end());

8.5. 函數調用

總述

要麼一行寫完函數調用, 要麼在圓括號裏對參數分行, 要麼參數另起一行且縮進四格. 若是沒有其它顧慮的話, 儘量精簡行數, 好比把多個參數適當地放在同一行裏.

說明

函數調用遵循以下形式:

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

若是同一行放不下, 可斷爲多行, 後面每一行都和第一個實參對齊, 左圓括號後和右圓括號前不要留空格:

bool retval = DoSomething(averyveryveryverylongargument1,
                          argument2, argument3);

參數也能夠放在次行, 縮進四格:

if (...) 
{
  ...
  ...
  if (...) 
  {
    DoSomething(
        argument1, argument2,  // 4 空格縮進
        argument3, argument4);
  }

把多個參數放在同一行以減小函數調用所需的行數, 除非影響到可讀性. 有人認爲把每一個參數都獨立成行, 不只更好讀, 並且方便編輯參數. 不過, 比起所謂的參數編輯, 咱們更看重可讀性, 且後者比較好辦:

若是一些參數自己就是略複雜的表達式, 且下降了可讀性, 那麼能夠直接建立臨時變量描述該表達式, 並傳遞給函數:

int my_heuristic = scores[x] * y + bases[x];
bool retval = DoSomething(my_heuristic, x, y, z);

或者放着無論, 補充上註釋:

bool retval = DoSomething(scores[x] * y + bases[x],  // Score heuristic.
                          x, y, z);

若是某參數獨立成行, 對可讀性更有幫助的話, 那也能夠如此作. 參數的格式處理應當以可讀性而非其餘做爲最重要的原則.

此外, 若是一系列參數自己就有必定的結構, 能夠酌情地按其結構來決定參數格式:

// 經過 3x3 矩陣轉換 widget.
my_widget.Transform(x1, x2, x3,
                    y1, y2, y3,
                    z1, z2, z3);

8.6. 列表初始化格式

總述

您平時怎麼格式化函數調用, 就怎麼格式化 列表初始化.

說明

若是列表初始化伴隨着名字, 好比類型或變量名, 格式化時將將名字視做函數調用名, [{}]{.title-ref} 視做函數調用的括號. 若是沒有名字, 就視做名字長度爲零.

// 一行列表初始化示範.
return {foo, bar};
functioncall({foo, bar});
pair<int, int> p{foo, bar};

// 當不得不斷行時.
SomeFunction(
    {"assume a zero-length name before {"},  // 假設在 { 前有長度爲零的名字.
    some_other_function_parameter);
SomeType variable{
    some, other, values,
    {"assume a zero-length name before {"},  // 假設在 { 前有長度爲零的名字.
    SomeOtherType{
        "Very long string requiring the surrounding breaks.",  // 很是長的字符串, 先後都須要斷行.
        some, other values},
    SomeOtherType{"Slightly shorter string",  // 稍短的字符串.
                  some, other, values}};
SomeType variable{
    "This is too long to fit all in one line"};  // 字符串過長, 所以沒法放在同一行.
MyType m = {  // 注意了, 您能夠在 { 前斷行.
    superlongvariablename1,
    superlongvariablename2,
    {short, interior, list},
    {interiorwrappinglist,
     interiorwrappinglist2}};

8.7. 條件語句

總述

不在圓括號內使用空格. 關鍵字 ifelse 另起一行,若是有else if 在最後要加一個 else.

//容許格式
if (condition) 
{  // 圓括號裏沒有空格.
  ...  // 2 空格縮進.
} 
else if (...) 
{  // else 與 if 的右括號同一行.
  ...
} 
else 
{
  ...
}

若是你更喜歡在圓括號內部加空格:

//不容許
if ( condition ) 
{  // 圓括號與空格緊鄰 - 不常見
  ...  // 2 空格縮進.
} 
else 
{  // else 與 if 的右括號同一行.
  ...
}

注意全部狀況下 if 和左圓括號間都有個空格:

if(condition)     // 差 - IF 後面沒空格.
if (condition)  // 好

8.8. 循環和開關選擇語句

總述

switch 語句使用大括號分段, 以代表 cases 之間不是連在一塊兒的. 在單語句循環裏,要加括號. 空循環體應使用 {} .

說明

switch 語句中的 case 塊使用大括號, 要按照下文所述的方法.

若是有不知足 case 條件的枚舉值, switch 應該老是包含一個 default 匹配 (若是有輸入值沒有 case 去處理, 編譯器將給出 warning). 若是 default 應該永遠執行不到, 簡單的加條 assert ,兩條case之間沒有break須要註釋加以說明:

switch (var) 
{
  case 0: 
  {  // 2 空格縮進
    ...      // 4 空格縮進
    break;
  }
  case 1: // 沒有break
  case 2:
  {
    ...
    break;
  }
  default: 
  {
    assert(false);
  }
}

在單語句循環裏,加括號:

for (int i = 0; i < kSomeNumber; ++i) 
{
  printf("I take it back\n");
}

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

while (condition) 
{
  // 反覆循環直到條件失效.
}
for (int i = 0; i < kSomeNumber; ++i) {}  // 可 - 空循環體.
while (condition);  // 差 - 看起來僅僅只是 while/loop 的部分之一.

8.9. 指針和引用表達式

總述

句點或箭頭先後不要有空格. 指針/地址操做符 (*, &) 以後不能有空格.

說明

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

x = *p;
p = &x;
x = r.y;
x = r->y;

注意:

  • 在訪問成員時, 句點或箭頭先後沒有空格.
  • 指針操做符 *& 後沒有空格.

在聲明指針變量或參數時, 星號與變量名緊挨:

// 好, 空格前置.
char *c;
const string &str;

// 壞, 空格後置.
char* c;
const string& str;
int x, *y;  // 不容許 - 在多重聲明中不能使用 & 或 *
char * c;  // 差 - * 兩邊都有空格
const string & str;  // 差 - & 兩邊都有空格.

在單個文件內要保持風格一致, 因此, 若是是修改現有文件, 要遵守該文件的風格.

8.10. 布爾表達式

總述

若是一個布爾表達式超過 標準行寬, 斷行方式要統一一下.==與常量比較時常量要寫在前面,爲了增長可讀性每一個表達式要加括號.

說明

常量放在==前面,防止==誤寫成=

if (NULL == p)
{
    ...
}

布爾表達式斷行方式, 邏輯與 (&&) 操做符總位於行頭:

if ((this_one_thing > this_other_thing) 
    && (a_third_thing == a_fourth_thing) 
    && yet_another 
    && last_one) 
{
  ...
}

8.11. 函數返回值

總述

不要在 return 表達式里加上非必須的圓括號.

說明

只有在寫 x = expr 要加上括號的時候纔在 return expr; 裏使用括號.

return result;                  // 返回值很簡單, 沒有圓括號.
// 能夠用圓括號把複雜表達式圈起來, 改善可讀性.
return (some_long_condition &&
        another_condition);
return (value);                // 畢竟您歷來不會寫 var = (value);
return(result);                // return 可不是函數!

8.12. 變量及數組初始化

總述

=, (){} 都可.

說明

您能夠用 =, (){}, 如下的例子都是正確的:

int x = 3;
int x(3);
int x{3};
string name("Some Name");
string name = "Some Name";
string name{"Some Name"};

請務必當心列表初始化 {...}std::initializer_list 構造函數初始化出的類型. 非空列表初始化就會優先調用 std::initializer_list, 不過空列表初始化除外, 後者原則上會調用默認構造函數. 爲了強制禁用 std::initializer_list 構造函數, 請改用括號.

vector<int> v(100, 1);  // 內容爲 100 個 1 的向量.
vector<int> v{100, 1};  // 內容爲 100 和 1 的向量.

此外, 列表初始化不容許整型類型的四捨五入, 這能夠用來避免一些類型上的編程失誤.

int pi(3.14);  // 好 - pi == 3.
int pi{3.14};  // 編譯錯誤: 縮窄轉換.

8.13. 預處理指令

總述

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

說明

即便預處理指令位於縮進代碼塊中, 指令也應從行首開始.

// 好 - 指令從行首開始
  if (lopsided_score) 
  {
#if DISASTER_PENDING      // 正確 - 從行首開始
    DropEverything();
# if NOTIFY               // 非必要 - # 後跟空格
    NotifyClient();
# endif
#endif
    BackToNormal();
  }
// 差 - 指令縮進
  if (lopsided_score) 
  {
    #if DISASTER_PENDING  // 差 - "#if" 應該放在行開頭
    DropEverything();
    #endif                // 差 - "#endif" 不要縮進
    BackToNormal();
  }

8.14. 類格式

總述

訪問控制塊的聲明依次序是 public:, protected:, private:, 不縮進.

說明

類聲明 (下面的代碼中缺乏註釋, 參考 類註釋) 的基本格式以下:

class CMyClass : public COtherClass 
{
public:      // 注意沒有縮進
  CMyClass();  // 標準的兩空格縮進
  explicit CMyClass(int var);
  ~CMyClass() {}

  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_;
};

注意事項:

  • 全部基類名應在 80 列限制下儘可能與子類名放在同一行.
  • 關鍵詞 public:, protected:, private: 不要縮進.
  • 除第一個關鍵詞 (通常是 public) 外, 其餘關鍵詞前要空一行.
  • 這些關鍵詞後不要保留空行.
  • public 放在最前面, 而後是 protected, 最後是 private.
  • 關於聲明順序的規則請參考 聲明順序 一節.

8.15. 構造函數初始值列表

總述

構造函數初始化列表放在同一行或按四格縮進並排多行.

說明

下面兩種初始值列表方式均可以接受:

// 若是全部變量能放在同一行:
CMyClass::CMyClass(int var) : some_var_(var) 
{
  DoSomething();
}

// 若是不能放在同一行,
// 必須置於冒號後, 並縮進 4 個空格
CMyClass::CMyClass(int var)
    : some_var_(var), some_other_var_(var + 1) 
{
  DoSomething();
}

// 若是初始化列表須要置於多行, 將每個成員放在單獨的一行
// 並逐行對齊
CMyClass::CMyClass(int var)
    : some_var_(var),             // 4 space indent
      some_other_var_(var + 1) 
{  // lined up
  DoSomething();
}

// 右大括號 } 能夠和左大括號 { 放在同一行
// 若是這樣作合適的話
CMyClass::CMyClass(int var)
    : some_var_(var) {}

8.16. 命名空間格式化

總述

命名空間內容不縮進.

說明

命名空間 不要增長額外的縮進層次, 例如:

namespace 
{

void foo() 
{  // 正確. 命名空間內沒有額外的縮進.
  ...
}

}  // namespace

不要在命名空間內縮進:

namespace 
{

  // 錯, 縮進多餘了.
  void foo() 
  {
    ...
  }

}  // namespace

聲明嵌套命名空間時, 每一個命名空間都獨立成行.

namespace foo 
{
namespace bar 
{

8.17. 水平留白

總述

水平留白的使用根據在代碼中的位置決定. 永遠不要在行尾添加沒意義的留白.

說明

通用

int i = 0;  // 分號前不加空格.
// 列表初始化中大括號內的空格是可選的.
// 若是加了空格, 那麼兩邊都要加上.
int x[] = { 0 };
int x[] = {0};

// 繼承與初始化列表中的冒號先後恆有空格.
class CFoo : public CBar 
{
public:
  // 對於單行函數的實現, 在大括號內加上空格
  // 而後是函數實現
  CFoo(int b) : CBar(), baz_(b) {}  // 大括號裏面是空的話, 不加空格.
  void Reset() { baz_ = 0; }  // 用括號把大括號與實現分開.
  ...

添加冗餘的留白會給其餘人編輯時形成額外負擔. 所以, 行尾不要留空格. 若是肯定一行代碼已經修改完畢, 將多餘的空格去掉; 或者在專門清理空格時去掉(尤爲是在沒有其餘人在處理這件事的時候). (注: 如今大部分代碼編輯器稍加設置後, 都支持自動刪除行首/行尾空格)

循環和條件語句

if (b) 
{          // if 條件語句和循環語句關鍵字後均有空格.
}
while (test) {}   // 圓括號內部不緊鄰空格.
switch (i)
for (int i = 0; i < 5; ++i) 
switch ( i ) 
{    // 循環和條件語句的圓括號裏能夠與空格緊鄰.
if ( test ) 
{     // 圓括號, 但這不多見. 總之要一致.
for ( int i = 0; i < 5; ++i ) 
{
for ( ; i < 5 ; ++i) 
{  // 循環裏內 ; 後恆有空格, ;  前能夠加個空格.
switch (i) 
{
  case 1:         // switch case 的冒號前無空格.
    ...
  case 2: break;  // 若是冒號有代碼, 加個空格.

操做符

// 賦值運算符先後老是有空格.
x = 0;

// 其它二元操做符也先後恆有空格, 不過對於表達式的子式能夠不加空格.
// 圓括號內部沒有緊鄰空格.
v = (w * x) + (y / z);

v = w * (x + z);

// 在參數和一元操做符之間不加空格.
x = -5;
++x;
if (x && !y)
  ...

模板和轉換

// 尖括號(< and >) 不與空格緊鄰, < 前沒有空格, > 和 ( 之間也沒有.
vector<string> x;
y = static_cast<char*>(x);

// 在類型與指針操做符之間留空格也能夠, 但要保持一致.
vector<char *> x;

8.18. 垂直留白

總述

垂直留白越少越好.

說明

這不只僅是規則而是原則問題了: 不在萬不得已, 不要使用空行. 尤爲是: 兩個函數定義之間的空行不要超過 2 行, 函數體首尾不要留空行, 函數體中也不要隨意添加空行.

基本原則是: 同一屏能夠顯示的代碼越多, 越容易理解程序的控制流. 固然, 過於密集的代碼塊和過於疏鬆的代碼塊一樣難看, 這取決於你的判斷. 但一般是垂直留白越少越好.

下面的規則可讓加入的空行更有效:

  • 函數體內開頭或結尾的空行可讀性微乎其微.
  • 在多重 if-else 塊里加空行或許有點可讀性.

9. 規則特例

前面說明的編程習慣基本都是強制性的. 但全部優秀的規則都容許例外, 這裏就是探討這些特例.

9.1. 現有不合規範的代碼

總述

對於現有不符合既定編程風格的代碼能夠網開一面.

說明

當你修改使用其餘風格的代碼時, 爲了與代碼原有風格保持一致能夠不使用本指南約定. 若是不放心, 能夠與代碼原做者或如今的負責人員商討. 記住, 一致性 也包括原有的一致性.

9.2. Windows 代碼

總述

Windows 程序員有本身的編程習慣, 主要源於 Windows 頭文件和其它 Microsoft 代碼. 咱們但願任何人均可以順利讀懂你的代碼, 因此針對全部平臺的 C++ 編程只給出一個單獨的指南.

說明

若是你習慣使用 Windows 編碼風格, 這兒有必要重申一下某些你可能會忘記的指南:

  • Windows 定義了不少原生類型的同義詞 , 如 DWORD, HANDLE 等等. 在調用 Windows API 時這是徹底能夠接受甚至鼓勵的. 即便如此, 仍是儘可能使用原有的 C++ 類型, 例如使用 const TCHAR * 而不是 LPCTSTR.
  • 使用 Microsoft Visual C++ 進行編譯時, 將警告級別設置爲 3 或更高, 並將全部警告(warnings)看成錯誤(errors)處理.
  • 不要使用 #pragma once; 而應該使用 Google 的頭文件保護規則. 頭文件保護的路徑應該相對於項目根目錄 .
  • 除非萬不得已, 不要使用任何非標準的擴展, 如 #pragma__declspec. 使用 __declspec(dllimport)__declspec(dllexport) 是容許的, 但必須經過宏來使用, 好比 DLLIMPORTDLLEXPORT, 這樣其餘人在分享使用這些代碼時能夠很容易地禁用這些擴展.

然而, 在 Windows 上仍然有一些咱們偶爾須要違反的規則:

  • 一般咱們 禁止使用多重繼承, 但在使用 COM 和 ATL/WTL 類時能夠使用多重繼承. 爲了實現 COM 或 ATL/WTL 類/接口, 你可能不得不使用多重實現繼承.
  • 雖然代碼中不該該使用異常, 可是在 ATL 和部分 STL(包括 Visual C++ 的 STL) 中異常被普遍使用. 使用 ATL 時, 應定義 _ATL_NO_EXCEPTIONS 以禁用異常. 你須要研究一下是否可以禁用 STL 的異常, 若是沒法禁用, 能夠啓用編譯器異常. (注意這只是爲了編譯 STL, 本身的代碼裏仍然不該當包含異常處理).
  • ·一般爲了利用頭文件預編譯, 每一個每一個源文件的開頭都會包含一個名爲 StdAfx.hprecompile.h 的文件. 爲了使代碼方便與其餘項目共享, 請避免顯式包含此文件 (除了在 precompile.cc 中), 使用 /FI 編譯器選項以自動包含該文件.
  • 資源頭文件一般命名爲 resource.h 且只包含宏, 這一文件不須要遵照本風格指南.

除非第三方庫用到CString,不容許使用CString等MFC不能跨平臺的類和函數,咱們的代碼必須考慮可移植性。

總結

指針形參須要判空 函數用到句柄須要判斷是否有效 函數只能有一個return(大衆) 基類析構須要寫成虛函數,子類重寫須要添加虛關鍵字 delete以後要置空 NULL和nullptr取決於項目規範,c++11用nullptr svn的提交message須要在代碼中註釋 註釋修改記錄

相關文章
相關標籤/搜索