引言
C++ 是一門十分復(fù)雜并且威力強(qiáng)大的語(yǔ)言鹿蜀,使用這門語(yǔ)言的時(shí)候我們應(yīng)該有所節(jié)制,絕對(duì)的自由意味著混亂惨好。
1.規(guī)范的作用:我十分清楚每個(gè)人對(duì)怎么編寫代碼都有自己的偏好埠巨。這里定下的規(guī)范,某些地方可能會(huì)跟個(gè)人原來(lái)熟悉的習(xí)慣相違背寓盗,并引起不滿。但多人協(xié)作的時(shí)候璧函,需要有一定規(guī)范贞让。定下一些規(guī)范,當(dāng)大家面對(duì)某些情況柳譬,有所分歧的時(shí)候喳张,容易達(dá)成共識(shí)。另外通過(guò)一定規(guī)范美澳,加強(qiáng)代碼的一致性销部,從團(tuán)隊(duì)中某人的代碼切換到另一個(gè)人的代碼,會(huì)更為自然制跟,讓別人可以讀懂你的代碼是很重要的舅桩。
2.這里規(guī)范是死的雨膨,現(xiàn)實(shí)是多變的擂涛,當(dāng)你覺(jué)得某些規(guī)范,對(duì)你需要解決問(wèn)題反而有很大限制聊记,可以違反撒妈,但要有理由恢暖,而不僅僅是借口。那到底是否在尋找借口狰右,并沒(méi)有很明確的判斷標(biāo)準(zhǔn)杰捂。這就如同不能規(guī)定少于多少根頭發(fā)為禿頭,但當(dāng)我們看到某個(gè)人的時(shí)候棋蚌,自然能夠判斷他是否是禿頭嫁佳。同樣,當(dāng)我們碰到具體情況的時(shí)候谷暮,自然能夠判斷是否在尋找借口蒿往。
3.本規(guī)范編寫過(guò)程中,大量參考了《Google C++ 編程規(guī)范》湿弦,Google那份規(guī)范十分好熄浓,建議大家對(duì)比著看。
1 格式
1.1 每行代碼不多于 80 個(gè)字符
從前的電腦終端省撑,每行只可以顯示 80 個(gè)字符《拿铮現(xiàn)在有更大更寬的顯示屏,很多人會(huì)認(rèn)為這條規(guī)則已經(jīng)沒(méi)有必要竟秫。但我們有充分的理由:
版本控制軟件娃惯,或者編碼過(guò)程中,經(jīng)常需要在同一顯示屏幕上肥败,左右并排對(duì)比新舊兩個(gè)文件趾浅。80 個(gè)字符的限制,使得兩個(gè)文件都不會(huì)折行馒稍,對(duì)比起來(lái)更清晰皿哨。
當(dāng)代碼超過(guò) 3 層嵌套,代碼行就很容易超過(guò) 80 個(gè)字符纽谒。這條規(guī)則防止我們嵌套太多層級(jí)证膨,層級(jí)嵌套太深會(huì)使得代碼難以讀懂。
規(guī)則總會(huì)有例外鼓黔。比如當(dāng)你有些代碼行央勒,是82個(gè)字符,假如我們強(qiáng)制規(guī)定少于80字符澳化,人為將一行容易讀的代碼拆分成兩行代碼崔步,就太不人性化了。
我們可以適當(dāng)超過(guò)這個(gè)限制缎谷。
1.2 使用空格(Space)井濒,而不是制表符(Tab)來(lái)縮進(jìn),每次縮進(jìn)4個(gè)字符
代碼編輯器,基本都可以設(shè)置將Tab轉(zhuǎn)為空格瑞你,請(qǐng)打開(kāi)這個(gè)設(shè)置酪惭。
制表符在每個(gè)軟件中的顯示,都會(huì)有所不同捏悬。有些軟件中每個(gè)Tab縮進(jìn)8個(gè)字符撞蚕,有些軟件每個(gè)Tab縮進(jìn)4個(gè)字符润梯,隨著個(gè)人的設(shè)置不同而不同过牙。只使用空格來(lái)縮進(jìn),保證團(tuán)隊(duì)中每個(gè)人纺铭,看同一份代碼寇钉,格式不會(huì)亂掉。
1.3 指針?lè)?hào)*舶赔,引用符號(hào)& 的位置扫倡,寫在靠近類型的地方
對(duì)比兩種寫法, 寫成第(1)種。
CCNode* p = CCNode::create(); // (1)
CCNode *p = CCNode::create(); // (2)
我知道這個(gè)規(guī)定有很大的爭(zhēng)議竟纳。指針?lè)?hào)到底靠近類型撵溃,還是靠近變量,這爭(zhēng)論一直沒(méi)有停過(guò)锥累。其實(shí)兩種寫法都沒(méi)有什么大問(wèn)題缘挑,關(guān)鍵是統(tǒng)一。經(jīng)考慮桶略,感覺(jué)第1種寫法更統(tǒng)一更合理语淘。理由:
在類中連續(xù)寫多個(gè)變量,通常會(huì)用 Tab 將變量對(duì)齊际歼。( Tab 會(huì)轉(zhuǎn)化成空格)惶翻。比如
CCNode* _a;
CCNode _b;
int _c;
當(dāng)星號(hào)靠近類型而不是變量。_a, _b, _c 等變量會(huì)很自然對(duì)齊鹅心。
而當(dāng)星號(hào)靠近變量吕粗,如果不手動(dòng)多按空格微調(diào),會(huì)寫成旭愧。
CCNode *_a;
CCNode _b;
int _c;
指針?lè)?hào)靠近類型溯泣,語(yǔ)法上更加統(tǒng)一。比如
const char* getTableName();
static_cast<CCLayer*>(node);
反對(duì)第一種寫法的理由通常是:
假如某人連續(xù)定義多個(gè)變量榕茧,就會(huì)出錯(cuò)垃沦。
int* a, b, c;
上面寫法本身就有問(wèn)題。指針應(yīng)該每行定義一個(gè)變量, 并初始化用押。
int* a = nullptr;
int* b = nullptr;
int* c = nullptr;
1.4 花括號(hào)位置
采用Allman風(fēng)格肢簿,if, for, while,namespace, 命名空間等等的花括號(hào),另起一行池充。例子
for (auto i = 0; i < 100; i++)
{
? ? printf("%d\n", i);
}
這條規(guī)定桩引,很可能又引起爭(zhēng)議。很多人采用 K&R 風(fēng)格收夸,將上面代碼寫成
for (auto i = 0; i < 100; i++) {
? ? printf("%d\n", i);
}
1.5 if, for, while等語(yǔ)句就算只有一行坑匠,也強(qiáng)制使用花括號(hào)
永遠(yuǎn)不要省略花括號(hào),不要寫成:
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
? ? goto fail;
需要寫成:
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
{
? ? goto fail;
}
省略花括號(hào)卧惜,以后修改代碼厘灼,或者代碼合并的時(shí)候,容易直接多寫一行咽瓷。如
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
? ? goto fail;
? ? goto fail;
就會(huì)引起錯(cuò)誤设凹。
2 命名約定
2.1 使用英文單詞,不能夾著拼音
這條規(guī)則強(qiáng)制執(zhí)行茅姜,不能有例外闪朱。
2.2 總體上采用駱駝命名法
單詞與單詞之間,使用大小寫相隔的方式分開(kāi)钻洒,中間不包含下劃線奋姿。比如
TimerManager? // (1)
playMusic? ? // (2)
其中(1)為大寫的駱駝命名法,(2)為小寫的駱駝命名法素标。
不要使用
timer_manager
play_music
這種小寫加下劃線的方式在 boost 庫(kù)称诗,C++ 標(biāo)準(zhǔn)庫(kù)中,用得很普遍糯钙。
2.3 名字不要加類型前綴
有些代碼庫(kù)粪狼,會(huì)在變量名字前面加上類型前綴。比如 b表示 bool, i 表示 int , arr 表示數(shù)組, sz 表示字符串等等任岸。他們會(huì)命名為
bool? ? ? ? ? bEmpty;
const char*? szName;
Array? ? ? ? arrTeachers;
我們不提倡這種做法再榄。變量名字應(yīng)該關(guān)注用途,而不是它的類型享潜。上面名字應(yīng)該修改為
bool? ? ? ? isEmpty;
const char* name;
Array? ? ? teachers;
注意困鸥,我們將 bool 類型添加上is。isEmpty, isOK, isDoorOpened剑按,等等疾就,讀起來(lái)就是一個(gè)詢問(wèn)句。
2.4 類型命名
類型命名采用大寫的駱駝命名法艺蝴,每個(gè)單詞以大寫字母開(kāi)頭猬腰,不包含下劃線。比如
GameObject
TextureSheet
類型的名字猜敢,應(yīng)該帶有描述性姑荷,是名詞盒延,而不要是動(dòng)詞鼠冕。盡量避開(kāi)Data, Info, Manager 這類的比較模糊的字眼懈费。(但我知道有時(shí)也真的避免不了憎乙,看著辦胶坠。)
所有的類型,class, struct, typedef, enum, 都使用相同的約定椭蹄。例如
class UrlTable
struct UrlTableProperties
typedef hash_map<UrlTableProperties*, std::string> PropertiesMap;
enum UrlTableError
2.5 變量命名
2.5.1 普通變量名字
變量名字采用小寫的駱駝命名法绳矩。比如
std::string tableName;
CCRect? ? ? shapeBounds;
變量的名字翼馆,假如作用域越長(zhǎng),就越要描述詳細(xì)中姜。作用域越短丢胚,適當(dāng)簡(jiǎn)短一點(diǎn)携龟。比如
for (auto& name : _studentNames)
{
? ? std::cout << name << std::endl;
}
for (size_t i = 0; i < arraySize; i++)
{
? ? array[i] = 1.0;
}
名字清晰峡蟋,并且盡可能簡(jiǎn)短层亿。
2.5.2 類成員變量
成員變量方灾,訪問(wèn)權(quán)限只分成兩級(jí)裕偿,private 和 public嘿棘,不要用 protected鸟妙。 私有的成員變量,前面加下劃線房午。比如:
class Image
{
public:
? ? .....
private:
? ? size_t? ? _width;
? ? size_t? ? _height;
}
public 的成員變量郭厌,通常會(huì)出現(xiàn)在 C 風(fēng)格的 struct 中,前面不用加下劃線液走。比如:
struct Color4f
{
? ? float? ? red;
? ? float? ? green;
? ? float? ? blue;
? ? float? ? alpha;
}
2.5.3 靜態(tài)變量
類中盡量不要出現(xiàn)靜態(tài)變量缘眶。類中的靜態(tài)變量不用加任何前綴。文件中的靜態(tài)變量統(tǒng)一加s_前綴顶燕,并盡可能的詳細(xì)命名涌攻。比如
static ColorTransformStack s_colorTransformStack;? ? // 對(duì)
static ColorTransformStack s_stack;? ? ? ? ? ? ? ? ? // 錯(cuò)(太簡(jiǎn)略)
2.5.4 全局變量
不要使用全局變量芝此。真的沒(méi)有辦法婚苹,加上前綴 g_,并盡可能的詳細(xì)命名廓译。比如
Document? g_currentDocument;
2.6 函數(shù)命名
變量名字采用小寫的駱駝命名法糟港。比如
playMusic
getSize
isEmpty
函數(shù)名字速和。整體上颠放,應(yīng)該是個(gè)動(dòng)詞,或者是形容詞(返回bool的函數(shù))欲低,但不要是名詞。
teacherNames();? ? ? ? // 錯(cuò)(這個(gè)是總體是名詞)
getTeacherNames();? ? // 對(duì)
無(wú)論是全局函數(shù)腊瑟,靜態(tài)函數(shù)膘格,私有的成員函數(shù),都不強(qiáng)制加前綴政敢。但有時(shí)靜態(tài)函數(shù)喷户,可以適當(dāng)加s_前綴。
類的成員函數(shù),假如類名已經(jīng)出現(xiàn)了某種信息璃谨,就不用重復(fù)寫了。比如
class UserQueue
{
public:
? ? size_t getQueueSize();? ? // 錯(cuò)(類名已經(jīng)為Queue了,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // 這里再命名為getQueueSize就無(wú)意義)
? ? size_t getSize();? ? ? ? // 對(duì)
}
2.7 命名空間
命令空間的名字,使用小寫加下劃線的形式阱冶,比如
namespace lua_wrapper;
使用小寫加下劃線熙揍,而不要使用駱駝命名法届囚。可以方便跟類型名字區(qū)分開(kāi)來(lái)泥耀。比如
lua_wrapper::getField();? // getField是命令空間lua_wrapper的函數(shù)
LuaWrapper::getField();? // getField是類型LuaWrapper的靜態(tài)函數(shù)
2.8 宏命名
不建議使用宏迎瞧,但真的需要使用。宏的名字,全部大寫,中間加下劃線相連接壮吩。這樣可以讓宏更顯眼一些生百。比如
#define PI_ROUNDED 3.0
CLOVER_TEST
MAX
MIN
頭文件出現(xiàn)的防御宏定義搜吧,也全部大寫,比如:
#ifndef __COCOS2D_FLASDK_H__
#define __COCOS2D_FLASDK_H__
....
#endif
不要寫成這樣:
#ifndef __cocos2d_flashsdk_h__
#define __cocos2d_flashsdk_h__
....
#endif
2.9 枚舉命名
盡量使用 0x11 風(fēng)格 enum伺帘,例如:
enum class ColorType : uint8_t
{
? ? Black,
? ? While,
? ? Red,
}
枚舉里面的數(shù)值偶垮,全部采用大寫的駱駝命名法。使用的時(shí)候,就為 ColorType::Black
有些時(shí)候,需要使用0x11之前的enum風(fēng)格,這種情況下,每個(gè)枚舉值,都需要帶上類型信息,用下劃線分割。比如
enum HttpResult
{
? ? HttpResult_OK? ? = 0,
? ? HttpResult_Error? = 1,
? ? HttpResult_Cancel = 2,
}
2.10 純 C 風(fēng)格的接口
假如我們需要結(jié)構(gòu)里面的內(nèi)存布局精確可控,有可能需要編寫一些純C風(fēng)格的結(jié)構(gòu)和接口。這個(gè)時(shí)候,接口前面應(yīng)該帶有模塊或者結(jié)構(gòu)的名字,中間用下劃線分割。比如
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 就是模塊的名字猖腕。這里的模塊,充當(dāng) C++ 中命名空間的作用。
struct Path
{
? ? ....
}
Path* Path_new();
void? Path_destrory(Path* path);
void? Path_moveTo(Path* path, float x, float y);
void? Path_lineTo(Path* path, float x, float y);
這里镜廉,接口中Path出現(xiàn)的是類的名字。
2.11 代碼文件敢茁,路徑命名
代碼文件的名字,應(yīng)該反應(yīng)出此代碼單元的作用碉哑。
比如 Point.h, Point.cpp贮尖,實(shí)現(xiàn)了class Point;
當(dāng) class Point关斜,的名字修改成裁着,Point2d, 代碼文件名字,就應(yīng)該修改成 Point2d.h, Point2d.cpp矗积。代碼文件名字,跟類型名字一樣百匆,采用大寫的駱駝命名法有巧。
路徑名字甜橱,對(duì)于于模塊的名字乘瓤。跟上一章的命名規(guī)范一樣归薛,采用小寫加下劃線的形式。比如
ui/home/HomeLayer.h
ui/battle/BattleCell.h
support/geo/Point.h
support/easy_lua/Call.h
路徑以及代碼文件名屋厘,不能出現(xiàn)空格瞻凤,中文所刀,不能夾著拼音溜族。假如隨著代碼的修改,引起模塊名乡话,類型名字的變化摧玫,應(yīng)該同時(shí)修改文件名跟路徑名。
2.12 命名避免帶有個(gè)人標(biāo)簽
比如绑青,不要將某個(gè)模塊名字為
HJCPoint
hjc/Label.h
hjc為團(tuán)隊(duì)某人名字的縮寫诬像。
項(xiàng)目歸全體成員所有,任何人都有權(quán)利跟義務(wù)整理修改工程代碼闸婴。當(dāng)某樣?xùn)|西打上個(gè)人標(biāo)記坏挠,就傾向?qū)⑵渥鳛樗接小F渌司蜁?huì)覺(jué)得那代碼亂不關(guān)自己事情邪乍,自己就不情愿別人來(lái)動(dòng)自己東西降狠。
當(dāng)然了对竣,文件開(kāi)始注釋可以出現(xiàn)創(chuàng)建者的名字,信息榜配。只是類型否纬,模塊,函數(shù)名字蛋褥,等容易在工程中散開(kāi)的東西不提倡临燃。個(gè)人項(xiàng)目可以忽略這條。
再?gòu)?qiáng)調(diào)一下烙心,任何人都有權(quán)利跟義務(wù)整理修改他人代碼膜廊,只要你覺(jué)得你修改得合理,但不要自作聰明淫茵。我知道有些程序員爪瓜,會(huì)覺(jué)得他人修改自己代碼,就是入侵自己領(lǐng)土匙瘪。
2.13 例外
有些時(shí)候铆铆,我們需要自己寫的庫(kù)跟C++的標(biāo)準(zhǔn)庫(kù)結(jié)合。這時(shí)候可以采用跟C++標(biāo)準(zhǔn)庫(kù)相類似的風(fēng)格辆苔。比如
class MyArray
{
public:
? ? typedef const char* const_iteator;
? ? ...
? ? const char* begin() const;
? ? const char* rbegin() const;
}
3 代碼文件
3.1 #define 保護(hù)
所有的頭文件算灸,都應(yīng)該使用#define來(lái)防止頭文件被重復(fù)包含。命名的格式為
__<模塊>_<文件名>_H__
很多時(shí)候驻啤,模塊名字都跟命名空間對(duì)應(yīng)菲驴。比如
#ifndef __GEO_POINT_H__
#define __GEO_POINT_H__
namespace geo
{
? ? class Point
? ? {
? ? ? ? .....
? ? };
}
#endif
并且,#define宏骑冗,的名字全部都為大寫赊瞬。不要出現(xiàn)大小寫混雜的形式。
3.2 #include 的順序
C++代碼使用#include來(lái)引入其它的模塊的頭文件贼涩。盡可能巧涧,按照模塊的穩(wěn)定性順序來(lái)排列#include的順序。按照穩(wěn)定性從高到低排列遥倦。
比如
#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++標(biāo)準(zhǔn)庫(kù)袒哥,boost庫(kù)缩筛,第三方庫(kù),我們自己寫的跟工程無(wú)關(guān)的庫(kù)堡称,工程中比較基礎(chǔ)的庫(kù)瞎抛,應(yīng)用層面的文件。
但有一個(gè)例外却紧,就是 .cpp中桐臊,對(duì)應(yīng)的.h文件放在第一位胎撤。比如geo模塊中的, Point.h 跟 Point.cpp文件,Point.cpp中的包含
#include "geo/Point.h"
#include <cmath>
這里断凶,將 #include "geo/Point.h"伤提,放到第一位,之后按照上述原則來(lái)排列#include順序懒浮。理由下一條規(guī)范來(lái)描述飘弧。
3.3 盡可能減少頭文件的依賴
代碼文件中,每出現(xiàn)一次#include包含, 就會(huì)多一層依賴砚著。比如,有A痴昧,B類型稽穆,各自有對(duì)應(yīng)的.h文件和.cpp文件。
當(dāng)A.cpp包含了A.h, A.cpp就依賴了A.h赶撰,我們表示為
A.cpp -> A.h
這樣舌镶,當(dāng)A.h被修改的時(shí)候,A.cpp就需要重修編譯豪娜。 假設(shè)
B.cpp -> B.h
B.h? -> A.h
這表示餐胀,B.cpp 包含了B.h, B.h包含了A.h, 這個(gè)時(shí)候。B.cpp雖然沒(méi)有直接包含A.h, 但也間接依賴于A.h瘤载。當(dāng)A.h修改了否灾,B.cpp也需要重修編譯。
當(dāng)在頭文件中鸣奔,出現(xiàn)不必要的包含墨技,就會(huì)生成不必要的依賴,引起連鎖反應(yīng)挎狸,使得編譯時(shí)間大大被拉長(zhǎng)扣汪。
使用前置聲明,而不是直接#include锨匆,可以顯著地減少依賴數(shù)量崭别。實(shí)踐方法:
3.3.1 頭文件第一位包含
比如寫類A,有文件 A.h, 和A.cpp 那么在A.cpp中恐锣,將A.h的包含寫在第一位茅主。在A.cpp中寫成
// 前面沒(méi)有別的頭文件包含
#include "A.h"
#include <string>
#include .......
.... 包含其它頭文件
之后可以嘗試在 A.h 中去掉多余的頭文件。當(dāng)A.cpp可以順利編譯通過(guò)的時(shí)候侥蒙,A.h包含的頭文件就是過(guò)多或者剛剛好的暗膜。而不會(huì)是包含不夠的。
3.3.2 前置聲明
首先鞭衩,只在頭文件中使用引用或者指針学搜,而不是使用值的娃善,可以前置聲明。而不是直接包含它的頭文件瑞佩。 比如
class Test : public Base
{
public:
? ? void funA(const A& a);
? ? void funB(const B* b);
? ? void funC(const space::C& c);
private:
? ? D? _d;
};
這里聚磺,我牽涉到幾個(gè)其它類,Base, A, B, space::C(C 在命名空間space里面), D炬丸。Base和D需要知道值瘫寝,A, B, space::C只是引用和指針。所以Base, C的頭文件需要包含稠炬。A, B焕阿,space::C只需要前置聲明。
#include "Base.h"
#include "D.h"
namespace space
{
? ? class C;
}
class A;
class B;
class Test : public Base
{
public:
? ? void funA(const A& a);
? ? void funB(const B* b);
? ? void funC(const space::C& c);
private:
? ? D? _d;
};
注意命名空間里面的寫法首启。
3.3.3 impl 手法
就是類里面包含實(shí)現(xiàn)類的指針暮屡。在cpp里面實(shí)現(xiàn)。
3.3.4 盡可能將代碼拆分成相對(duì)獨(dú)立的毅桃,粒度小的單元褒纲,放到不同的文件中
簡(jiǎn)單說(shuō),就是不要將所有東西都塞在一起钥飞。這樣的代碼組積相對(duì)清晰莺掠。頭文件包含也相對(duì)較少。但現(xiàn)實(shí)中读宙,或多或少會(huì)違反彻秆。
比如,工程用到一些常量字符串(或者消息定義论悴,或者enum值掖棉,有多個(gè)變種)。一個(gè)似乎清晰的結(jié)構(gòu)膀估,是將字符串都放到同一個(gè)頭文件中幔亥。不過(guò)這樣一來(lái),這個(gè)字符串文件察纯,就幾乎會(huì)被所有項(xiàng)目文件包含帕棉。當(dāng)以后新加一個(gè)字符串時(shí)候,就算只加一行饼记,工程幾乎被全部編譯香伴。
更好的做法,是按照字符串的用途來(lái)分拆開(kāi)具则。
又比如即纲,有些支持庫(kù)。有時(shí)貪圖方便博肋,不注意的低斋,就會(huì)寫一個(gè) GlobalUtils.h 之類的頭文件蜂厅,包含所有支持庫(kù),因?yàn)檫@樣可以不關(guān)心到底應(yīng)該包含哪個(gè)膊畴,反正包含GlobalUtils.h就行掘猿,這樣多省事。不過(guò)這樣一來(lái)唇跨,需要加一個(gè)支持的函數(shù)稠通,比如就只是角度轉(zhuǎn)弧度的小函數(shù),也會(huì)發(fā)生連鎖編譯买猖。
更好的做法改橘,是根據(jù)需要來(lái)包含必要的文件。就算你麻煩一點(diǎn)政勃,寫10行#include的代碼唧龄,都比之后修改一行代碼,就編譯上10多分鐘要好奸远。
3.4 小結(jié)
減少編譯時(shí)間,這點(diǎn)很重要讽挟。再啰嗦一下
要減少頭文件重復(fù)包含懒叛,需要團(tuán)隊(duì)的人所有人達(dá)成共識(shí),認(rèn)識(shí)到這是不好的耽梅。很多人對(duì)這問(wèn)題認(rèn)識(shí)不夠薛窥,會(huì)被當(dāng)成小題大作。
不要貪方便眼姐。直接包含一個(gè)大的頭文件诅迷,短期是很方便,長(zhǎng)期會(huì)有麻煩众旗。
3.5 #include中的頭文件罢杉,盡量使用全路徑,或者相對(duì)路徑
路徑的起始點(diǎn)贡歧,為工程文件代碼文件的根目錄滩租。
比如
#include "ui/home/HomeLayer.h"
#include "ui/home/HomeCell.h"
#include "support/MathUtils.h"
不要直接包含
#include "HomeLayer.h"
#include "HomeCell.h"
#include "MathUtils.h"
這樣可以防止頭文件重名,比如一個(gè)第三方庫(kù)文件有可能就叫 MathUtils.h利朵。
并且移植到其它平臺(tái)律想,配置起來(lái)會(huì)更容易。比如上述例子绍弟,在安卓平臺(tái)上技即,就需要配置包含路徑
<Project_Root>/ui/home/
<Project_Root>/support/
也可以使用相對(duì)路徑。比如
#include "../MathUtil.h"
#include "./home/HomeCell.h"
這樣做樟遣,還有個(gè)好處而叼。就是只用一個(gè)簡(jiǎn)單腳本身笤,或者一些簡(jiǎn)單工具。就可以分析出頭文件的包含關(guān)系圖澈歉,然后就很容易看出循環(huán)依賴展鸡。
4 作用域
作用域,表示某段代碼或者數(shù)據(jù)的生效范圍埃难。作用域越大莹弊,修改代碼時(shí)候影響區(qū)域也就越大,原則上涡尘,作用域越小越好忍弛。
4.1 全局變量
禁止使用全局變量。全局變量在項(xiàng)目的任何地方都可以訪問(wèn)考抄。兩個(gè)看起來(lái)沒(méi)有關(guān)系的函數(shù)细疚,一旦訪問(wèn)了全局變量,就會(huì)產(chǎn)生無(wú)形的依賴川梅。使用全局變量疯兼,基本上都是怕麻煩,貪圖方便贫途。比如
funA -> funB -> funC -> funD
上圖表示調(diào)用順序吧彪。當(dāng)funD需要用到funA中的某個(gè)數(shù)據(jù)。正確的方式丢早,是將數(shù)據(jù)一層層往下傳遞姨裸。但因?yàn)檫@樣做,需要修改幾個(gè)地方怨酝,修改的人怕麻煩傀缩,直接定義出全局變量。這樣做农猬,當(dāng)然是可以快速fix bug赡艰。但funA跟funD就引入無(wú)形的依賴,從接口處看不出來(lái)盛险。
單件可以看做全局變量的變種瞄摊。最優(yōu)先的方式,應(yīng)該將數(shù)據(jù)從接口中傳遞苦掘,其次封裝單件换帜,再次使用函數(shù)操作靜態(tài)數(shù)據(jù),最糟糕就是使用全局變量鹤啡。
若真需要使用全局變量惯驼。變量使用g_開(kāi)頭。
4.2 類的成員變量
類的成員變量,只能夠是private或者public, 不要設(shè)置成protected祟牲。protected的數(shù)據(jù)看似安全隙畜,實(shí)際只是一種錯(cuò)覺(jué)。
數(shù)據(jù)只能通過(guò)接口來(lái)修改訪問(wèn)说贝,不要直接訪問(wèn)议惰。這樣的話,在接口中設(shè)置個(gè)斷點(diǎn)就可以調(diào)試知道什么時(shí)候數(shù)據(jù)被修改乡恕。另外改變類的內(nèi)部數(shù)據(jù)表示言询,也可以維持接口的不變,而不影響全局傲宜。
絕大多數(shù)情況运杭,數(shù)據(jù)都應(yīng)該設(shè)置成私有private, 變量加 _前綴。比如
class Data
{
private:
? ? const uint8_t*? _bytes;
? ? size_t? ? ? ? ? _size;
}
公有的數(shù)據(jù)函卒,通常出現(xiàn)在C風(fēng)格的結(jié)構(gòu)中辆憔,或者一些數(shù)據(jù)比較簡(jiǎn)單,并很常用的類报嵌,public數(shù)據(jù)不要加前綴虱咧。
class Point
{
public:
? ? Point(float x_, float y_) : x(x_), y(y_)
? ? {
? ? }
? ? .....
? ? float x;
? ? float y;
}
注意,我們?cè)跇?gòu)造函數(shù)锚国,使用 x_ 的方式表示傳入的參數(shù)彤钟,防止跟 x 來(lái)重名。
4.3 局部變量
局部變量盡可能使它的作用范圍最小跷叉。換句話說(shuō),就是需要使用的時(shí)候才定義营搅,而不要在函數(shù)開(kāi)始就全部定義云挟。
從前C語(yǔ)言有個(gè)約束,需要將用到的全部變量都定義在函數(shù)最前面转质。之后這個(gè)習(xí)慣也被傳到C++的代碼當(dāng)中园欣。但這種習(xí)慣是很不好的。
在函數(shù)最前面定義變量休蟹,變量就在整個(gè)函數(shù)都可見(jiàn)沸枯,作用域越大,就越容易被誤修改赂弓。
C++ 中绑榴,定義類型的變量,需要調(diào)用構(gòu)造函數(shù)盈魁,跟釋放函數(shù)翔怎。很多時(shí)候函數(shù)中途就退出了,這時(shí)候調(diào)用構(gòu)造函數(shù)和釋放函數(shù),就顯得浪費(fèi)赤套。
變量在最開(kāi)始的時(shí)候飘痛,很難給變量一個(gè)合理的初始值,很難的話容握,也就很容易忘記宣脉。
我們的結(jié)論是,局部變量真正需要使用的時(shí)候才定義剔氏,一行定義一個(gè)變量塑猖,并且一開(kāi)始就給它一個(gè)合適的初始值。
int i;
i = f();? ? // 錯(cuò)介蛉,初始化和定義分離
int j = g(); // 對(duì)萌庆,定義時(shí)候給出始值
4.4 命名空間
C++中,盡量不要出現(xiàn)全局函數(shù)币旧,應(yīng)該放入某個(gè)命名空間當(dāng)中践险。命名空間將全局的作用域細(xì)分,可有效防止全局作用域的名字沖突吹菱。
比如
namespace json
{
? ? class Value
? ? {
? ? ? ? ....
? ? }
}
namespace splite
{
? ? class Value
? ? {
? ? ? ? ...
? ? }
}
兩個(gè)命名空間都出現(xiàn)了Value類巍虫。外部訪問(wèn)時(shí)候,使用 json::Value, splite::Value來(lái)區(qū)分鳍刷。
4.5 文件作用域
假如占遥,某個(gè)函數(shù),或者類型输瓜,只在某個(gè).cpp中使用瓦胎,請(qǐng)將函數(shù)或者類放入匿名命名空間。來(lái)防止文件中的函數(shù)導(dǎo)出尤揣。比如
// fileA.cpp
namespace
{
? ? void doSomething()
? ? {
? ? ? ? ....
? ? }
}
上述例子搔啊,doSomething這個(gè)函數(shù),放入了匿名空間北戏。因此负芋,此函數(shù)限制在fileA.cpp中使用。另外的文件定義相同名字的函數(shù)嗜愈,也不會(huì)造成沖突旧蛾。
另外傳統(tǒng)C的做法,是在 doSomething 前面加 static, 比如
// fileB.cpp
static void doSomething()
{
? ? ...
}
doSomething也限制到文件fileB.cpp中蠕嫁。
同理锨天,只在文件中出現(xiàn)的類型,也放到匿名空間中拌阴。比如
// sqlite/Value.cpp
namespace sqlite
{
? ? namespace
? ? {
? ? ? ? class Record
? ? ? ? {
? ? ? ? ? ? ....
? ? ? ? }
? ? }
}
上述例子绍绘,匿名空間嵌套到sqlite空間中。這樣Record這個(gè)結(jié)構(gòu)只可以在sqlite/Value.cpp中使用,就算是同屬于空間sqlite的文件陪拘,也不知道 Record 的存在厂镇。
4.6 頭文件不要出現(xiàn) using namespace …
頭文件,很可能被多個(gè)文件包含左刽。當(dāng)某個(gè)頭文件出現(xiàn)了 using namespace ... 的字樣捺信,所有包含這個(gè)頭文件的文件,都簡(jiǎn)直看到此命令空間的全部?jī)?nèi)容欠痴,就有可能引起沖突迄靠。比如
// Test.h
#include <string>
using namespace std;
class Test
{
public:
? ? Test(const string& name);
};
這個(gè)時(shí)候,只要包含了Test.h, 就都看到std的所有內(nèi)容喇辽。正確的做法掌挚,是頭文件中,將命令空間寫全菩咨。將 string, 寫成 std::string, 這里不要偷懶吠式。
5 類
面向?qū)ο缶幊讨校愂腔镜拇a單元抽米。本節(jié)列舉了在寫一個(gè)類的時(shí)候特占,需要注意的事情。
5.1 讓類的接口盡可能小
設(shè)計(jì)類的接口時(shí)云茸,不要想著接口以后可能有用就先加上是目,而應(yīng)該想著接口現(xiàn)在沒(méi)有必要,就直接去掉标捺。
這里的接口懊纳,你可以當(dāng)成類的成員函數(shù)。添加接口是很容易的亡容,但是修改长踊,去掉接口會(huì)會(huì)影響較大。
接口小萍倡,不單指成員函數(shù)的數(shù)量少,也指函數(shù)的作用域盡可能小辟汰。
比如:
class Test
{
public:
? ? void funA();
? ? void funB();
? ? void funC();
? ? void funD();
};
假如列敲,funD 其實(shí)是可以使用 funA, funB, funC 來(lái)實(shí)現(xiàn)的。
這個(gè)時(shí)候帖汞,funD戴而,就不應(yīng)該放到Test里面◆嬲海可以將funD抽取出來(lái)所意。funD 只是一個(gè)封裝函數(shù),而不是最核心的。
void Test_funD(Test* test);
編寫類的函數(shù)時(shí)候扶踊,一些輔助函數(shù)泄鹏,優(yōu)先采用 Test_funD 這樣的方式,將其放到.cpp中秧耗,使用匿名空間保護(hù)起來(lái)备籽,外界就就不用知道此函數(shù)的存在,那些都只是實(shí)現(xiàn)細(xì)節(jié)分井。
當(dāng)不能抽取獨(dú)立于類的輔助函數(shù)车猬,先將函數(shù),變成private, 有必要再慢慢將其提出到public尺锚。 不要覺(jué)得這函數(shù)可能有用珠闰,一下子就寫上一堆共有接口。
再?gòu)?qiáng)調(diào)一次瘫辩,如無(wú)必要伏嗜,不要加接口。
從作用域大小杭朱,來(lái)看
獨(dú)立于類的函數(shù)阅仔,比類的成員函數(shù)要好
私有函數(shù),比共有函數(shù)要好
非虛函數(shù)弧械,比虛函數(shù)要好
5.2 聲明順序
類的成員函數(shù)或者成員變量八酒,按照使用的重要程度,從高到低來(lái)排列刃唐。
比如羞迷,使用類的時(shí)候,用戶更關(guān)注函數(shù)画饥,而不是數(shù)據(jù)衔瓮,所以成員函數(shù)應(yīng)該放到成員變量之前。 再比如抖甘,使用類的時(shí)候热鞍,用戶更關(guān)注共有函數(shù),而不是私有函數(shù)衔彻,所以public薇宠,應(yīng)該放在private前面。
具體規(guī)范
按照 public, protected, private 的順序分塊艰额。那一塊沒(méi)有澄港,就直接忽略。
每一塊中柄沮,按照下面順序排列
typedef回梧,enum废岂,struct,class 定義的嵌套類型
常量
構(gòu)造函數(shù)
析構(gòu)函數(shù)
成員函數(shù),含靜態(tài)成員函數(shù)
數(shù)據(jù)成員,含靜態(tài)數(shù)據(jù)成員
.cpp 文件中狱意,函數(shù)的實(shí)現(xiàn)盡可能給聲明次序一致湖苞。
5.3 繼承
優(yōu)先使用組合,而不是繼承髓涯。
繼承主要用于兩種場(chǎng)合:實(shí)現(xiàn)繼承袒啼,子類繼承了父類的實(shí)現(xiàn)代碼。接口繼承纬纪,子類僅僅繼承父類的方法名稱蚓再。
我們不提倡實(shí)現(xiàn)繼承,實(shí)現(xiàn)繼承的代碼分散在子類跟父親當(dāng)中包各,理解起來(lái)變得很困難摘仅。通常實(shí)現(xiàn)繼承都可以采用組合來(lái)替代。
規(guī)則:
繼承應(yīng)該都是 public
假如父類有虛函數(shù)问畅,父類的析構(gòu)函數(shù)為 virtual
假如子類覆寫了父類的虛函數(shù)娃属,應(yīng)該顯式寫上 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;
上面的例子,使用父類的指針指向子類护姆,假如父類的析構(gòu)函數(shù)不為virtual, 就只會(huì)調(diào)用父類的Definition的釋放函數(shù)矾端,引起子類獨(dú)有的數(shù)據(jù)不能釋放。所有需要加上virtual卵皂。
另外子類覆寫的虛函數(shù)寫上秩铆,override的時(shí)候,當(dāng)父類修改了虛函數(shù)的名字灯变,就會(huì)編譯錯(cuò)誤殴玛。從而防止,父類修改了虛函數(shù)接口添祸,而忘記修改子類相應(yīng)虛函數(shù)接口的情況滚粟。
6 函數(shù)
6.1 編寫短小的函數(shù)
函數(shù)盡可能的短小,凝聚刃泌,功能單一凡壤。
只要某段代碼,可以用某句話來(lái)描述耙替,盡可能將這代碼抽取出來(lái)鲤遥,作為獨(dú)立的函數(shù),就算那代碼只有一行林艘。最典型的就是C++中的max, 實(shí)現(xiàn)只有一句話。
template <typename T>
inline T max(T a, T b)
{
? ? return a > b ? a : b;
}
將一段代碼抽取出來(lái)混坞,作為一個(gè)整體狐援,一個(gè)抽象钢坦,就不用糾結(jié)在細(xì)節(jié)之中。
將一個(gè)長(zhǎng)函數(shù)啥酱,切割成多個(gè)短小的函數(shù)爹凹。每個(gè)函數(shù)中使用的局部變量,作用域也會(huì)變小镶殷。
短小的函數(shù)禾酱,更容易復(fù)用,從一個(gè)文件搬到另一個(gè)文件也會(huì)更容易绘趋。
短小的函數(shù)颤陶,因?yàn)閮?nèi)存局部性,運(yùn)行起來(lái)通常會(huì)更快陷遮。
短小的函數(shù)滓走,也容易閱讀,調(diào)試帽馋。
6.2 函數(shù)的參數(shù)可能少搅方,原則上不超過(guò)5個(gè)
人腦短時(shí)記憶的數(shù)字是很有限的,大約可以記憶7個(gè)數(shù)字绽族。有些人多些陪腌,有些人少些。我們這里取最少值甫贯,就是5個(gè)參數(shù)篇梭。
參數(shù)的個(gè)數(shù),太多娄蔼,就很容易混亂怖喻,記不住參數(shù)的意義。
同時(shí)參數(shù)的個(gè)數(shù)太多岁诉,很可能是因?yàn)檫@個(gè)函數(shù)做的事情有點(diǎn)多了锚沸。
可以通過(guò)很多手段來(lái)減少參數(shù)的個(gè)數(shù)。比如將函數(shù)分解涕癣,分解成多個(gè)短小的函數(shù)哗蜈。或者將幾個(gè)經(jīng)常一起的參數(shù)坠韩,封裝成一個(gè)類或者結(jié)構(gòu)距潘。比如,設(shè)計(jì)一個(gè)繪畫貝塞爾曲線的接口
void drawQuadBeizer(float startX,? float startY,
? ? ? ? ? ? ? ? ? ? float controlX, float controlY,
? ? ? ? ? ? ? ? ? ? float endX,? ? float endY);
這樣的接口只搁,就不夠
void drawQuadBeizer(const Point& start,
? ? ? ? ? ? ? ? ? ? const Point& control,
? ? ? ? ? ? ? ? ? ? const Point& end);
簡(jiǎn)潔易用音比。
當(dāng)然,每個(gè)規(guī)則都會(huì)有例外氢惋。比如設(shè)置一個(gè)矩陣的數(shù)值洞翩,二維矩陣本來(lái)就需要6個(gè)數(shù)字來(lái)表示稽犁,設(shè)置接口自然需要6個(gè)參數(shù)。
6.3 函數(shù)參數(shù)順序
參數(shù)順序骚亿,按照傳入?yún)?shù)已亥,傳出參數(shù),的順序排列来屠。不要使用可傳入可傳出的參數(shù)虑椎。
bool loadFile(const std::string& filePath, ErrorCode* code);? // 對(duì)
bool loadFile(ErrorCode* code, const std::string& filePath);? // 錯(cuò)
保持統(tǒng)一的順序,使得他人容易記憶俱笛。
6.4 函數(shù)的傳出參數(shù)捆姜,使用指針,而不要使用引用
比如
bool loadFile(const std::string& filePath, ErrorCode* code);? // 對(duì)
bool loadfile(const std::string& filePath, ErrorCode& code);? // 錯(cuò)
因?yàn)楫?dāng)使用引用的時(shí)候嫂粟,使用函數(shù)的時(shí)候會(huì)變成
ErrorCode code;
if (loadFile(filePath, code))
{
? ? ...
}
而使用指針娇未,調(diào)用的時(shí)候,會(huì)是
ErrorCode code;
if (loadFile(filePath, &code))
{
? ? ...
}
這樣從星虹,&code的方式可以很明顯的區(qū)分零抬,傳入,傳出參數(shù)宽涌。試比較
doFun(arg0, arg1, arg2);? ? // 錯(cuò)
doFun(arg0, &arg1, &arg2);? // 對(duì)
6.5 不建議使用函數(shù)的缺省參數(shù)
我們經(jīng)常會(huì)通過(guò)查看現(xiàn)有的代碼來(lái)了解如何使用函數(shù)的接口平夜。缺省參數(shù)使得某些參數(shù)難以從調(diào)用方就完全清楚,需要去查看函數(shù)的接口卸亮,也就是完全了解某個(gè)接口忽妒,需要查看兩個(gè)地方。
另外兼贸,缺省參數(shù)那個(gè)數(shù)值段直,其實(shí)是實(shí)現(xiàn)的一部分,寫在頭文件是不適當(dāng)?shù)摹?/p>
缺省參數(shù)溶诞,其實(shí)可以通過(guò)將一個(gè)函數(shù)拆分成兩個(gè)函數(shù)鸯檬。實(shí)現(xiàn)放到.cpp中。
7 其它
7.1 const的使用
我們建議螺垢,盡可能的多使用const喧务。
C++中,const是個(gè)很重要的關(guān)鍵字枉圃,應(yīng)用了const之后功茴,就不可以隨便改變變量的數(shù)值了,不小心改變了編譯器會(huì)報(bào)錯(cuò)孽亲,就容易找到錯(cuò)誤的地方坎穿。只要你覺(jué)得有不變的地方,就用const來(lái)修飾吧返劲。比如:
想求圓的周長(zhǎng)玲昧,需要用到Pi, Pi不會(huì)變的犯祠,加const,const double Pi = 3.1415926;
需要在函數(shù)中傳引用酌呆,只讀,不會(huì)變的搔耕,前面加const;
函數(shù)有個(gè)返回值隙袁,返回值是個(gè)引用,只讀弃榨,不會(huì)變的菩收,前面加const;
類中有個(gè)private數(shù)據(jù),外界要以函數(shù)方式讀取鲸睛,不會(huì)變的娜饵,加const,這個(gè)時(shí)候const就是加在函數(shù)定義末尾官辈。
const的位置:
const int* name;? // 對(duì)(這樣寫箱舞,可讀性更好)
int const* name;? // 錯(cuò)
7.2 不要注釋代碼,代碼不使用就直接刪掉
有些人不習(xí)慣使用版本控制工具拳亿,某段代碼不再使用了晴股,他們會(huì)注釋掉代碼,而不是直接刪除掉肺魁。他們的理由是电湘,這段代碼現(xiàn)在沒(méi)有用,可能以后會(huì)有用鹅经,我注釋了寂呛,以后真的再用的時(shí)候,就不用再寫了瘾晃。
不要這樣做贷痪。
注釋掉的代碼,放在源文件里面酗捌,會(huì)將正常的代碼搞混亂呢诬。有個(gè)破窗理論,說(shuō)假如一個(gè)窗戶破了胖缤,不去管它尚镰,路人就會(huì)傾向敲爛其它的窗戶。同樣哪廓,假如你看到代碼某個(gè)地方亂了狗唉,會(huì)覺(jué)得再搞的更亂也沒(méi)有關(guān)系,就會(huì)越來(lái)越亂涡真。
而在現(xiàn)代的版本控制工具下分俯,只要寫好提交記錄肾筐,找回從前的代碼是很容易的。