C++编程规范笔记

原文链接:

https://zh-google-styleguide.readthedocs.io/en/latest/google-cpp-styleguide/contents/

头文件

Self-contained 头文件

头文件应该能够自给自足(self-contained,也就是可以作为第一个头文件被引入),以 .h 结尾。至于用来插入文本的文件,说到底它们并不是头文件,所以应以 .inc 结尾。不允许分离出 -inl.h 头文件的做法.

#define保护

所有头文件都应该使用 #define 来防止头文件被多重包含, 命名格式当是: <PROJECT>_<PATH>_<FILE>_H_ ,如下:

1
2
3
4
#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_
...
#endif // FOO_BAR_BAZ_H_

前置声明

尽可能地避免使用前置声明。使用 #include 包含需要的头文件即可。

内联函数

只有当函数只有 10 行甚至更少时才将其定义为内联函数.

#include 的路径及顺序

使用标准的头文件包含顺序可增强可读性, 避免隐藏依赖: 相关头文件, C 库, C++ 库, 其他库的 .h, 本项目内的 .h.

作用域

命名空间

鼓励在 .cc 文件内使用匿名命名空间或 static 声明. 使用具名的命名空间时, 其名称可基于项目名或相对路径. 禁止使用 using 指示(using-directive)。禁止使用内联命名空间(inline namespace)。且要在命名空间的最后注释出命名空间的名字。

匿名命名空间和静态变量

.cc 文件中定义一个不需要被外部引用的变量时,可以将它们放在匿名命名空间或声明为 static 。但是不要在 .h 文件中这么做。

匿名命名空间的声明和具名的格式相同,在最后注释上 namespace

非成员函数、静态成员函数和全局函数

使用静态成员函数或命名空间内的非成员函数, 尽量不要用裸的全局函数. 将一系列函数直接置于命名空间中,不要用类的静态方法模拟出命名空间的效果,类的静态方法应当和类的实例或静态数据紧密相关.

局部变量

将函数变量尽可能置于最小作用域内, 并在变量声明时进行初始化.

【注】有一个例外, 如果变量是一个对象, 每次进入作用域都要调用其构造函数, 每次退出作用域都要调用其析构函数. 这会导致效率降低.

静态和全局变量

禁止定义静态储存周期非POD变量,禁止使用含有副作用的函数初始化POD全局变量,因为多编译单元中的静态变量执行时的构造和析构顺序是未明确的,这将导致代码的不可移植。

构造函数的职责

不要在构造函数中调用虚函数, 也不要在无法报出错误时进行可能失败的初始化.

如果对象需要进行有意义的 (non-trivial) 初始化, 考虑使用明确的 Init() 方法或使用工厂模式.

隐式类型转换

不要定义隐式类型转换. 对于转换运算符和单参数构造函数, 请使用 explicit 关键字.

一个例外是, 拷贝和移动构造函数不应当被标记为 explicit, 因为它们并不执行类型转换.

可拷贝类型和可移动类型

如果你的类型需要, 就让它们支持拷贝 / 移动. 否则, 就把隐式产生的拷贝和移动函数禁用.

由于存在对象切割的风险, 不要为任何有可能有派生类的对象提供赋值操作或者拷贝 / 移动构造函数 (当然也不要继承有这样的成员函数的类). 如果你的基类需要可复制属性, 请提供一个 public virtual Clone() 和一个 protected 的拷贝构造函数以供派生类实现.

结构体 VS. 类

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

继承

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

必要的话, 析构函数声明为 virtual. 如果你的类有虚函数, 则析构函数也应该为虚函数,数据成员都必须是私有的。

对于重载的虚函数或虚析构函数, 使用 override, 或 (较不常用的) final 关键字显式地进行标记. 较早 (早于 C++11) 的代码可能会使用 virtual 关键字作为不得已的选项. 因此, 在声明重载时, 请使用 override, finalvirtual 的其中之一进行标记. 标记为 overridefinal 的析构函数如果不是对基类虚函数的重载的话, 编译会报错, 这有助于捕获常见的错误. 这些标记起到了文档的作用, 因为如果省略这些关键字, 代码阅读者不得不检查所有父类, 以判断该函数是否是虚函数.

多重继承

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

接口

接口是指满足特定条件的类, 这些类以 Interface 为后缀 (不强制).

当一个类满足以下要求时, 称之为纯接口:

  • 只有纯虚函数 (“=0”) 和静态函数 (除了下文提到的析构函数).
  • 没有非静态数据成员.
  • 没有定义任何构造函数. 如果有, 也不能带有参数, 并且必须为 protected.
  • 如果它是一个子类, 也只能从满足上述条件并以 Interface 为后缀的类继承.

运算符重载

除少数特定环境外, 不要重载运算符. 也不要创建用户定义字面量.

存取控制

所有 数据成员声明为 private, 除非是 static const 类型成员

存取函数一般内联在头文件中。

声明顺序

将相似的声明放在一起, 将 public 部分放在最前.

类定义一般应以 public: 开始, 后跟 protected:, 最后是 private:. 省略空部分.

函数

函数顺序

函数的参数顺序为: 输入参数在先, 后跟输出参数.

编写简短函数

我们倾向于编写简短, 凝练的函数.

引用参数

所有按引用传递的参数必须加上 const.

总而言之, 大多时候输入形参往往是 const T&. 若用 const T* 则说明输入另有处理. 所以若要使用 const T*, 则应给出相应的理由, 否则会使得读者感到迷惑.

函数重载

若要使用函数重载, 则必须能让读者一看调用点就胸有成竹, 而不用花心思猜测调用的重载函数到底是哪一种. 这一规则也适用于构造函数.

如果重载函数的目的是为了支持不同数量的同一类型参数, 则优先考虑使用 std::vector 以便使用者可以用列表初始化指定参数.

缺省参数

只允许在非虚函数中使用缺省参数, 且必须保证缺省参数的值始终一致. 缺省参数与 函数重载 遵循同样的规则. 一般情况下建议使用函数重载。

函数返回类型后置语法

只有在常规写法 (返回类型前置) 不便于书写或不便于阅读时使用返回类型后置语法.

来自Google的奇技

所有权与智能指针

动态分配出的对象最好有单一且固定的所有主, 并通过智能指针传递所有权.

不要使用 std::auto_ptr, 使用 std::unique_ptr 代替它.

Cpplint

使用 cpplint.py 检查风格错误,可以单独下载使用。

其他C++特性

引用参数

所有按引用传递的参数必须加上 const.

右值引用

只在定义移动构造函数与移动赋值操作时使用右值引用. 不要使用 std::forward.

变长数组和 alloca()

我们不允许使用变长数组和 alloca(),改用更安全的分配器(allocator),就像 std::vectorstd::unique_ptr<T[]>.

友元

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

某些情况下, 相对于将类成员声明为 public, 使用友元是更好的选择, 尤其是如果你只允许另一个类访问该类的私有成员时. 当然, 大多数类都只应该通过其提供的公有成员进行互操作.

异常

我们不使用 C++ 异常.

运行时类型识别

我们禁止使用 RTTI。

RTTI允许程序员在运行时识别C++类对象的类型,通过使用typeid或者dynamic_cast完成。

在单元测试中可以使用 RTTI, 但是在其他代码中请尽量避免.

类型转换

使用 C++ 的类型转换, 如 static_cast<>(). 不要使用 int y = (int)xint y = int(x) 等转换方式

只在记录日志时使用流.

不要使用流, 除非是日志接口需要. 使用 printf 之类的代替.

前置自增和自减

对于迭代器和其他模板对象使用前缀形式 (++i) 的自增, 自减运算符.

const 用法

我们强烈建议你在任何可能的情况下都要使用 const. 此外有时改用 C++11 推出的 constexpr 更好。

constexpr 用法

在 C++11 里,用 constexpr 来定义真正的常量,或实现常量初始化。

整型

C++ 内建整型中, 仅使用 int. 如果程序中需要不同大小的变量, 可以使用 <stdint.h> 中长度精确的整型, 如 int16_t.如果您的变量可能不小于 2^31 (2GiB), 就用 64 位变量比如 int64_t. 此外要留意,哪怕您的值并不会超出 int 所能够表示的范围,在计算过程中也可能会溢出。所以拿不准时,干脆用更大的类型。

不要为了指出数值永不会为负, 而使用无符号类型. 相反, 你应该使用断言来保护数据.

64位下的可移植性

代码应该对 64 位和 32 位系统友好. 处理打印, 比较, 结构体对齐时应切记。

预处理宏

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

  • 不要在 .h 文件中定义宏.
  • 在马上要使用时才进行 #define, 使用后要立即 #undef.
  • 不要只是对已经存在的宏使用#undef,选择一个不会冲突的名称;
  • 不要试图使用展开后会导致 C++ 构造不稳定的宏, 不然也至少要附上文档说明其行为.
  • 不要用 ## 处理函数,类和变量的名字。

0, nullptrNULL

整数用 0, 实数用 0.0, 指针用 nullptrNULL, 字符 (串) 用 '\0'.

整数用 0, 实数用 0.0, 这一点是毫无争议的.

对于指针 (地址值), 到底是用 0, NULL 还是 nullptr. C11 项目用 nullptr; C03 项目则用 NULL, 毕竟它看起来像指针。实际上,一些 C++ 编译器对 NULL 的定义比较特殊,可以输出有用的警告,特别是 sizeof(NULL) 就和 sizeof(0) 不一样。

字符 (串) 用 '\0', 不仅类型正确而且可读性好.

sizeof

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

使用 sizeof(varname) 是因为当代码中变量类型改变时会自动更新. 您或许会用 sizeof(type) 处理不涉及任何变量的代码,比如处理来自外部或内部的数据格式,这时用变量就不合适了。

auto

auto 绕过烦琐的类型名,只要可读性好就继续用,别用在局部变量之外的地方。

列表初始化

你可以用列表初始化。但千万别直接列表初始化 auto 变量。

Lambda表达式

适当使用 lambda 表达式。别用默认 lambda 捕获,所有捕获都要显式写出来。

模板编程

不要使用复杂的模板编程

Boost库

只使用 Boost 中被认可的库.

C++11

适当用 C11(前身是 C0x)的库和语言扩展,在贵项目用 C++11 特性前三思可移植性。

命名约定

通用命令规则

函数命名, 变量命名, 文件命名要有描述性; 少用缩写.

文件命名

文件名要全部小写, 可以包含下划线 (_) 或连字符 (-), 依照项目的约定. 如果没有约定, 那么 “_” 更好.

可接受的文件命名示例:

  • my_useful_class.cc
  • my-useful-class.cc
  • myusefulclass.cc

类型命名

类型名称的每个单词首字母均大写, 不包含下划线: MyExcitingClass, MyExcitingEnum.

变量命名

变量 (包括函数参数) 和数据成员名一律小写, 单词之间用下划线连接. 类的成员变量以下划线结尾, 但结构体的就不用, 如: a_local_variable, a_struct_data_member, a_class_data_member_.

常量命名

声明为 constexprconst 的变量, 或在程序运行期间其值始终保持不变的, 命名时以 “k” 开头, 大小写混合. 例如:

1
const int kDaysInAWeek = 7;

函数命名

常规函数使用大小写混合, 取值和设值函数则要求与变量名匹配: MyExcitingFunction(), MyExcitingMethod(), my_exciting_member_variable(), set_my_exciting_member_variable().

一般来说, 函数名的每个单词首字母大写 (即 “驼峰变量名” 或 “帕斯卡变量名”), 没有下划线. 对于首字母缩写的单词, 更倾向于将它们视作一个单词进行首字母大写 (例如, 写作 StartRpc() 而非 StartRPC()).

命名空间命名

命名空间以小写字母命名. 最高级命名空间的名字取决于项目名称. 要注意避免嵌套命名空间的名字之间和常见的顶级命名空间的名字之间发生冲突.

枚举命名

枚举的命名应当和常量或宏一致: kEnumName 或是 ENUM_NAME.

宏命名

如果你一定要用宏, 像这样命名: MY_MACRO_THAT_SCARES_SMALL_CHILDREN.

命名规则的特例

如果你命名的实体与已有 C/C++ 实体相似, 可参考现有命名策略.

注释

注释风格

使用 ///* */, 统一就好.

文件注释

在每一个文件开头加入版权公告.

类注释

每个类的定义都要附带一份注释, 描述类的功能和用法, 除非它的功能相当明显.

函数注释

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

变量注释

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

如果变量可以接受 NULL-1 等警戒值, 须加以说明.

实现注释

对于代码中巧妙的, 晦涩的, 有趣的, 重要的地方加以注释.

标点, 拼写和语法

注意标点, 拼写和语法; 写的好的注释比差的要易读的多.

TODO注释

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

弃用注释

通过弃用注释(DEPRECATED comments)以标记某接口点已弃用.

格式

行长度

每一行代码字符数不超过 80.

非ASCII字符

尽量不使用非 ASCII 字符, 使用时必须使用 UTF-8 编码.

空格还是制表符

只使用空格, 每次缩进 2 个空格.

我们使用空格缩进. 不要在代码中使用制表符. 你应该设置编辑器将制表符转为空格.

函数声明与定义

返回类型和函数名在同一行, 参数也尽量放在同一行, 如果放不下就对形参分行, 分行方式与 函数调用一致.

注意以下几点:

  • 使用好的参数名.
  • 只有在参数未被使用或者其用途非常明显时, 才能省略参数名.
  • 如果返回类型和函数名在一行放不下, 分行.
  • 如果返回类型与函数声明或定义分行了, 不要缩进.
  • 左圆括号总是和函数名在同一行.
  • 函数名和左圆括号间永远没有空格.
  • 圆括号与参数间没有空格.
  • 左大括号总在最后一个参数同一行的末尾处, 不另起新行.
  • 右大括号总是单独位于函数最后一行, 或者与左大括号同一行.
  • 右圆括号和左大括号间总是有一个空格.
  • 所有形参应尽可能对齐.
  • 缺省缩进为 2 个空格.
  • 换行后的参数保持 4 个空格的缩进.

Lambda表达式

Lambda 表达式对形参和函数体的格式化和其他函数一致; 捕获列表同理, 表项用逗号隔开.

函数调用

要么一行写完函数调用, 要么在圆括号里对参数分行, 要么参数另起一行且缩进四格. 如果没有其它顾虑的话, 尽可能精简行数, 比如把多个参数适当地放在同一行里.

如果同一行放不下, 可断为多行, 后面每一行都和第一个实参对齐, 左圆括号后和右圆括号前不要留空格。

参数也可以放在次行, 缩进四格。

列表初始化格式

您平时怎么格式化函数调用, 就怎么格式化列表初始化。

条件语句

倾向于不在圆括号内使用空格. 关键字 ifelse 另起一行.

注意所有情况下 if 和左圆括号间都有个空格. 右圆括号和左大括号之间也要有个空格

循环和开关选择语句

switch 语句可以使用大括号分段, 以表明 cases 之间不是连在一起的. 在单语句循环里, 括号可用可不用. 空循环体应使用 {}continue,而不是一个简单的分号.

指针和引用表达式

句点或箭头前后不要有空格. 指针/地址操作符 (*, &) 之后不能有空格.

在多重声明中不能使用 & 或 *。

布尔表达式

如果一个布尔表达式超过 标准行宽, 断行方式要统一一下.

函数返回值

不要在 return 表达式里加上非必须的圆括号.

变量及数组初始化

=, (){} 均可.

请务必小心列表初始化 {...}std::initializer_list 构造函数初始化出的类型. 非空列表初始化就会优先调用 std::initializer_list, 不过空列表初始化除外, 后者原则上会调用默认构造函数. 为了强制禁用 std::initializer_list 构造函数, 请改用括号.

此外, 列表初始化不允许整型类型的四舍五入, 这可以用来避免一些类型上的编程失误.

预处理指令

预处理指令不要缩进, 从行首开始.

即使预处理指令位于缩进代码块中, 指令也应从行首开始.

类格式

访问控制块的声明依次序是 public:, protected:, private:, 每个都缩进 1 个空格.

  • 所有基类名应在 80 列限制下尽量与子类名放在同一行.
  • 关键词 public:, protected:, private: 要缩进 1 个空格.
  • 除第一个关键词 (一般是 public) 外, 其他关键词前要空一行. 如果类比较小的话也可以不空.
  • 这些关键词后不要保留空行.
  • public 放在最前面, 然后是 protected, 最后是 private.

构造函数初始值列表

构造函数初始化列表放在同一行或按四格缩进并排多行.

命名空间格式化

命名空间内容不缩进.

水平留白

水平留白的使用根据在代码中的位置决定. 永远不要在行尾添加没意义的留白.

添加冗余的留白会给其他人编辑时造成额外负担. 因此, 行尾不要留空格. 如果确定一行代码已经修改完毕, 将多余的空格去掉; 或者在专门清理空格时去掉。

垂直留白

垂直留白越少越好.

这不仅仅是规则而是原则问题了: 不在万不得已, 不要使用空行. 尤其是: 两个函数定义之间的空行不要超过 2 行, 函数体首尾不要留空行, 函数体中也不要随意添加空行.

规则特例

现有不合规范的代码

对于现有不符合既定编程风格的代码可以网开一面.

Windows代码

Windows 程序员有自己的编程习惯, 主要源于 Windows 头文件和其它 Microsoft 代码. 我们希望任何人都可以顺利读懂你的代码, 所以针对所有平台的 C++ 编程只给出一个单独的指南.

结束语

运用常识和判断力, 并且 保持一致.