C++ 代码风格

注解

我的C++编码风格确定为:严格遵守Google C++ Style、注释遵循Doxygen Comment规范

Google C++ Style 笔记

  • 头文件

    • 所有的头文件都必须且只使用#define进行头文件保护,命名格式当是: <PROJECT>_<PATH>_<FILE>_H_
    • 能用前置声明的时候就不要包含头文件(包含头文件意味着引入依赖),不允许访问类的定义的前提下, 我们在一个头文件中能对类 Foo 做哪些操作?
      • 可以将数据成员类型声明为 Foo * 或 Foo &.
      • 可以将函数参数 / 返回值的类型声明为 Foo (但不能定义实现).
      • 可以将静态数据成员的类型声明为 Foo, 因为静态数据成员的定义在类定义之外.
    • 只有当函数只有 10 行甚至更少时才将其定义为内联函数.复杂的内联函数的定义, 应放在后缀名为 -inl.h 的头文件中.
    • 定义函数时, 参数顺序依次为: 输入参数, 然后是输出参数.
    • 使用标准的头文件包含顺序可增强可读性, 避免隐藏依赖: C 库, C++ 库, 其他库的 .h, 本项目内的 .h.
  • 命令空间 & 作用域

    • 鼓励在源文件中使用匿名的命令空间,除std外不要有任何的using namespace,但是可以在源文件和头文件的函数、方法或类的内部using其它的实体,如using std::vector;等等;
    • 嵌套类受到所在的访问标签的作用,一般不要将嵌套类定义成公有, 除非它们是接口的一部分。比如, 嵌套类含有某些方法的一组选项. 但即使作为接口的一部分时,也要慎重告诉嵌套类定义成公有的,因为嵌套类的缺点决定了:只能在外围类的内部做前置声明. 因此, 任何使用了 Foo::Bar* 指针的头文件不得不包含类 Foo 的整个声明.
    • 尽量不要使用裸的全局函数
    • 函数变量尽可能置于小的作用域(即在不得不使用的时候再定义),同时在确保在定义的同时初始化,绝不能出现未初始化的变量。
    • 禁止使用 class 类型的静态或全局变量: 它们会导致很难发现的 bug 和不确定的构造和析构函数调用顺序. 永远不要使用函数返回值初始化静态变量; 不要在多线程代码中使用非 const 的静态变量.
    • 如果对象需要进行有意义的 (non-trivial) 初始化, 考虑使用明确的 Init() 方法并 (或) 增加一个成员标记用于指示对象是否已经初始化成功.

    • 如果一个类定义了若干成员变量又没有其它构造函数, 必须定义一个默认构造函数. 否则编译器将自动生产一个很糟糕的默认构造函数.

    • 对单个参数的构造函数使用 C++ 关键字 explicit.除非明显的想拥有隐匿转换的能力(透明包装)。

    • 仅在代码中需要拷贝一个类对象的时候使用拷贝构造函数; 大部分情况下的类都不需要具有拷贝功能的, 此时应使用 DISALLOW_COPY_AND_ASSIGN.明确的予以拒绝拷贝功能(放在类的未尾部分)。

      为了能作为 STL 容器的值, 你可能有使类可拷贝的冲动. 在大多数类似的情况下, 真正该做的是把对象的 指针 放到 STL 容器中. 可以考虑使用 std::tr1::shared_ptr.

    • 仅当只有数据时使用 struct, 其它一概使用 class.

    • 使用组合常常比使用继承更合理. 如果使用继承的话, 定义为 public 继承.

    • 数据成员在任何情况下都必须是私有的,并根据需要提供相应的存取函数(内联)。

    • 当重载一个虚函数, 在衍生类中把它明确的声明为 virtual. 理论依据: 如果省略 virtual 关键字, 代码阅读者不得不检查所有父类, 以判断该函数是否是虚函数.

    • 真正需要用到多重实现继承的情况少之又少. 只在以下情况我们才允许多重继承: 最多只有一个基类是非抽象类; 其它基类都是以 Interface 为后缀的 纯接口类.

    • 接口类最好都以Interface为后缀。

    • 除少数特定环境外,不要重载运算符.

    • 类的访问控制区段的声明顺序依次为: public:, protected:, private:. 如果某区段没内容, 可以不声明.

    • 每个区段内的声明通常按以下顺序:

      • typedefs 和枚举
      • 常量
      • 构造函数
      • 析构函数
      • 成员函数, 含静态成员函数
      • 数据成员, 含静态数据成员
    • 一般来说,一个函数不要太长,以40行以内为宜,但这只是建议。

    • 如果确实需要使用智能指针的话, scoped_ptr 完全可以胜任. 你应该只在非常特定的情况下使用 std::tr1::shared_ptr, 例如 STL 容器中的对象. 任何情况下都不要使用 auto_ptr.

  • 其它C++特性

    • 所以按引用传递的参数必须加上 const:输入参数是值参或 const 引用, 输出参数为指针. 输入参数可以是 const 指针, 但决不能是 非 const 的引用参数.

    • 仅在输入参数类型不同, 功能相同时使用重载函数 (含构造函数). 不要用函数重载模拟 缺省函数参数,而且禁止使用默认参数(这样强迫API的使用者去理解所有的参数)

    • 不允许使用变长数组和 alloca().

    • 允许合理的使用友元类及友元函数.

    • 不使用 C++ 异常 .

      不同意这个观点,也许在继续开发原有代码时可以禁止异常,但是一个新的项目使用异常是一个好的选择 ,很多时候异常是C++错误处理中的最佳选择(如构造函数中出错只有异常可以处理)。

      而且C++大牛们也在众多的经典书籍中大力推荐多使用异常。

    • 除单元测试外, 不要使用 RTTI。 虚函数和双重分派(访问者模式)可以替换RTTI并完成相同的工作;

    • 使用C++风格的类型转换,拒绝使用C风格的类型转换

    • 只在记录日志时使用流(流的好处在于类型的透明性),输入输出使用printf/scanf;

    • 在任何可能的情况下,尽量使用const

    • 使用断言而不是无符号类型来保证非负数 | for (unsigned int i = foo.Length()-1; i >= 0; --i)      //普通的int就不会有这个BUG | 使用无符号数导致的BUG我已经碰到过好多次了,类型提升导致这成了一个死循环; | 所以在 for 循环中还是使用普通的 int 类型比较好

    • 使用宏时要非常谨慎, 尽量以内联函数, 枚举和常量代替之.

    • 整数用 0, 实数用 0.0, 指针用 NULL(C++1x中有了nullptr), 字符 (串) 用 ‘0’.

    • 尽可能用 sizeof(varname) 代替 sizeof(type).

  • 命令约定

    • 函数命名, 变量命名, 文件命名应具备描述性; 不要过度缩写. 类型和变量应该是名词, 函数名可以用 “命令性” 动词.

    • 文件名要全部小写, 可以包含下划线 (_) 或连字符 (-). 按项目约定来.

    • 其中C++ 文件要以 .cc 结尾, 头文件以 .h 结尾.

    • 类型名称(类, 结构体, 类型定义 (typedef), 枚举)的每个单词首字母均大写, 不包含下划线: MyExcitingClass, MyExcitingEnum.

    • 变量名一律小写, 单词之间用下划线连接. 类的成员变量以下划线结尾:

      my_exciting_local_variable
      my_exciting_member_variable_        //成员变量
      
    • 结构体的数据成员可以和普通变量一样, 不用像类那样接下划线:

      struct UrlTableProperties {
        string name;
        int num_entries;
      }
      
    • 对全局变量没有特别要求, 少用就好, 但如果你要用, 可以用 g_ 或其它标志作为前缀, 以便更好的区分局部变量.(静态成员/变量也可以考虑加上s_的前缀)

    • 常量在名称前加 k: kDaysInAWeek(变量用全小写、常量不用全小写).

    • 常规函数使用大小写混合, 取值和设值函数则要求与变量名匹配:

      MyExcitingFunction(), MyExcitingMethod(),
      my_exciting_member_variable(), set_my_exciting_member_variable().
      
    • 名字空间用小写字母命名, 并 基于项目名称和目录结构 : google_awesome_project.

    • 枚举的命名应当和 常量一致: kEnumName

    • 尽量不使用宏,要用的时候显然应该用全大写加下划线的命令方式: THIS_IS_MACRO

  • 注释

    • // 或 /* */ 都可以; 但 // 更 常用. 要在如何注释及注释风格上确保统一.

    • ====== 在它的基础上额外使用一个!号从而符合了Doxygen注释规范 ======

    • 在每一个文件开头加入文件注释,依次是:

      • 版权声明 (比如, Copyright 2008 Google Inc.)
      • 许可证(如果有). 为项目选择合适的许可证版本 (比如, Apache 2.0, BSD, LGPL, GPL)
      • 作者:标识文件的原始作者.
      • 作者:修改的作者信息
      • (文件注释可以炫耀你的成就, 也是为了捅了篓子别人可以找你)
    • 紧接着版权许可和作者信息之后, 每个文件都要用注释描述文件内容.
      通常, .h 文件要对所声明的类的功能和用法作简单说明. .cc 文件通常包含了更多的实现细节或算法技巧讨论, 如果你感觉这些实现细节或算法技巧讨论对于理解 .h 文件有帮助, 可以该注释挪到 .h, 并在 .cc 中指出文档在 .h.
      不要简单的在 .h 和 .cc 间复制注释. 这种偏离了注释的实际意义
    • 每个类的定义都要附带一份注释, 描述类的功能和用法.

    • 函数声明处注释描述函数功能; 定义处描述函数实现.

    • 通常变量名本身足以很好说明变量用途. 某些情况下, 也需要额外的注释说明.

    • 对于代码中巧妙的, 晦涩的, 有趣的, 重要的地方加以注释.(注意 永远不要 用自然语言翻译代码作为注释. 要假设读代码的人 C++ 水平比你高)

      注释要言简意赅, 不要拖沓冗余, 复杂的东西简单化和简单的东西复杂化都是要被鄙视的;

    • 向函数传入 NULL, 布尔值或整数时, 要注释说明含义, 或使用常量让代码望文知意(不说明的话单独传一个NULL和数字谁也不知道是什么意思).

    • 对那些临时的, 短期的解决方案, 或已经够好但仍不完美的代码使用 TODO 注释.

  • 格式

    • 原则上每一行代码字符数不超过 80.
    • 尽量不使用非 ASCII 字符, 使用时必须使用 UTF-8 编码.
    • 只使用空格, 每次缩进 2 个空格(WINDOWS下常用的一个TAB).
    • 返回类型和函数名在同一行, 参数也尽量放在同一行. | 如果要换行的参数则保持 4 个空格的缩进; | 如果函数声明成 const, 关键字 const 应与最后一个参数位于同一行 | 如果有些参数没有用到, 在函数定义处将参数名注释起来(不能省略)
    • 倾向于不在圆括号内使用空格. 关键字 else 与 then 分支的 } 在同一行 | 如果能增强可读性, 简短的条件语句允许写在同一行. 只有当语句简单并且没有使用 else 子句时使用:
    • switch 语句可以使用大括号分段. 空循环体应使用 {} 或 continue.
    • 句点或箭头前后不要有空格. 指针/地址操作符 (*, &) 之后不能有空格.
    • return 表达式中不要用圆括号包围.
    • 变量及数组初始化时用 = 或 () 均可.
    • 预处理指令不要缩进, 从行首开始.(VS中会自动处理)
    • 访问控制块的声明依次序是 public:, protected:, private:, 每次缩进 1 个空格(其它的默认2个空格,上面说过).
    • 构造函数初始化列表放在同一行或按四格缩进并排几行.
    • 名字空间内容不缩进.
    • 水平留白的使用因地制宜. 永远不要在行尾添加没意义的留白.
    • 垂直留白越少越好,不在万不得已, 不要使用空行.(因为在一屏能显示的行数越多,思路就会越清晰) 同理:这就明白了为什么左大括号也不要去占用一行了,放在上一行的行尾就好了。
  • 规则特例

    • 对于现有不符合既定编程风格的代码可以网开一面.
    • Windows中C++编程需要注意的问题:
      • 尽量使用原有的 C++ 类型, 例如, 使用 const TCHAR * 而不是 LPCTSTR.
      • 使用 Microsoft Visual C++ 进行编译时, 将警告级别设置为 3 或更高, 并将所有 warnings 当作 errors 处理.
      • 不要使用 #pragma once;
      • 除非万不得已, 不要使用任何非标准的扩展, 如 #pragma 和 __declspec. 允许使用 __declspec(dllimport) 和 __declspec(dllexport); 但你必须通过宏来使用, 比如 DLLIMPORT 和 DLLEXPORT
      • MSVC中的预编译头文件stdafx.h,应该避免显式包含此文件 (precompile.cc), 使用 /FI 编译器选项以自动包含.
  • 运用常识和判断力, 并保持一致.

总结出自己的C++注释风格,遵循Doxygen

  • 让自己的注释符合Doxygen的风格,将注释分为两种:

    • 需要文档化的注释(如文件、类、函数、枚举等的注释)
    • 不需要文档化的简单注释(如在代码块内部的简短解释)
  • 需要文档化的注释统一使用Doxygen的注释风格,即/*! … */和//!和//!< 三种方法
    不需要文档化的注释统一使用普通的注释风格,即2斜杠//

    这样又同时符合了Google C++风格(Google推荐/*…*/和//2种,上面的三种方式是从这2种演变而来)

  • 常用的描述格式:

    /*!
      @brief 这个函数用于打开文件
      (除了只有Brief注释的情况外,其它的情况都要使用@breif将 简述 与 详述 分开!!)
      用某某某方法来打开文件 \n
      文件打开成功后,必须使用 ::CloseFile 函数关闭。
      @param [in|out] arg          参数描述
      @param [in] file_name         文件名字符串
      @param [in] file_mode 文件打开模式字符串
      @return             是否打开成功
      @retval     0             成功
      @retval     -1             失败
      @remarks    注意事项
      @note            注意事项,功能同@remarks,显示字样不同
      @attention 注意
      @warning {warning message } 一些需要注意的事情
      @relates <name> 通常用做把非成员函数的注释文档包含在类的说明文档中。
      @since {text} 通常用来说明从什么版本、时间写此部分代码。
      @pre { description of the precondition } 用来说明代码项的前提条件。
      @post { description of the postcondition } 用来说明代码项之后的使用条件。
      @par 代码示例: ===这是用来告诉doxygen最后生成的文件在这里分一下段===
      @code(必须使用@endcode结束)
      示例代码(无需缩进)
      @endcode
      @see             ::ReadFile ::WriteFile ::CloseFile
      @deprecated 由于特殊的原因,这个函数可能会在将来的版本中取消。
      @todo { things to be done } 对将要做的事情进行注释
      @exception <exception-object> {exception description} 对一个异常对象进行注释。
      @enum CTest::MyEnum 引用了某个枚举,Doxygen会在该枚举处产生一个链接
      @var CTest::m_FileKey 引用了某个变量,Doxygen会在该枚举处产生一个链接
      @class CTest "inc/class.h" 引用某个类,Doxygen会在该枚举处产生一个链接
    */
    
  • 列表的形式使用以下的形式
    - 一级目录1
    -# 二级目录11
    -# 二级目录12
    - 一级目录2
    -# 二级目录21
  • 对变量、成员、宏什么的(貌似除了类、函数外的所有情况)使用以下两种形式:

    //! comment  更适合注释可能有多行的情况
    int a;            //!< comment 更适合简短的一行注释
    
  • 所有的文件(#include之前)、类、函数、枚举等都必须有注释

  • 所有的在同一语义下的 分行都要明确的使用\n,因为doxygen默认忽略换行 ,但一般没必要换行

  • 一个简短的类或者函数的说明:

  • 使用空行或者小数点加空格的方式分开简述与详述,这里我统一使用空行来进行分隔:

    /*!
      本类的功能:打印错误信息
    
      本类是一个单键,在程序中需要进行错误信息打印的地方
    */