• 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、不要注释代码,代码不使用就直接删掉

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

  • 相关阅读:
    杜马岛
    Type interface com.zhaoka.mapper.DatKcardKmMapper is not known to the MapperRegistry
    jsp实现自动登录
    Struts2中的get、set方法重要性 .
    struts2 通过前台标签name属性将值传到后台,没有name属性传值,则后台对象有默认值,不为null。
    Jsp 操作 Cookie 实现自动登录
    struts2的bean类名首字母和第二个字母都不能大写
    mybatis自动生成的ExamMapper.xml方法总结
    Mybatis插入时注意,没有主键值。所以主键一般为自增类型
    <ywaf:code code="${project.projectType}" typeCode="PROJECT_TYPE"/>
  • 原文地址:https://www.cnblogs.com/y4247464/p/13836624.html
Copyright © 2020-2023  润新知