C++ 编码风格 #
这是基于 Google 发布的 C++ 编程风格指南整理的,每一项规则的详细的阐述可以参见
官方的原版指南或者
中文版。
注意:这份指南只是提供了一份通用的编程规范,当你的项目已有自己的约定则优先遵守项目的约定!
头文件 #
通常一个一个 .cc
对应一个 .h
,但也有一些常见的例外,例如单元测试代码只有 .cc
文件和 main()
函数。
Self-contained 头文件 #
- 头文件,以
.h
结尾; - 用于插入文本的文件,以
.inc
结尾; - 模板或内联函数的定义不要放到
-inl.h
文件中; - 不建议从
.h
中分离出-inl.h
文件;
#define 保护 #
所有头文件都应该使用 #define
来防止头文件被多重包含, 命名格式当是:
<PROJECT>_<PATH>_<FILE>_H_
(路径是从项目的源代码树的根路径开始)。
前置声明 #
尽可能地避免使用前置声明。使用 #include
包含需要的头文件即可。
- 尽量避免前置声明那些定义在其他项目中的实体;
- 函数:总是使用
#include
; - 类模板:优先使用
#include
;
内联函数 #
- 不要内联超过 10 行的函数;
- 不要内联包含循环或
switch
语句的函数; - 虚函数和递归函数不应该声明成内联;
#include 的路径及顺序 #
- 避免使用特殊的快捷目录
.
和..
; - 项目内头文件应按照项目源代码目录树结构排列;
- 头文件的包含顺序如下:
.cc
文件对应的头文件(优先位置)- C 系统文件
- C++ 系统文件
- 第三方库
.h
文件 - 本项目内
.h
文件
- 以空行分割以上不同类别的
#include
语句 - 例外:平台特定代码需要进行条件编译,这些可以放到其他的
#include
之后
作用域 #
命名空间 #
- 命名空间的最后(右括号)注释出名字空间的名字;
- 不要在命名空间
std
内声明任何东西, 包括标准库的类前置声明; - 不要在头文件中使用命名空间别名除非显式标记内部命名空间使用;
- 禁止使用
using
指令引入其他空间的符号; - 禁止使用内联命名空间;
匿名命名空间和静态变量 #
- 推荐、鼓励在
.cc
中对于不需要在其他地方引用的标识符使用内部链接性声明,但是不要在.h
中使用; - 匿名命名空间的声明和具名的格式相同,在最后注释上
namespace
;
非成员函数、静态成员函数和全局函数 #
- 尽量不要用裸的全局函数,使用静态成员函数或命名空间内的非成员函数;
- 将一系列函数直接置于命名空间中,不要用类的静态方法模拟出命名空间的效果;
- 非成员函数不应依赖于外部变量, 应尽量置于某个命名空间内;
- 相比单纯为了封装若干不共享任何静态数据的静态成员函数而创建类, 不如使用 命名空间;
- 如果必须定义非成员函数, 又只是在
.cc
文件中使用它, 可使用 匿名命名空间 或static
链接关键字(如static int Foo() {...}
)限定其作用域;
局部变量 #
- 在尽可能小的作用域中声明变量, 离第一次使用越近越好;
- 使用初始化的方式替代声明再赋值,例如:
int j = g();
;
静态和全局变量 #
- 禁止使用类的
静态储存周期 变量,不过
constexpr
变量除外; - 全局变量,静态变量,静态类成员变量和函数静态变量,都必须是 POD (原生数据类型)以及 POD 类型的指针、数组和结构体;
- 不允许用函数返回值来初始化 POD 变量,除非该函数(比如 ‘‘getenv()’ 或 ‘‘getpid()’ )不涉及任何全局变量;
- 多线程中的全局变量 (含静态成员变量) 不要使用 class 类型 (含 STL 容器);
类 #
构造函数的职责 #
- 不要在构造中调用虚函数;
- 不要在无法报出错误时进行可能失败的初始化;
- 如果对象需要正确的初始化,可以考虑使用明确的
Init()
方法或者工厂模式;
隐式类型转换 #
- 类型转换运算符和单参数构造函数都应当用
explicit
进行标记; - 拷贝和移动构造函数是个例外, 不应当被标记为
explicit
; - 不能以一个参数进行调用的构造函数不应当加上
explicit
; - 接受一个
std::initializer_list
作为参数的构造函数也应当省略explicit
, 以便支持拷贝初始化;
可拷贝类型和可移动类型 #
- 如果类型的拷贝操作不是显而易见的,就不要设置为可拷贝;
- 如果类型可拷贝,那么一定要同时给出拷贝构造函数和赋值操作的定义,反之亦然;
- 如果让类型可拷贝, 同时移动操作的效率高于拷贝操作, 那么就把移动的两个操作 (移动构造函数和赋值操作) 也给出定义;
- 如果类型不可拷贝, 但是移动操作的正确性对用户显然可见, 那么把这个类型设置为只可移动并定义移动的两个操作;
- 如果定义了拷贝/移动操作, 则要保证这些操作的默认实现是正确的. 记得时刻检查默认操作的正确性, 并且在文档中说明类是可拷贝的且/或可移动的;
- 不要为任何有可能有派生类的对象提供赋值操作或者拷贝/移动构造函数 (当然也不要继承有这样的成员函数的类);
- 如果你的基类需要可复制属性, 请提供一个 public virtual Clone() 和一个 protected 的拷贝构造函数以供派生类实现;
- 如果你的类不需要拷贝 / 移动操作, 请显式地通过在 public 域中使用 = delete 或其他手段禁用;
结构体 VS 类 #
- 只有数据成员时使用
struct
: - 用来定义包含数据的被动式对象, 也可以包含相关的常量, 但除了存取数据成员之外, 没有别的函数功能.
- 成员变量直接访问而不是函数调用.
- 除了构造函数, 析构函数,
Initialize()
,Reset()
,Validate()
等类似的用于设定数据成员的函数外, 不能提供其它功能的函数. - 如果需要更多的函数功能,
class
更适合. 如果拿不准, 就用class
. - 除上述功能外其它的一概用
class
; - 为了和 STL 保持一致, 对于仿函数等特性可以不用
class
而是使用struct
; - 注意类和结构体的成员变量使用不同的 命名规则;
继承 #
未完待续
函数 #
参数顺序 #
- 输入参数在前,输出参数在后;
编写简短的函数 #
- 建议编写简短、凝练的函数(不硬性限制函数长度,但超过了 40 行,可以考虑下在不影响程序结构的前提下对其分割);
引用参数 #
- 输入参数是值参或
const
引用, 输出参数为指针;
函数重载 #
- 如果打算重载一个函数,可以试试在函数名中加上参数信息,例如:用
AppendString()
和AppendInt()
等,而不是重载多个Append()
; - 当重载函数是为了支持相同类型不同数量的参数时,则优先考虑使用
vector
;
缺省参数 #
- 虚函数不允许使用缺省参数;
- 也不允许每次调用缺省参数的值都不同的情况下使用缺省参数,例如:
void f(int n = counter+);
函数返回类型后置语法 #
- 只有在常规写法不方便时使用,例如 Lambda 表达式;
来自 Google 的奇技 #
所有权与智能指针 #
- 当你 new 了一块内存时尽量保证在释放前自已一直都持有这个内存的指针,当其他地方需要时传递过去指针或引用,再或者是使用
std::unique_ptr
; - 尽量不要共享所有权,如果确实需要共享,建议使用
std::shared_ptr
; - 不要使用
std::auto_ptr
, 推荐使用std::unique_ptr
;
Cpplint #
cpplint.py
是一个用来分析源文件, 能检查出多种风格错误的工具;- 在行尾加
// NOLINT
, 或在上一行加// NOLINTNEXTLINE
, 可以忽略报错;
命名约定 #
通用命名规则 #
- 尽可能使用描述性的命名,少用缩写(一些特定的广为人知的缩写是允许的,例如:用
i
表示迭代变量和用T
表示模板参数); - 根据经验,如果在 Wikipedia 中列出了的缩写,则它可以考虑被使用;
文件名 #
- 全部用小写,可以包含
_
或-
(更推荐_
); - C++ 文件以
.cc
结尾,头文件以.h
结尾,专门插入文本的文件以.inc
结尾; - 不要使用与系统头文件相同的名字;
- 尽量让文件名更加明确,不要太笼统(例如:使用
http_server_logs.h
而不是logs.h
);
类型命名 #
- 类型名称的每个单词首字母均大写, 不包含下划线;
变量命名 #
- 变量 (包括函数参数) 和数据成员名一律小写, 单词之间用下划线连接;
- 类的成员变量以下划线结尾, 但结构体的就不用;
常量命名 #
- 声明为
constexpr
或const
的变量, 或在程序运行期间其值始终保持不变的, 命名时以k
开头, 大小写混合;
函数命名 #
- 函数名的每个单词首字母大写 (即 “驼峰变量名” 或 “帕斯卡变量名”), 没有下划线;
- 缩写也是将其视为一个单词进行首字母大写,而不是全大写;
- 取值和设值函数的命名与变量一致;
名字空间命名 #
- 命名空间以小写字母命名;
- 最高级命名空间的名字取决于项目名称;
- 注意避免嵌套命名空间的名字之间和常见的顶级命名空间的名字之间发生冲突;
枚举命名 #
宏命名 #
- 不建议使用宏,但一定要用推荐使用全大写单词间以下划线分隔的方式来命名;
命名规则的特例 #
- 如果你命名的实体与已有 C/C++ 实体相似, 可参考 现有命名策略;
注释 #
注释风格 #
- 使用
//
或/* */
, 统一就好. 但//
更常用.
文件注释 #
- 不要在
.h
和.cc
之间复制注释, 保留一份即可.
类注释 #
- 每个类的定义都要附带一份注释, 描述类的功能和用法, 除非它的功能相当明显.
- 描述类用法的注释应当和接口定义放在一起, 描述类的操作和实现的注释应当和实现放在一起.
函数注释 #
- 函数声明处的注释描述函数功能;
- 函数的输入输出.
- 对类成员函数而言: 函数调用期间对象是否需要保持引用参数, 是否会释放这些参数.
- 函数是否分配了必须由调用者释放的空间.
- 参数是否可以为空指针.
- 是否存在函数使用上的性能隐患.
- 如果函数是可重入的, 其同步前提是什么?
- 定义处的注释描述函数实现.
- 如果函数的实现过程中用到了很巧妙的方式, 那么在函数定义处应当加上解释性的注释, 例如, 使用的编程技巧, 实现的大致步骤, 或解释如此实现的理由.
- 不要从
.h
文件或其他地方的函数声明处直接复制注释, 简要重述函数功能是可以的, 但重点要放在如何实现上.
变量注释 #
- 当变量名和类型不能够用明确表达作用, 则应当加上注释(例如特殊值,数据成员之间的关系, 生命周期等);
- 如果变量可以接受
NULL
或-1
等警戒值, 须加以说明;
实现注释 #
- 巧妙或复杂的代码段前要加注释;
- 比较隐晦的地方要在行尾加入注释. 在行尾空两格进行注释;
- 对齐连续多行的注释;
- 如果函数参数的意义不明显, 考虑用下面的方式进行弥补:
- 如果参数是一个字面常量, 并且这一常量在多处函数调用中被使用, 用以推断它们一致, 你应当用一个常量名让这一约定变得更明显, 并且保证这一约定不会被打破.
- 考虑更改函数的签名, 让某个
bool
类型的参数变为enum
类型, 这样可以让这个参数的值表达其意义. - 如果某个函数有多个配置选项, 你可以考虑定义一个类或结构体以保存所有的选项, 并传入类或结构体的实例. 这样的方法有许多优点, 例如这样的选项可以在调用处用变量名引用, 这样就能清晰地表明其意义. 同时也减少了函数参数的数量, 使得函数调用更易读也易写. 除此之外, 以这样的方式, 如果你使用其他的选项, 就无需对调用点进行更改.
- 用具名变量代替大段而复杂的嵌套表达式.
- 万不得已时, 才考虑在调用点用注释阐明参数的意义.
- 不要描述显而易见的现象;
标点, 拼写和语法 #
- 注意标点, 拼写和语法;
TODO 注释 #
TODO
注释要使用全大写的字符串, 在随后的圆括号里写上你的名字, 邮件地址, bug ID 或其它身份标识和这一TODO
相关的 issue;- 如果加 TODO 是为了在 “将来某一天做某事”, 可以附上一个非常明确的时间;
弃用注释 #
- 可以写上包含全大写的
DEPRECATED
的注释, 以标记某接口为弃用状态. 注释可以放在接口声明前, 或者同一行; - 仅仅标记接口为
DEPRECATED
并不会让大家不约而同地弃用, 您还得亲自主动修正调用点;
格式 #
行长度 #
- 每一行代码字符数不超过 80;
- 包含长路径的
#include
语句可以超出80列; - 头文件卫士无视该原则;
非 ASCII 字符 #
- 尽量不使用非 ASCII 字符, 使用时必须使用 UTF-8 编码;
空格还是制表位 #
- 只使用空格, 每次缩进 2 个空格;
- 设置编辑器将制表符转为空格;
函数声明与定义 #
- 返回类型和函数名在同一行;
- 参数尽量放在同一行, 如果放不下就对形参分行, 分行方式与 函数调用 一致;
- 使用好的参数名.
- 只有在参数未被使用或者其用途非常明显时, 才能省略参数名.
- 如果返回类型和函数名在一行放不下, 分行.
- 如果返回类型与函数声明或定义分行了, 不要缩进.
- 左圆括号总是和函数名在同一行.
- 函数名和左圆括号间永远没有空格.
- 圆括号与参数间没有空格.
- 左大括号总在最后一个参数同一行的末尾处, 不另起新行.
- 右大括号总是单独位于函数最后一行, 或者与左大括号同一行.
- 右圆括号和左大括号间总是有一个空格.
- 所有形参应尽可能对齐.
- 缺省缩进为 2 个空格.
- 换行后的参数保持 4 个空格的缩进.
- 未被使用的参数如果其用途不明显的话, 在函数定义处将参数名注释起来;
- 属性, 和展开为属性的宏, 写在函数声明或定义的最前面, 即返回类型之前;
Lambda 表达式 #
- 对形参和函数体的格式化和其他函数一致; 捕获列表同理, 表项用逗号隔开;
函数调用 #
- 要么一行写完函数调用, 要么在圆括号里对参数分行, 要么参数另起一行且缩进四格. 如果没有其它顾虑的话, 尽可能精简行数, 比如把多个参数适当地放在同一行里;
- 如果参数本身是略复杂的表达式,且降低了可读性,那么可以直接创建临时变量描述该表达式并传递给函数;
- 如果参数本身就有一定的结构, 可以酌情地按其结构来决定参数格式;
列表初始化格式 #
- 平时怎么格式化函数调用, 就怎么格式化 列表初始化.
条件语句 #
- 关键字
if
和else
另起一行,else
与if
的}
同一行; if
和(
间都有个空格,)
和{
之间也要有个空格;- 简短的条件语句允许写在同一行,当存在
else
分支时则不允许; - 当语句中某个
if-else
分支使用了大括号时, 其它分支也必须使用;
循环和开关选择语句 #
switch
应该总是包含一个default
匹配, 如果default
应该永远执行不到, 简单的加条assert
;- 空循环体应使用
{}
或continue
, 而不是一个简单的分号;
指针和引用表达式 #
- 在访问成员时, 句点或箭头前后没有空格;
- 指针操作符
*
或&
后没有空格;
布尔表达式 #
- 断行时逻辑操作符(
&&
,||
)总位于行尾;
函数返回值 #
- 不要在
return
表达式里加上非必须的圆括号(类似return (value)
这种形式); - 可以用圆括号把复杂表达式圈起来, 改善可读性;
变量及数组初始化 #
- 用
=
,()
和{}
均可; - 列表初始化(
{}
)不允许整型类型的四舍五入;
预处理指令 #
- 预处理指令不要缩进, 从行首开始, 即使位于缩进代码块中,也应从行首开始;
类格式 #
- 访问控制块的声明依次序是
public:
,protected:
,private:
,每个都缩进 1 个空格; - 所有基类名应在 80 列限制下尽量与子类名放在同一行;
- 除第一个关键词 (一般是 public) 外, 其他关键词前要空一行,这些关键词后不要保留空行..如果类比较小的话也可以不空
构造函数初始值列表 #
- 构造函数初始化列表放在同一行或按四格缩进并排多行;
- 如果不能放在同一行, 必须置于冒号后, 并缩进 4 个空格.
- 如果初始化列表需要置于多行, 将每一个成员放在单独的一行并逐行对齐.
}
可以和{
放在同一行, 如果这样做合适的话.
命名空间格式化 #
- 命名空间内容不要增加额外的缩进层次;
- 声明嵌套命名空间时, 每个命名空间都独立成行;
水平留白 #
通用 #
-{
前总是有空格.
;
前不加空格.- 继承与初始化列表中的
:
前后恒有空格. - 对于单行函数的实现, 在大括号内加上空格, 即
int fun(int v) { return v; }
. - 函数的
{}
里面是空的话, 不加空格. (
的后面和)
的前面都不加空格.
循环和条件语句 #
if
条件语句和循环语句关键字后均有空格.else
前后都有空格.for
循环里;
后有空格.switch case
的:
前无空格, 如果:
后面有代码, 则:
后面加个空格.
操作符 #
- 赋值运算符前后总是有空格.
- 其它二元操作符也前后总有空格, 不过对于表达式的子式可以不加空格.
- 在参数和一元操作符之间不加空格.
模板和转换 #
< >
(尖括号) 不与空格紧邻,<
前没有空格,>
和(
之间也没有.
垂直留白 #
- 两个函数定义之间的空行不要超过 2 行;
- 函数体首尾不要留空行;
- 函数体中也不要随意添加空行;
- 在多重
if-else
块里加空行或许有点可读性;
规则特例 #
现有不合规范的代码 #
- 对于现有不符合既定编程风格的代码,优先与代码原有风格保持一致;
Windows 代码 #
这一部分可以参考指南的原文档。