一、格式
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、不要注釋代碼,代碼不使用就直接刪掉
要想看之前的代碼,可以通過版本控制工具;
