C++编程规范

参考于知乎某用户

一、格式

1、每行代码不多于80个字符;

2、使用空格,而不是制表符(Tab)来缩进,每次缩进4个字符;

3、指针符号*,引用符号&写在靠近类型的位置;

4、花括号的位置,使用Allman风格,另起一行,代码会更清晰;

for (auto i = 0; i < 100; i++)
{
    printf("%d
", 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、不要注释代码,代码不使用就直接删掉

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

原文地址:https://www.cnblogs.com/y4247464/p/13836624.html