代码规范

前言

本篇总结一个很重要的修养,在CodeReview时代码的易读性十分重要,CodeReview提升代码质量、减少bug,降低修复成本,团队系统共享。

因此总结下代码命名规范、格式规范、注释规范、结构设计等的规范。

没有规范与风格的代码就是垃圾!!
以后代码必须精炼、鲁棒、规范!!

代码规范

参考:https://zh-google-styleguide.readthedocs.io/en/latest/

文件

自足

一个文件一个类,.h为接口、.cc为实现。每个cc对应一个h。
所有头文件要能够自给自足,用户不需要包含额外的文件来使用头文件。一个模板或内联函数在h中定义,static不用

保护

使用define保护全头文件,命名:<PROJECT>_<PATH>_<FILE>_H_

前置声明

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

内敛函数

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

include的顺序

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

  1. 本文件对应的
  2. C 系统文件
  3. C++ 系统文件
  4. 其他库的 .h 文件
  5. 本项目内 .h 文件

您所依赖的符号 (symbols) 被哪些头文件所定义,您就应该包含(include)哪些头文件,不管某个头是否已经包含过了

作用域

命名空间

在 .cc 文件中定义一个不需要被外部引用的变量时,可以将它们放在匿名命名空间或声明为 static 。但是不要在 .h 文件中这么做。.-为了使声明仅在该模块内
使用具名的命名空间时, 其名称可基于项目名或相对路径. 禁止使用 using 指示(using-directive)。
不要using整个命名空间,不要在头文件中别名-保证文件的独立性,不污染命名空间

静态与全局函数

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

局部变量

将函数变量尽可能置于最小作用域内, 并在变量声明时进行初始化. 离第一次使用越近越好。有一个例外, 如果变量是一个对象, 每次进入作用域都要调用其构造函数, 每次退出作用域都要调用其析构函数. 这会导致效率降低.

构造函数的职责

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

隐式类型转换

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

可拷贝类型和可移动类型

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

结构体 VS. 类

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

继承

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

多重继承

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

运算符重载

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

存取控制

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

声明顺序

类定义一般应以 public: 开始, 后跟 protected:, 最后是 private:. 省略空部分.
在各个部分中, 建议将类似的声明放在一起, 并且建议以如下的顺序: 类型 (包括 typedef, using 和嵌套的结构体与类), 常量, 工厂函数, 构造函数, 赋值运算符, 析构函数, 其它函数, 数据成员.

函数

参数顺序

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

编写简短函数

我们倾向于编写简短, 凝练的函数.
如果函数超过 40 行, 可以思索一下能不能在不影响程序结构的前提下对其进行分割.

引用参数

所有按引用传递的参数必须加上 const.
输入参数是值参或 const 引用, 输出参数为指针. 输入参数可以是 const 指针
输入参数必须是不可更改的,输出才是可改的

函数重载

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

缺省参数

只允许在非虚函数中使用缺省参数, 且必须保证缺省参数的值始终一致. 缺省参数与 函数重载 遵循同样的规则. 一般情况下建议使用函数重载, 尤其是在缺省函数带来的可读性提升不能弥补下文中所提到的缺点的情况下.

后置语法

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

动态分配

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

其他语言特性

变长数组

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

友元

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

异常

我们不使用 C++ 异常.—这个自己定

RTTI

禁止

类型转换

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

只在记录日志时使用流.

前置自增和自减

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

const 用法

我们强烈建议你在任何可能的情况下都要使用 const

整形

C++ 内建整型中, 仅使用 int. 如果程序中需要不同大小的变量, 可以使用 < stdint.h> 中长度精确的整型, 如 int16_t.如果您的变量可能不小于 2^31 (2GiB), 就用 64 位变量比如 int64_t.
无符号数慎重

64 位下的可移植性

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

预处理宏

使用宏时要非常谨慎, 尽量以内联函数, 枚举和常量代替之。不要在 .h 文件中定义宏.

nullptr 和 NULL

整数用 0, 实数用 0.0, 指针用 nullptr 或 NULL, 字符 (串) 用 ‘\0’.
整数用 0, 实数用 0.0, 这一点是毫无争议的. C++11 项目用 nullptr

auto

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

Lambda 表达式

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

模板编程

不要使用复杂的模板编程

命名约定

通用命名规则

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

文件命名

文件名要全部小写,使用_分割单词。定义类时文件名一般成对出现。

类型命名

类型名称的每个单词首字母均大写, 不包含下划线: MyExcitingClass, MyExcitingEnum.
所有类型命名 —— 类, 结构体, 类型定义 (typedef), 枚举, 类型模板参数 —— 均使用相同约定, 即以大写字母开始, 每个单词首字母均大写, 不包含下划线.

变量命名

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

1
2
3
4
5
6
7
8
9
10
11
12
string table_name;
class TableInfo {
...
private:
string table_name_; // 好 - 后加下划线.
static Pool<TableInfo>* pool_; // 好.
};
struct UrlTableProperties {
string name;
int num_entries;
static Pool<UrlTableProperties>* pool;
};

常量命名

声明为 constexpr 或 const 的变量, 或在程序运行期间其值始终保持不变的, 命名时以 “k” 开头, 大小写混合
所有具有静态存储类型的变量 (例如静态变量或全局变量) 都应当以此方式命名

1
const int kDaysInAWeek = 7;

函数命名

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

1
2
3
4
5
6
AddTableEntry()
DeleteUrl()
OpenFileOrDie()
取值和设值函数的命名与变量一致. 一般来说它们的名称与实际的成员变量对应, 但并不强制要求.
例如 int count() 与 void set_count(int count).

(同样的命名规则同时适用于类作用域与命名空间作用域的常量, 因为它们是作为 API 的一部分暴露对外的, 因此应当让它们看起来像是一个函数)

命名空间命名

命名空间以小写字母命名. 最高级命名空间的名字取决于项目名称

枚举命名

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

宏命名

一般不用宏

1
MY_MACRO_THAT_SCARES_SMALL_CHILDREN

注释

注释虽然写起来很痛苦, 但对保证代码可读性至关重要. 下面的规则描述了如何注释以及在哪儿注释. 当然也要记住: 注释固然很重要, 但最好的代码应当本身就是文档. 有意义的类型名和变量名, 要远胜过要用注释解释的含糊不清的名字.

注释风格

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

文件注释

在每一个文件开头加入版权公告.
文件注释描述了该文件的内容.

1
2
3
4
5
6
7
8
9
/*
* Copyright (c) 2013 The WebRTC project authors. All Rights Reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree. An additional intellectual property rights grant can be found
* in the file PATENTS. All contributing project authors may
* be found in the AUTHORS file in the root of the source tree.
*/

类注释

每个类的定义都要附带一份注释, 描述类的功能和用法
如果类的声明和定义分开了(例如分别放在了 .h 和 .cc 文件中), 此时, 描述类用法的注释应当和接口定义放在一起, 描述类的操作和实现的注释应当和实现放在一起.

1
2
3
4
5
6
7
// Iterates over the contents of a GargantuanTable.
// Example:
// GargantuanTableIterator* iter = table->NewIterator();
// for (iter->Seek("foo"); !iter->done(); iter->Next()) {
// process(iter->key(), iter->value());
// }
// delete iter;

函数注释

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

  1. 函数的输入输出.
  2. 对类成员函数而言: 函数调用期间对象是否需要保持引用参数, 是否会释放这些参数.
  3. 函数是否分配了必须由调用者释放的空间.
  4. 参数是否可以为空指针.
  5. 是否存在函数使用上的性能隐患.
  6. 如果函数是可重入的, 其同步前提是什么?

注释构造/析构函数时, 切记读代码的人知道构造/析构函数的功能, 所以 “销毁这一对象” 这样的注释是没有意义的. 你应当注明的是注明构造函数对参数做了什么 (例如, 是否取得指针所有权) 以及析构函数清理了什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Returns an iterator for this table. It is the client's
// responsibility to delete the iterator when it is done with it,
// and it must not use the iterator once the GargantuanTable object
// on which the iterator was created has been deleted.
//
// The iterator is initially positioned at the beginning of the table.
//
// This method is equivalent to:
// Iterator* iter = table->NewIterator();
// iter->Seek("");
// return iter;
// If you are going to immediately seek to another place in the
// returned iterator, it will be faster to use NewIterator()
// and avoid the extra seek.
Iterator* GetIterator() const;

如果函数的实现过程中用到了很巧妙的方式, 那么在函数定义处应当加上解释性的注释. 例如, 你所使用的编程技巧, 实现的大致步骤, 或解释如此实现的理由.

变量注释

通常变量名本身足以很好说明变量用途. 某些情况下, 也需要额外的注释说明.
每个类数据成员 (也叫实例变量或成员变量) 都应该用注释说明用途. 如果有非变量的参数(例如特殊值, 数据成员之间的关系, 生命周期等)不能够用类型与变量名明确表达, 则应当加上注释.
和数据成员一样, 所有全局变量也要注释说明含义及用途, 以及作为全局变量的原因

1
2
3
4
private:
// Used to bounds-check table accesses. -1 means
// that we don't yet know how many entries the table has.
int num_total_entries_;

实现注释

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

代码前、行注释均可
函数实际参数注释:如果函数参数的意义不明显

不要描述显而易见的现象, 永远不要 用自然语言翻译代码作为注释, 除非即使对深入理解 C++ 的读者来说代码的行为都是不明显的. 要假设读代码的人 C++ 水平比你高, 即便他/她可能不知道你的用意

标点, 拼写和语法

注释的通常写法是包含正确大小写和结尾句号的完整叙述性语句. 大多数情况下, 完整的句子比句子片段可读性更高. 短一点的注释, 比如代码行尾注释, 可以随意点, 但依然要注意风格的一致性.

TODO注释

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

1
2
3
// TODO(kl@gmail.com): Use a "*" here for concatenation operator.
// TODO(Zeke) change this to use relations.
// TODO(bug 12345): remove the "Last visitors" feature

弃用注释

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

格式

行长度

每一行代码字符数<80

非ascii

尽量不使用非 ASCII 字符, 使用时必须使用 UTF-8 编码.
别用 C++11 的 char16_t 和 char32_t, 它们和 UTF-8 文本没有关系, wchar_t
存储格式和编码没有关系,字节存就对了

空格还是制表位

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

函数声明与定义

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

1
2
3
4
5
ReturnType ClassName::ReallyLongFunctionName(Type par_name1, Type par_name2,
Type par_name3) {
DoSomething();
...
}

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

函数调用

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

列表初始化格式

与函数调用同

条件语句

倾向于不在圆括号内使用空格. 关键字 if 和 else 另起一行.与()同行

1
2
3
4
5
6
7
if (condition) { // 圆括号里没有空格.
... // 2 空格缩进.
} else if (...) { // else 与 if 的右括号同一行.
...
} else {
...
}

循环和开关选择语句

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

1
2
3
4
5
6
7
8
9
10
11
12
13
switch (var) {
case 0: { // 2 空格缩进
... // 4 空格缩进
break;
}
case 1: {
...
break;
}
default: {
assert(false);
}
}

变量及数组初始化

用 =, () 和 {} 均可.

预处理指令

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

类格式

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MyClass : public OtherClass {
public: // 注意有一个空格的缩进
MyClass(); // 标准的两空格缩进
explicit MyClass(int var);
~MyClass() {}
void SomeFunction();
void SomeFunctionThatDoesNothing() {
}
void set_some_var(int var) { some_var_ = var; }
int some_var() const { return some_var_; }
private:
bool SomeInternalFunction();
int some_var_;
int some_other_var_;
};

构造函数初始值列表

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

1
2
3
4
5
6
7
// 如果初始化列表需要置于多行, 将每一个成员放在单独的一行
// 并逐行对齐
MyClass::MyClass(int var)
: some_var_(var), // 4 space indent
some_other_var_(var + 1) { // lined up
DoSomething();
}

命名空间格式化

命名空间内容不缩进

垂直留白

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

代码逻辑注意点

函数参数数量一般不要超过三个
函数功能尽力单一!尽力单一!
类职责单一!
错误处理统一!
接口设计