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