我的C++代码风格

1 前言

  在多人团队开发中,使用统一的代码规范,项目开发前期利于团队协作,中期利于工作审核,后期利于降低维护成本,总而言之,代码规范很重要。这话虽然简单,但一直未能引起重视,直到我看到下图的那个新闻后,我就知道有很多码农还活着真不简单啊。我们姑且不去深究这个新闻的真实性,但至少我们也该意识到代码规范的重要性了吧。

0001.png

  关于C++的代码风格,网上有很多,比较权威的有 谷歌华为 这两种,我个人推荐使用 华为 的,这也是我喜欢的代码风格。之所以选择 华为 的,而不是 谷歌 的,主要还是因为它们形成的背景年代不一样。谷歌的代码风格,很多都受老一辈程序员( Unix 时代的程序员)的影响,他们早期工作的显示设备,都是低分辨率的屏幕,代码倾向于压缩排版,以期待显示更多内容。而到了现在,我们工作的绝大多数显示设备都是高分辨率的屏幕,压缩排版的代码布局,看起来就显得过于紧凑和密集,这会导致我们阅读上的疲劳。相比之下,华为的代码风格,在代码排布上,很多地方经常会使用空格、空行进行分隔,这样显得比较宽松,阅读起来降低了疲劳感。

  在这里,我不打算重复去谈代码编写中很细节的代码风格问题(比如 命名、对齐、缩进 等),而着重于谈谈我平时在 C++类编写时的代码排版方式,以及如何编写符合 Doxygen 文档生成规范的代码注释,并提一提我在用 IDE(QtCreator、VisualStudio) 编码时使用的一些技巧来改善代码的布局。

2 C++类的布局基本结构

  通常的C++类编写,常使用头文件(*.h、*.hh 等)写类的声明,而用源文件(*.cpp、*.cc、*.cxx 等)写类的实现,当然,对于模板类一般不会这样将声明与实现分开两部分文件编写,它要么是声明与实现合在一起放到头文件中,要么是头文件后用一个(或者多个)内嵌文件(如 *.inl)去编写类的实现流程。此处,我们不提模板类的代码布局方式(直接参考常规类的代码布局方式),而着重谈常规类的代码布局。

2.1 类声明文件(头文件)

先给出类声明文件的整体区段排序的摘要,如下所示:

  1. 文件说明区段:版权说明、版本号、生成日期、作者、内容、功能、修改日志等注释内容。
  2. 类声明的开始。
  3. 类中,共同属性内容区段(可选内容,主要填写一些 类的友元关系、隶属库的某些宏声明 等等)。
  4. 类中,构造函数 与 析构函数 区段。
  5. 类中,通用数据类型的声明 区段。
  6. 类中,类属性(静态属性)的相关函数与数据成员 区段。
  7. 类中,重载区段:重载父类的接口(虚函数)。
  8. 类中,扩展区段:继承自该类的子类可重载的接口(虚函数)。
  9. 类中,可对外调用的接口区段(public访问属性的成员函数)。
  10. 类中,只对内调用的接口区段(protected访问属性的成员函数)。
  11. 类中,类对象的相关数据成员 区段。
  12. 类声明的结束。

以上区段的排序,在实际项目中,可视具体情况做调整,也可增加其他区段。在此,我给出如下的样例代码(内容中,以 //=> 开头的内容,均为说明性的内容,非代码中要编写的内容):

//=> 文件说明区段:版权说明、版本号、生成日期、作者、内容、功能、修改日志等注释内容。
/**
 * @file    FileName.h
 * <pre>
 * Copyright (c) 2018, Gaaagaa All rights reserved.
 * 
 * 文件名称:FileName.h
 * 创建日期:2018年11月25日
 * 文件标识:
 * 文件摘要:
 * 
 * 当前版本:1.0.0.0
 * 作    者:
 * 完成日期:2018年11月25日
 * 版本摘要:
 * 
 * 取代版本:
 * 原作者  :
 * 完成日期:
 * 版本摘要:
 * </pre>
 */

//=> 文件头的守卫宏定义(防止 文件头重复包含时 产生重复定义)
#ifndef __HEADFILENAME_H__
#ifndel __HEADFILENAME_H__

//=> 包含类设计所依赖的外部头文件
#include "dependent.h"
#include <dependent.h>

//=> 以 80 个正斜杠字符进行大区段分割,有承前启后的作用,这里标识一个大区段(类)内容的开始
////////////////////////////////////////////////////////////////////////////////
// class name  //=> 填写要设计的类名,表示这个大区段的内容主题

//=> 此处可增加一些类设计所依赖的其他数据类型、接口函数等的前置声明

//=> 类的简要说明(用于 Doxygen 文档生成操作)
/**
 * @class ClassName
 * @brief 类的简要描述。
 */
class ClassName : public SuperClass
{
    //=> 类的共同属性内容区段(可选内容,主要填写一些 类的友元关系、隶属库的某些宏声明 等等),举例如下:
    friend class FriendClass;     //=> 友元关系
    Q_OBJECT                      //=> QT 库的宏声明(或特性的实现)
    DECLARE_DYNCREATE(ClassName)  //=> 库的宏声明(或特性的实现)

    //=> 构造函数 与 析构函数 区段
    // constructor/destructor
public:
    ClassName(void);
    virtual ~ClassName(void);

    //=> 通用数据类型的声明
    // common data types
public:
    /**
     * @enum  emConstValue
     * @brief 类相关枚举常量值。
     */
    typedef enum emConstValue
    {
        ECV_CLASS_ID = 0x00000000,
    } emConstValue;

    typedef std::vector<Type> VecType;

    //=> 类属性(静态属性)的相关函数与数据成员
    // common invoking
public:
    //=> 关于 函数/方法 的注释方式,后面会提到
    /**********************************************************/
    /**
     * @brief 申请对象流水号。
     */
    static int make_object_seqno(void);

protected:
    //=> (类静态的 或 类对象的)成员变量的注释,符合 Doxygen 文档生成的规范两种注释
    static int ms_object_seqno_builder;    ///< 用于对象流水号申请操作的生成器  //=> 单行注释

    /** 所创类对象的计数器 */ //=> 与成员变量不同行的注释,可多行编写
    static int ms_object_counter;

    //=> 重载区段:重载父类的接口(虚函数)
    // overrides
protected:
    //=> 函数/方法 的注释方式
    /**********************************************************/ //=> 以此线进行分隔,表示函数的小区段
    /**
     * @brief 重载父类的接口。  //=> 接口的功能 简要说明
     * 
     * @param [in    ] param_1: 入参形式的参数注释。     //=> 几种功能类型的参数注释(可选段)
     * @param [out   ] param_2: 回参形式的参数注释。
     * @param [in,out] param_3: 可作入参和回参形式的参数注释。
     * 
     * @return int            //=> 返回值的注释(可选段)
     *         - 成功,返回 0;
     *         - 失败,返回 错误码。
     */
    virtual int super_method(int param_1, int & param_2, int & param_3) override;   //=> override 为 C++11 后的关键字,代码中可选

    //=> 扩展区段:继承自该类的子类可重载的接口(虚函数)
    // extensible interfaces
protected:
    //=> 函数/方法 的注释方式
    /**********************************************************/ //=> 以此线进行分隔,表示函数的小区段
    /**
     * @brief 继承自该类的子类可重载的接口。  //=> 接口的功能 简要说明
     * 
     * @param [in    ] param_1: 入参形式的参数注释。     //=> 几种功能类型的参数注释(可选段)
     * @param [out   ] param_2: 回参形式的参数注释。
     * @param [in,out] param_3: 可作入参和回参形式的参数注释。
     * 
     * @return int            //=> 返回值的注释(可选段)
     *         - 成功,返回 0;
     *         - 失败,返回 错误码。
     */
    virtual int extensible_method(int param_1, int & param_2, int & param_3);

    //=> 可对外调用的接口区段(public访问属性的成员函数)
    // public interfaces
public:
    //=> 精简的 函数/方法 的注释方式
    /**********************************************************/
    /**
     * @brief 获取对象标识 ID。
     */
    int get_object_id(void);

    //=> 只对内调用的接口区段(protected访问属性的成员函数)
    // inner invoking methods
protected:
    //=> 精简的 函数/方法 的注释方式
    /**********************************************************/
    /**
     * @brief 返回计算得到对象标识 ID。
     */
    int calc_object_id(void);

    //=> 类对象的相关数据成员 区段
    // data members
protected:
    int m_object_id;   ///< 对象标识 ID
};

//=> 以 80 个正斜杠字符进行大区段分割,有承前启后的作用,这里标识一个大区段(类)内容的结束
////////////////////////////////////////////////////////////////////////////////

#endif // __HEADFILENAME_H__


2.2 类的实现文件(源文件)

对于类的文件,要描述的区段没有声明文件中的多,主要有如下区段:

  1. 文件说明区段:版权说明、版本号、生成日期、作者、内容、功能、修改日志等注释内容。
  2. 类实现的区段:类属性(静态属性)的相关函数与数据成员。
  3. 类实现的区段:构造函数 与 析构函数 区段。
  4. 类实现的区段:重载区段:重载父类的接口(虚函数)。
  5. 类实现的区段:扩展区段:继承自该类的子类可重载的接口(虚函数)。
  6. 类实现的区段:可对外调用的接口区段(public访问属性的成员函数)。
  7. 类实现的区段:只对内调用的接口区段(protected访问属性的成员函数)。

以上区段的排序,在实际项目中,可视具体情况做调整,也可增加其他区段。在此,也给出如下的样例代码(内容中,以 //=> 开头的内容,均为说明性的内容,非代码中要编写的内容):

//=> 文件说明区段:版权说明、版本号、生成日期、作者、内容、功能、修改日志等注释内容。
/**
 * @file    FileName.cpp
 * <pre>
 * Copyright (c) 2018, Gaaagaa All rights reserved.
 * 
 * 文件名称:FileName.cpp
 * 创建日期:2018年11月25日
 * 文件标识:
 * 文件摘要:
 * 
 * 当前版本:1.0.0.0
 * 作    者:
 * 完成日期:2018年11月25日
 * 版本摘要:
 * 
 * 取代版本:
 * 原作者  :
 * 完成日期:
 * 版本摘要:
 * </pre>
 */

//=> 包含类设计所依赖的外部头文件
#include "dependent.h"
#include <dependent.h>

//=> 以 80 个正斜杠字符进行大区段分割
////////////////////////////////////////////////////////////////////////////////
// class name  //=> 大区段的内容主题(可多行注释)

//=> 以 70个字符(“=”)进行分割,表示次级区段
//====================================================================

// 
// ClassName : common invoking ( static methods and data )  //=> 类属性(静态属性)的相关函数与数据成员 区段
// 

//=> 优先声明静态成员(或初始化)
int ClassName::ms_object_seqno_builder = 0;  ///< 用于对象流水号申请操作的生成器
int ClassName::ms_object_counter       = 0;  ///< 所创类对象的计数器


//=> 再编写 静态函数的实现流程
/**********************************************************/
/**
 * @brief 申请对象流水号。
 */
int ClassName::make_object_seqno(void)
{
    return ++ms_object_seqno_builder;
}

//====================================================================

// 
// ClassName : constructor/destructor   //=> 构造函数 与 析构函数 区段
// 

ClassName::ClassName(void)
{
    m_object_id = calc_object_id();

    ClassName::ms_object_counter += 1;
}

ClassName::~ClassName(void)
{
    ClassName::ms_object_counter -= 1;
}

//====================================================================

// 
// ClassName : overrides   //=> 重载区段:重载父类的接口(虚函数)
// 

/**********************************************************/
/**
 * @brief 重载父类的接口。
 * 
 * @param [in    ] param_1: 入参形式的参数注释。
 * @param [out   ] param_2: 回参形式的参数注释。
 * @param [in,out] param_3: 可作入参和回参形式的参数注释。
 * 
 * @return int
 *         - 成功,返回 0;
 *         - 失败,返回 错误码。
 */
int ClassName::super_method(int param_1, int & param_2, int & param_3) override
{
    //=> 以 40 个字符(“=”)的短线分割,明确某个代码块具备一定的逻辑相关性,便于阅读与理解
    //======================================
    // 代码块说明

    param_2 = param_1 + get_object_id();
    param_3 = param_3 + get_object_id();

    //======================================

    return 0;
}

//====================================================================

// 
// ClassName : extensible interfaces   //=> 扩展区段:继承自该类的子类可重载的接口(虚函数)
// 

/**********************************************************/
/**
 * @brief 继承自该类的子类可重载的接口。
 * 
 * @param [in    ] param_1: 入参形式的参数注释。
 * @param [out   ] param_2: 回参形式的参数注释。
 * @param [in,out] param_3: 可作入参和回参形式的参数注释。
 * 
 * @return int
 *         - 成功,返回 0;
 *         - 失败,返回 错误码。
 */
int ClassName::extensible_method(int param_1, int & param_2, int & param_3)
{
    //======================================
    // 代码块说明

    param_2 = param_1 + get_object_id();
    param_3 = param_3 + get_object_id();

    //======================================

    return 0;
}

//====================================================================

// 
// ClassName : public interfaces   //=> 可对外调用的接口区段(public访问属性的成员函数)
// 

/**********************************************************/
/**
 * @brief 获取对象标识 ID。
 */
int ClassName::get_object_id(void)
{
    return m_object_id;
}

//====================================================================

// 
// ClassName : inner invoking   //=> 只对内调用的接口区段(protected访问属性的成员函数)
// 

/**********************************************************/
/**
 * @brief 返回计算得到对象标识 ID。
 */
int ClassName::calc_object_id(void)
{
    return ClassName::make_object_seqno();
}

在上面代码中, ClassName::super_method() 内部使用了短线分割进行代码逻辑块标注,这种方式,在较长代码流程中很便于代码的阅读和理解,我个人的很多代码中会经常使用这种方式进行代码块的注释。

3. 其他数据类型的风格

  C++除了类这种数据类型外,主要的还有结构体、枚举、C函数这些,这部分基本上属于原来 C 部分的范畴,只要代码中的注释规范符合 Doxygen 的规范即可,其他的不做过多的说明,只给出简单的参考样例:

/**
 * @struct StructName
 * @brief  结构体的描述信息说明。
 */
typedef struct StructName
{
    int  v1;  ///< v1 字段说明
    int  v2;  ///< v2 字段说明
    int  v3;  ///< v3 字段说明
} StructName;

/**
 * @enum  emContext
 * @brief 枚举常量值。
 */
typedef enum emContext
{
    ECV_CONTEXT_VALUE_V1  = 0x0001,   ///< 枚举值说明
    ECV_CONTEXT_VALUE_V2  = 0x0002,   ///< 枚举值说明
    ECV_CONTEXT_VALUE_V3  = 0x0003,   ///< 枚举值说明
    ECV_CONTEXT_VALUE_V4  = 0x0004,   ///< 枚举值说明
} emContext;

/**********************************************************/
/**
 * @brief 字符串忽略大小写的比对操作。
 *
 * @param [in ] xszt_lcmp : 比较操作的左值字符串。
 * @param [in ] xszt_rcmp : 比较操作的右值字符串。
 *
 * @return x_int32_t
 *         - xszt_lcmp <  xszt_rcmp,返回 <= -1;
 *         - xszt_lcmp == xszt_rcmp,返回 ==  0;
 *         - xszt_lcmp >  xszt_rcmp,返回 >=  1;
 */
x_int32_t vx_stricmp(x_cstring_t xszt_lcmp, x_cstring_t xszt_rcmp)
{
    x_int32_t xit_lvalue = 0;
    x_int32_t xit_rvalue = 0;

    const x_char_t * xct_lptr = xszt_lcmp;
    const x_char_t * xct_rptr = xszt_rcmp;

    if (xszt_lcmp == xszt_rcmp)
        return 0;
    if (X_NULL == xszt_lcmp)
        return -1;
    if (X_NULL == xszt_rcmp)
        return 1;

    do
    {
        if (((xit_lvalue = (*(xct_lptr++))) >= 'A') && (xit_lvalue <= 'Z'))
        {
            xit_lvalue -= ('A' - 'a');
        }

        if (((xit_rvalue = (*(xct_rptr++))) >= 'A') && (xit_rvalue <= 'Z'))
        {
            xit_rvalue -= ('A' - 'a');
        }

    } while (xit_lvalue && (xit_lvalue == xit_rvalue));

    return (xit_lvalue - xit_rvalue);
}

4. IDE中的代码编写技巧

  就我个人使用过的 IDE(针对 C++ 开发的),主要有 Visual C++QtCreator 这两款,这里,我也就针对这两款 IDE 谈一谈我的一些使用技巧,怎么对自己编写的代码进行注释和排版。

4.1 Visual C++

  首先,我不得不说,VC++自身的代码智能提示真的很弱鸡,所以我安装完 VS IDE 后,都会安装 VisualAssitX 这个插件。按照上面提到我的代码风格,利用这个插件的代码片段快速插入功能,可极大提高代码编写的效率。

  VisualAssitX的代码片段快速插入功能,按如下步骤,我们可以设置自定义的代码片段以及对应的快捷键:

  1. 依次操作:"VAssistX" => "Visual Assist Options..." => "Visual Assist Options"对话框 => 左"Suggestions" => 右侧"Edit VA Snippets"按钮 => "VA Snippet Editor"对话框;
  2. 调出的 "VA Snippet Editor"对话框 如下图所示:

    0002.png
  3. Language: 的下拉选择框中,设置为 C++Type: 设置为 All by Shortcut
  4. 点击工具栏上的第一个按钮,即可新增“代码片段”,在右侧可编辑“代码片段”的“Shortcut”(快捷输入的短字符串)和代码片段“内容”;在这里,也可对原有的“代码片段”进行修改操作,在完成所有的“代码片段”设置后,记得点击“OK”按钮保存所有更改的内容。
  5. 步骤3中,Type也可以选择其他的选项,修改一些默认的代码片段内容,以适配我们实际需要;比如,我自己就修改了 Refactoring 下的 Document Method 的内容,满足自己代码中的函数注解格式。

在这里列举几个我常用的代码片段(主要用在“注释”和“代码区段分割”上):

  • 文件头部的说明:快捷键为 //-,代码片段内容如下
/**
 * @file    $FILE_BASE$.$FILE_EXT$
 * <pre>
 * Copyright (c) $YEAR$, Gaaagaa All rights reserved.
 * 
 * 文件名称:$FILE_BASE$.$FILE_EXT$
 * 创建日期:$YEAR$年$MONTH_02$月$DAY_02$日
 * 文件标识:
 * 文件摘要:$end$
 * 
 * 当前版本:1.0.0.0
 * 作    者:
 * 完成日期:$YEAR$年$MONTH_02$月$DAY_02$日
 * 版本摘要:
 * 
 * 取代版本:
 * 原作者  :
 * 完成日期:
 * 版本摘要:
 * </pre>
 */
  • 大区段分割线(80 个字符):快捷键为 /// ,代码片段如下:
////////////////////////////////////////////////////////////////////////////////
  • 次级区段分割线(70 个字符):快捷键为 //= ,代码片段如下:
//====================================================================
  • 函数的简短注释:快捷键为 //; ,代码片段如下:
/**********************************************************/
/**
 * @brief $end$
 */
  • 逻辑相关(函数内 或 结构内定义)的代码块分割线(40 个字符):快捷键为 //=s ,代码片段如下:
//======================================
  • 在上面的“步骤5”中,我们提到修改 Document Method 的内容如下:
/**********************************************************/
/**
 * @brief $end$
 * 
 * @param [in ] $MethodArgName$ : 
 * 
 * @return $SymbolType$
 *         
 */

在编辑代码时,我们将光标定位到(放到)函数名中,然后通过如下菜单步骤添加函数的注释:

"VAssistX" => "Code Generation and Refactoring" => "Document Method"

另外,我们可以通过添加全局的组合快捷键:Ctrl+Shift+D 来快速调用该菜单命令,设置步骤如下:

1. 菜单 “Tools” => “Options...” => “Options” 对话框 => “Environment” => “Keyboard”;
2. 在 “Show commands containing:” 输入:VAssistX.RefactorDocumentMethod ,然后在列表中选中该命令项;
3. “Use new shortcut in:” :Global
4. “Press shortcut keys:” :Ctrl+Shift+D
5. 点击 “Assign” 按钮,然后再点击 “OK” 按钮保存设置后退出,这就完成了整个命令的快捷键设置流程。

以上提到的代码片段,也只是我个人常用的一部分,其他的,视个人习惯与项目情况,自行配制。VisualAssistX设置的代码片段最终会保存到磁盘中的文件:C:\Users\{用户名}\AppData\Roaming\VisualAssist\Autotext\cpp.tpl ,可以备份该文件用于其他情况的用途(如团队中使用统一的代码片段输入方式)。我个人的可在此下载cpp.tpl

4.2 QtCreator

  QtCreator没有VisualAssistX那样强大的插件,但也提供了代码片段快速插入的功能,可通过如下步骤进行配置:

  1. 菜单“Tools” => “Options...” => “Options”对话框 => “Text Editor” => “Snippets” 选项卡;
  2. 此时可添加相应的代码片段,其中“Trigger” 为代码编写过程中触发的字符串(相当于快捷键);
  3. 完成添加(或修改)操作后,记得保存。

  至于要配置成什么样的代码片段功能,我这里就不再赘述,但就我个人的使用经验来讲,这方面,QtCreator当前版本(4.7.1版)做得还不够成熟,期待在以后的升级版本中,能有更大改进。

4.3 Doxygen 的代码文档生成

  这部分的内容,以后我会在其他文章中再来详细写吧。

5. 参考资料

posted @ 2018-11-26 16:50  VxGaaagaa  阅读(627)  评论(0编辑  收藏  举报