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默认忽略换行 ,但一般没必要换行
一个简短的类或者函数的说明:
使用空行或者小数点加空格的方式分开简述与详述,这里我统一使用空行来进行分隔:
/*! 本类的功能:打印错误信息 本类是一个单键,在程序中需要进行错误信息打印的地方 */