C++編程規范


參考於知乎某用戶

一、格式

1、每行代碼不多於80個字符;

2、使用空格,而不是制表符(Tab)來縮進,每次縮進4個字符;

3、指針符號*,引用符號&寫在靠近類型的位置;

4、花括號的位置,使用Allman風格,另起一行,代碼會更清晰;

for (auto i = 0; i < 100; i++)
{
    printf("%d\n", i);
}

5、if、for、while等語句就算只有一行,也要強制使用花括號;

//永遠不要省略花括號
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
    goto fail;

//需要寫成:
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
{
    goto fail;
}

二、命名約定

1、使用英文單詞,不要夾雜拼音;

2、總體上使用駝峰命名法;

3、名字前不要加上類型前綴;

bool          bEmpty;
const char*   szName;
Array         arrTeachers;
//不提倡這種做法。變量名字應該關注用途,而不是它的類型。上面名字應該修改為:
bool        isEmpty;
const char* name;
Array       teachers;

4、類型命名

      類型命名采用大寫的駱駝命名法,每個單詞以大寫字母開頭,不包含下划線。比如

GameObject
TextureSheet

5、變量命名

5.1、普通變量名字

變量名字采用小寫的駱駝命名法。比如:

std::string tableName;
CCRect      shapeBounds;

變量的名字,假如作用域越長,就越要描述詳細。作用域越短,適當簡短一點。

5.2、類成員變量

成員變量,訪問權限只分成兩級,private 和 public,不要用 protected。 私有的成員變量,前面加下划線。比如:

class Image
{
public:
    .....

private:
    size_t    _width;
    size_t    _height;
}

public 的成員變量,通常會出現在 C 風格的 struct 中,前面不用加下划線。比如:

struct Color4f
{
    float    red;
    float    green;
    float    blue;
    float    alpha;
}

5.3、靜態成員

類中盡量不要出現靜態變量。類中的靜態變量不用加任何前綴。文件中的靜態變量統一加s_前綴,並盡可能的詳細命名。比如

static ColorTransformStack s_colorTransformStack;    //
static ColorTransformStack s_stack;                  // 錯(太簡略)

5.4、全局變量

不要使用全局變量。真的沒有辦法,加上前綴 g_,並盡可能的詳細命名。比如

Document  g_currentDocument;

6、函數命名

變量名稱采用小寫的駝峰命名法,eg: playMusic;

函數名,整體上,應該是個動詞,或者是形容詞(返回bool的函數),但不要是名詞。

teacherNames();        // 錯(這個是總體是名詞)
getTeacherNames();     //

無論是全局函數,靜態函數,私有的成員函數,都不強制加前綴。

7、命名空間

命名空間名字,使用小寫加下划線的形式;

namespace lua_wrapper;

使用小寫加下划線,而不要使用駱駝命名法。可以方便跟類型名字區分開來。比如

lua_wrapper::getField();  // getField是命令空間lua_wrapper的函數
LuaWrapper::getField();   // getField是類型LuaWrapper的靜態函數

8、宏命名

不建議使用宏,除非真的需要。宏的名字全部大寫,中間使用下划線相連。

頭文件的防御宏定義

#ifndef __COCOS2D_FLASDK_H__
#define __COCOS2D_FLASDK_H__

....

#endif

9、枚舉命名

盡量使用 0x11 風格 enum,例如:

enum class ColorType : uint8_t
{
    Black,
    While,
    Red,
}

枚舉里面的數值,全部采用大寫的駱駝命名法。使用的時候,就為 ColorType::Black

有些時候,需要使用0x11之前的enum風格,這種情況下,每個枚舉值,都需要帶上類型信息,用下划線分割。比如

enum HttpResult
{
    HttpResult_OK     = 0,
    HttpResult_Error  = 1,
    HttpResult_Cancel = 2,
}

10、純C風格的接口

假如我們需要結構里面的內存布局精確可控,有可能需要編寫一些純C風格的結構和接口。這個時候,接口前面應該帶有模塊或者結構的名字,中間用下划線分割。比如

struct HSBColor
{
    float h;
    float s;
    float b;
};

struct RGBColor
{
    float r;
    float g;
    float b;
}

RGBColor color_hsbToRgb(HSBColor hsb);
HSBColor color_rgbToHsb(RGBColor rgb);

這里,color 就是模塊的名字。這里的模塊,充當 C++ 中命名空間的作用。

11、代碼文件、路徑命名

代碼名跟類名一樣,采用大寫駝峰命名法;

12、命名避免帶有個人標簽


 

三、代碼文件

1、#define保護

所有的頭文件,都應該使用#define來防止頭文件被重復包含。命名的格式為:

__<模塊>_<文件名>_H__

很多時候,模塊名字都跟命名空間對應。比如

#ifndef __GEO_POINT_H__
#define __GEO_POINT_H__

namespace geo
{
    class Point
    {
        .....
    };
}

#endif

2、#include的順序

C++代碼使用#include來引入其它的模塊的頭文件。盡可能,按照模塊的穩定性順序來排列#include的順序。按照穩定性從高到低排列。比如:

#include <map>
#include <vector>
#include <boost/noncopyable.hpp>
#include "cocos2d.h"
#include "json.h"
#include "FlaSDK.h"
#include "support/TimeUtils.h"
#include "Test.h"

上面例子中。#include的順序,分別是C++標准庫,boost庫,第三方庫,我們自己寫的跟工程無關的庫,工程中比較基礎的庫,應用層面的文件。

但有一個例外,就是 .cpp中,對應的.h文件放在第一位。比如geo模塊中的, Point.h 跟 Point.cpp文件,Point.cpp中的包含

#include "geo/Point.h"
#include <cmath>

這里,將 #include "geo/Point.h",放到第一位,之后按照上述原則來排列#include順序。理由下一條規范來描述。

3、盡可能減少對頭文件的依賴

代碼文件中,每多出現一次#include包含, 就會多一層依賴。比如,有A,B類型,各自有對應的.h文件和.cpp文件。

當A.cpp包含了A.h, A.cpp就依賴了A.h,當A.h被修改的時候,A.cpp就需要重修編譯。

若B.cpp 包含了B.h, B.h包含了A.h, 這個時候。B.cpp雖然沒有直接包含A.h, 但也間接依賴於A.h。當A.h修改了,B.cpp也需要重修編譯。

當在頭文件中,出現不必要的包含,就會生成不必要的依賴,引起連鎖反應,使得編譯時間大大被拉長。

使用前置聲明,而不是直接#include,可以顯著地減少依賴數量。

具體實踐方法見:原文

5、#include中的頭文件,盡量使用全路徑,或者相對路徑

路徑的起始點,為工程文件代碼文件的根目錄。

#include "ui/home/HomeLayer.h"
#include "ui/home/HomeCell.h"
#include "support/MathUtils.h"

不要直接包含:

#include "HomeLayer.h"
#include "HomeCell.h"
#include "MathUtils.h"

也可以使用相對路徑。比如

#include "../MathUtil.h"
#include "./home/HomeCell.h"

四、作用域

作用域,表示某段代碼或者數據的生效范圍。作用域越大,修改代碼時候影響區域也就越大,原則上,作用域越小越好。

1、全局變量

禁止使用全局變量。全局變量在項目的任何地方都可以訪問。兩個看起來沒有關系的函數,一旦訪問了全局變量,就會產生無形的依賴。使用全局變量,基本上都是怕麻煩,貪圖方便。比如:

funA -> funB -> funC -> funD

上圖表示調用順序。當funD需要用到funA中的某個數據。正確的方式,是將數據一層層往下傳遞。但因為這樣做,需要修改幾個地方,修改的人怕麻煩,直接定義出全局變量。這樣做,當然是可以快速fix bug。但funA跟funD就引入無形的依賴,從接口處看不出來。

單件可以看做全局變量的變種。最優先的方式,應該將數據從接口中傳遞,其次封裝單件,再次使用函數操作靜態數據,最糟糕就是使用全局變量。

若真需要使用全局變量。變量使用g_開頭。

2、類的成員變量

類的成員變量,只能夠是private或者public, 不要設置成protected。protected的數據看似安全,實際只是一種錯覺。

數據只能通過接口來修改訪問,不要直接訪問。這樣的話,在接口中設置個斷點就可以調試知道什么時候數據被修改。另外改變類的內部數據表示,也可以維持接口的不變,而不影響全局。

絕大多數情況,數據都應該設置成私有private, 變量加 _前綴。比如:

class Data
{
private:
    const uint8_t*  _bytes;
    size_t          _size;
}

公有的數據,通常出現在C風格的結構中,或者一些數據比較簡單,並很常用的類,public數據不要加前綴。

class Point
{
public:
    Point(float x_, float y_) : x(x_), y(y_)
    {
    }
    .....
    float x;
    float y;
}

注意,我們在構造函數,使用 x_ 的方式表示傳入的參數,防止跟 x 來重名。

3、局部變量

局部變量真正需要使用的時候才定義,一行定義一個變量,並且一開始就給它一個合適的初始值。

(在函數最前面定義變量,變量就在整個函數都可見,作用域越大,就越容易被誤修改。)

4、命名空間

C++中,盡量不要出現全局函數,應該放入某個命名空間當中。命名空間將全局的作用域細分,可有效防止全局作用域的名字沖突。

比如:

namespace json
{
    class Value
    {
        ....
    }
}

namespace splite
{
    class Value
    {
        ...
    }
}

兩個命名空間都出現了Value類。外部訪問時候,使用 json::Value, splite::Value來區分。

5、文件作用域

詳見原文

6、頭文件中不要出現using namespace ...

頭文件,很可能被多個文件包含。當某個頭文件出現了 using namespace ... 的字樣,所有包含這個頭文件的文件,都簡直看到此命令空間的全部內容,就有可能引起沖突。

// Test.h
#include <string>
using namespace std;

class Test
{
public:
    Test(const string& name);
};

這個時候,只要包含了Test.h, 就都看到std的所有內容。正確的做法,是頭文件中,將命令空間寫全。將 string, 寫成 std::string, 這里不要偷懶。


 

五、類

1、讓類的接口盡可能小

設計類的接口時,不要想着接口以后可能有用就先加上,而應該想着接口現在沒有必要,就直接去掉。這里的接口,你可以當成類的成員函數。添加接口是很容易的,但是修改,去掉接口會會影響較大。

接口小,不單指成員函數的數量少,也指函數的作用域盡可能小。

比如,

class Test
{
public:
    void funA();
    void funB();
    void funC();
    void funD();
};

假如,funD 其實是可以使用 funA, funB, funC 來實現的。這個時候,funD,就不應該放到Test里面。可以將funD抽取出來。funD 只是一個封裝函數,而不是最核心的。

void Test_funD(Test* test);

編寫類的函數時候,一些輔助函數,優先采用 Test_funD 這樣的方式,將其放到.cpp中,使用匿名空間保護起來,外界就就不用知道此函數的存在,那些都只是實現細節。

當不能抽取獨立於類的輔助函數,先將函數,變成private, 有必要再慢慢將其提出到public。 不要覺得這函數可能有用,一下子就寫上一堆共有接口。

再強調一次,如無必要,不要加接口。

從作用域大小,來看

  • 獨立於類的函數,比類的成員函數要好
  • 私有函數,比共有函數要好
  • 非虛函數,比虛函數要好

2、聲明順序

類的成員函數或者成員變量,按照使用的重要程度,從高到低來排列。

比如,使用類的時候,用戶更關注函數,而不是數據,所以成員函數應該放到成員變量之前。 再比如,使用類的時候,用戶更關注共有函數,而不是私有函數,所以public,應該放在private前面。

具體規范

  • 按照 public, protected, private 的順序分塊。那一塊沒有,就直接忽略。

每一塊中,按照下面順序排列

  • typedef,enum,struct,class 定義的嵌套類型
  • 常量
  • 構造函數
  • 析構函數
  • 成員函數,含靜態成員函數
  • 數據成員,含靜態數據成員

.cpp 文件中,函數的實現盡可能給聲明次序一致。

3、繼承

優先使用組合,而不是繼承。

繼承主要用於兩種場合:實現繼承,子類繼承了父類的實現代碼。接口繼承,子類僅僅繼承父類的方法名稱。

我們不提倡實現繼承,實現繼承的代碼分散在子類跟父親當中,理解起來變得很困難。通常實現繼承都可以采用組合來替代。

規則:

  • 繼承應該都是 public
  • 假如父類有虛函數,父類的析構函數為 virtual
  • 假如子類覆寫了父類的虛函數,應該顯式寫上 override

 比如:

// swf/Definition.h
class Definition
{
public:
    virtual ~Definition()   {}
    virtual void parse(const uint8_t* bytes, size_t len) = 0;
};

// swf/ShapeDefinition.h
class ShapeDefinition : public Definition
{
public:
    ShapeDefinition()   {}
    virtual void parse(const uint8_t* bytes, size_t len) override;

private:
    Shape   _shape;
};
Definition* p = new ShapeDefinition();
....
delete p;

上面的例子,使用父類的指針指向子類,假如父類的析構函數不為virtual, 就只會調用父類的Definition的釋放函數,引起子類獨有的數據不能釋放。所有需要加上virtual。

另外子類覆寫的虛函數寫上,override的時候,當父類修改了虛函數的名字,就會編譯錯誤。從而防止,父類修改了虛函數接口,而忘記修改子類相應虛函數接口的情況。


 

六、函數

1、編寫短小的函數

函數盡可能的短小,凝聚,功能單一。

只要某段代碼,可以用某句話來描述,盡可能將這代碼抽取出來,作為獨立的函數,就算那代碼只有一行。最典型的就是C++中的max, 實現只有一句話。

template <typename T>
inline T max(T a, T b)
{
    return a > b ? a : b;
}
  • 將一段代碼抽取出來,作為一個整體,一個抽象,就不用糾結在細節之中。
  • 將一個長函數,切割成多個短小的函數。每個函數中使用的局部變量,作用域也會變小。
  • 短小的函數,更容易復用,從一個文件搬到另一個文件也會更容易。
  • 短小的函數,因為內存局部性,運行起來通常會更快。
  • 小的函數,也容易閱讀,調試。

2、函數的參數盡可能少,原則上不超過5個

3、函數參數順序

參數順序按照傳入參數,傳出參數的順序排列

4、函數的傳出參數,使用指針,而不要使用引用

比如:

bool loadFile(const std::string& filePath, ErrorCode* code);  //
bool loadfile(const std::string& filePath, ErrorCode& code);  //

因為當使用引用的時候,使用函數的時候會變成

ErrorCode code;
if (loadFile(filePath, code))
{
    ...
}

而使用指針,調用的時候,會是

ErrorCode code;
if (loadFile(filePath, &code))
{
    ...
}

這樣從,&code的方式可以很明顯的區分,傳入,傳出參數。試比較

doFun(arg0, arg1, arg2);    //
doFun(arg0, &arg1, &arg2);  //


七、其他

1、const

建議,盡可能多使用const

C++中,const是個很重要的關鍵字,應用了const之后,就不可以隨便改變變量的數值了,不小心改變了編譯器會報錯,就容易找到錯誤的地方。只要你覺得有不變的地方,就用const來修飾;

2、不要注釋代碼,代碼不使用就直接刪掉

要想看之前的代碼,可以通過版本控制工具;

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM