本文中的规范大部分是参考 google 发布的 c++ 编码规范,有同学翻译成了中文版本。但这个规范我觉得顺序有点乱,有些地方讨论太详细,有些地方又表达得不清楚。因此整理,挑选了部分规范。
1. 文件编码格式
1.1 文件编码:统一使用 utf-8 编码
如果文件编码不统一,又有同学使用中文注释的话,就很容易出现乱码的问题。应该所有的文本编辑器都是支持 utf-8 编码的。Linux 的文本编辑器默认使用的都是 utf-8 编码。但 windows 的系统文本编辑器大部分默认编码是 gbk, 所以使用 windows 的同学需要检测一下自己使用的代码编辑器是否已经设置默认编码为 utf-8 了。
1.2 换行符:统一使用 unix 换行符
换行符有三种:unix(LF), macos(CR), windows(CRLF)。其中, CR 表示回车(ASCII 13, \r), LF 表示换行(ASCII 10, \n)。
虽然我们在代码编辑器里面看不出来有什么区别。但在一些特殊情况下是会有影响的。比如板子上运行的 shell 脚本,如果换行符是 windows 换行符的话,可能就会执行错误了。为了避免这类问题,我们要求统一使用 unix 换行符。如果现有文件的换行符是 windows 的话,可以通过 dos2unix 这个工具来做转换。代码编辑器也是可以设置默认换行符的。
2. 命名约定
最重要的一致性规则是命名管理。 命名的风格能让我们在不需要去查找类型声明的条件下快速地了解某个名字代表的含义: 类型, 变量, 函数, 常量, 宏, 等等。 甚至, 我们大脑中的模式匹配引擎非常依赖这些命名规则。命名规则具有一定随意性, 但相比按个人喜好命名, 一致性更重要, 所以无论你认为它们是否重要, 规则总归是要遵守的。
2.1 通用命名规则
- 总述
函数命名, 变量命名, 文件命名要有描述性; 少用缩写。 - 说明
尽可能使用描述性的命名, 别心疼空间, 毕竟相比之下让代码易于新读者理解更重要。 不要用只有项目开发者能理解的缩写, 也不要通过砍掉几个字母来缩写单词。 - 示例
好的命名:不好的命名:1
2
3int price_count_reader; // 无缩写
int num_errors; // "num" 是一个常见的写法
int num_dns_connections; // 人人都知道 "DNS" 是什么1
2
3
4
5
6int n; // 毫无意义.
int nerr; // 含糊不清的缩写.
int n_comp_conns; // 含糊不清的缩写.
int wgc_connections; // 只有贵团队知道是什么意思.
int pc_reader; // "pc" 有太多可能的解释了.
int cstmr_id; // 删减了若干字母.
例外,一些特定的广为人知的缩写是允许的, 例如用 i 表示迭代变量和用 T 表示模板参数。
2.2 文件命名
- 总述
文件名要全部小写, 单词之间使用下划线_
连接。 - 说明
C++ 文件要以 .cpp 结尾, 头文件以 .h 结尾。
不要使用已经存在于 /usr/include 下的文件名 (注: 即编译器搜索系统头文件的路径), 如 string.h。
通常应尽量让文件名更加明确, http_server_logs.h 就比 logs.h 要好。 定义类时文件名一般成对出现, 如 foo_bar.h 和 foo_bar.cpp, 对应于类 FooBar。 - 示例
可接受的文件命名:不接受的文件命名:1
http_server_logs.h
1
2httpserverlogs.h // 没有单词分隔,看得眼花
http-server-logs.cpp // 单词分隔没有使用下划线 _
2.3 类型命名
- 总述
类型名称的每个单词首字母均大写, 不包含下划线,比如: MyExcitingClass, MyExcitingEnum。 - 说明
所有类型命名 (类, 结构体, 类型定义 (typedef), 枚举, 类型模板参数) 均使用相同约定, 即以大写字母开始, 每个单词首字母均大写, 不包含下划线。 - 示例
1
2
3
4
5
6
7
8
9
10
11
12
13// 类和结构体
class UrlTable {}
class UrlTableTester {}
struct UrlTableProperties {}
// 枚举
enum UrlTableErrors {}
// typedef 别名
typedef hash_map<UrlTableProperties *, string> PropertiesMap;
// using 别名
using PropertiesMap = hash_map<UrlTableProperties *, string>;
2.4 变量命名
- 总述
变量 (包括函数参数) 和数据成员名一律小写, 单词之间用下划线连接。 类的成员变量以下划线结尾, 但结构体的就不用, 如: a_local_variable, a_struct_data_member, a_class_data_member_。 - 说明
类的成员变量最后面需要添加下划线,结构体的成员变量不需要在最后面添加下划线。因类的成员变量我们要求是都是 private 的,不能直接访问。 加下载线是为了容易区分这个变量是成员变量还是局部变量。结构体的成员变量默认是 public 的,我们使用结构体的一般用法也是要直接访问其成员变量的。另外,结构体一般也没有方法,不需要区分成员变量和局部变量,所以后面不用加下划线。 - 示例
普通变量命名类数据成员1
2string table_name; // 好 - 用下划线
string tableName; // 差 - 混合大小写
不管是静态的还是非静态的, 类数据成员都可以和普通变量一样, 但要接下划线。结构体变量1
2
3
4
5
6class TableInfo {
...
private:
string table_name_; // 好 - 后加下划线
static Pool<TableInfo>* pool_; // 好
};
不管是静态的还是非静态的, 结构体数据成员都可以和普通变量一样, 不用像类那样接下划线。1
2
3
4
5struct UrlTableProperties {
string name;
int num_entries;
static Pool<UrlTableProperties>* pool;
};
2.5 常量命名
- 总述
声明为 constexpr 或 const 的变量, 或在程序运行期间其值始终保持不变的, 命名时以 “k” 开头, 大小写混合. - 说明
所有具有静态存储类型的变量 (例如静态变量或全局变量, 参见 存储类型) 都应当以此方式命名。 - 示例
1
const int kDaysInAWeek = 7;
2.6 函数命名
- 总述
常规函数使用大小写混合, 取值和设值函数则要求与变量名匹配: MyExcitingFunction(), MyExcitingMethod(), my_exciting_member_variable(), set_my_exciting_member_variable()。 - 说明
一般来说, 函数名的每个单词首字母大写 (即 “驼峰变量名” 或 “帕斯卡变量名”), 没有下划线。 对于首字母缩写的单词, 更倾向于将它们视作一个单词进行首字母大写。例如, 写作 StartRpc() 而非 StartRPC()。 - 示例
1
2
3
4
5AddTableEntry()
DeleteUrl()
OpenFileOrDie()
count() // 取值
set_count(int count) // 设值
2.7 枚举命名
- 总述
枚举的命名应当和 常量 一致: kEnumName 。 - 说明
单独的枚举值应该优先采用常量的命名方式。 枚举名 UrlTableErrors (以及 AlternateUrlTableErrors) 是类型, 所以要用大小写混合的方式。 - 示例
1
2
3
4
5enum UrlTableErrors {
kOK = 0,
kErrorOutOfMemory,
kErrorMalformedInput,
};
2.8 宏命名
- 总述
一般来说不推荐使用宏定义,应该使用常量或者内联函数替代。如果你一定要用, 像这样命名: MY_MACRO_THAT_SCARES_SMALL_CHILDREN。 - 说明
通常不应该使用宏. 如果不得不用, 其命名全部大写, 使用下划线分隔单词。像 LOGD 这种调试信息,需要输出函数名,位置信息的,就必须得用宏定义了。 - 示例
1
2
3. 头文件
通常每一个 .cpp 文件都有一个对应的 .h 文件。 也有一些常见例外, 如单元测试代码和只包含 main() 函数的 .cpp 文件。
正确使用头文件可令代码在可读性、文件大小和性能上大为改观。下面的规则将引导你规避使用头文件时的各种陷阱。
3.1 Self-contained 头文件
- 总述
看 google 原来的规范,写了一大堆,还是不太明白。个人的理解是:头文件本身依赖的其它头文件,需要全部包含。 - 说明
确保你的 header files 包含了你需要的所有东西, 而不是假设你 #include 进来的某个(些)headers 帮你包含了你需要的东西。 - 示例
不好的做法:1
2// A.h
1
2// B.h
有 A.h, B.h, C.h 三个头文件,A.h 中需要使用 C.h 中的 MyClass 类,虽然你 #include B.h 也实现了使用 MyClass 的目的,但是这并不是编程的最佳实践。这种写法会令人感到困惑, MyClass 到底是来自于哪里?最好的做法是在 A.h 中直接 #include C.h。1
2// C.h
class MyClass{};
另一种不好的做法:1
2
3
4
5// my_class.h
class MyClass
{
MyClass(std::string s);
};这里 my_class.h 就不是 self-contained 的,他需要1
2
3
4
5// my_class.cpp
MyClass::MyClass(std::string s)
{}#include <string>
才能编译。上面的代码虽然编译不会带来问题,前提是 cpp 中你把#include <string>
放在#include "my_class.h
的前面。如果将来某一天你不小心把#include "my_class.h
放在了#include <string>
前面, 就会报 string 未定义错误。cpp可以认为是单纯的使用某个头文件,我们不应该对头文件使用者做出任何假设。因此头文件要做到 self-contained。正确的做法是在 my_class.h 中#include <string>
。
3.2 避免头文件重复包含
- 总述
所有头文件都应该使用 #define 来防止头文件被重复包含, 命名格式当是:<PROJECT>_<PATH>_<FILE>_H_
。 - 说明
为保证唯一性, 头文件的命名应该基于所在项目源代码树的全路径。 例如, 项目 foo 中的头文件 foo/src/bar/baz.h 可按如下方式保护: - 示例
1
2
3
4
...
3.3 前置声明
- 总述
尽可能地避免使用前置声明。使用 #include 包含需要的头文件即可。 - 说明
所谓「前置声明」(forward declaration)是类、函数和模板的纯粹声明,没伴随着其定义。
优点:
- 前置声明能够节省编译时间,多余的 #include 会迫使编译器展开更多的文件,处理更多的输入。
- 前置声明能够节省不必要的重新编译的时间。 #include 使代码因为头文件中无关的改动而被重新编译多次。
缺点:
- 前置声明隐藏了依赖关系,头文件改动时,用户的代码会跳过必要的重新编译过程。
- 前置声明可能会被库的后续更改所破坏。前置声明函数或模板有时会妨碍头文件开发者变动其 API. 例如扩大形参类型,加个自带默认参数的模板形参等等。
- 前置声明来自命名空间 std:: 的 symbol 时,其行为未定义。
- 前置声明了不少来自头文件的 symbol 时,就会比单单一行的 include 冗长。
- 仅仅为了能前置声明而重构代码(比如用指针成员代替对象成员)会使代码变得更慢更复杂。
- 极端情况下,用前置声明代替 includes 甚至都会暗暗地改变代码的含义。
3.4 #include 的路径及顺序
- 总述
使用标准的头文件包含顺序可增强可读性, 避免隐藏依赖,头文件的包含顺序应为: 相关头文件(foobar.cpp 的相关头文件为 foobar.h) –> C 库 –> C++ 库 –> 其他库的 .h –> 本项目内的 .h。
在这些类型的头文件之间使用空行来分隔开。 - 说明
项目内头文件应按照项目源代码目录树结构排列, 避免使用 UNIX 特殊的快捷目录.
(当前目录) 或..
(上级目录)。 例如, google-awesome-project/src/base/logging.h 应该按如下方式包含:1
- 示例
google-awesome-project/src/foo/internal/fooserver.cpp 的头文件包含顺序如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
// c 库头文件
// c++ 库头文件
// 本项目内的其他头文件
4. 作用域
4.1 局部变量
- 总述
将函数变量尽可能置于最小作用域内, 并在变量声明时进行初始化。 - 说明
C++ 允许在函数的任何位置声明变量。 我们提倡在尽可能小的作用域中声明变量, 离第一次使用越近越好。 这使得代码浏览者更容易定位变量声明的位置, 了解变量的类型和初始值。 特别是,应使用初始化的方式替代声明再赋值, 比如:属于 if, while 和 for 语句的变量应当在这些语句中正常地声明,这样子这些变量的作用域就被限制在这些语句中了,举例而言:1
2
3
4
5
6
7int i;
i = f(); // 坏——初始化和声明分离
int j = g(); // 好——初始化时声明
vector<int> v;
v.push_back(1); // 用花括号初始化更好
v.push_back(2);
vector<int> v = {1, 2}; // 好——v 一开始就初始化1
while (const char* p = strchr(str, '/')) str = p + 1;
- 例外
有一个例外, 如果变量是一个对象, 每次进入作用域都要调用其构造函数, 每次退出作用域都要调用其析构函数, 这会导致效率降低。在循环作用域外面声明这类变量要高效的多:1
2
3
4
5// 低效的实现
for (int i = 0; i < 1000000; ++i) {
Foo f; // 构造函数和析构函数分别调用 1000000 次!
f.DoSomething(i);
}1
2
3
4Foo f; // 构造函数和析构函数只调用 1 次
for (int i = 0; i < 1000000; ++i) {
f.DoSomething(i);
}
4.2 静态和全局变量
- 总述
非基本类型变量的变量,不能作为全局变量和静态变量。静态变量或者全局变量不能通过函数调用来赋初始值 - 说明
基本类型变量是指 char, int, float, double 等。全局变量和静态变量是在静态存储区的,它们的初始化顺序是不确定的, 如果一个 class 类型的变量作为作为静态变量,则它的构造函数、析构函数和初始化的顺序在 C++ 中是只有部分明确的,甚至随着构建变化而变化,导致难以发现的 bug。
4.3 非成员函数、静态成员函数和全局函数
- 总述
使用静态成员函数或命名空间内的非成员函数, 尽量不要用裸的全局函数。 将一系列函数直接置于命名空间中,不要用类的静态方法模拟出命名空间的效果,类的静态方法应当和类的实例或静态数据紧密相关。 - 说明
有时, 把函数的定义同类的实例脱钩是有益的, 甚至是必要的. 这样的函数可以被定义成静态成员, 或是非成员函数。 非成员函数不应依赖于外部变量, 应尽量置于某个命名空间内。 相比单纯为了封装若干不共享任何静态数据的静态成员函数而创建类, 不如使用命名空间(namespace) 。 - 示例
对于头文件 myproject/foo_bar.h , 应当使用而非1
2
3
4
5
6namespace myproject {
namespace foo_bar {
void Function1();
void Function2();
} // namespace foo_bar
} // namespace myproject定义在同一编译单元的函数, 被其他编译单元直接调用可能会引入不必要的耦合和链接时依赖; 静态成员函数对此尤其敏感。 可以考虑提取到新类中, 或者将函数置于独立库的命名空间内。1
2
3
4
5
6
7namespace myproject {
class FooBar {
public:
static void Function1();
static void Function2();
};
} // namespace myproject
如果你必须定义非成员函数, 又只是在当前 .cpp 文件中使用它, 应该 static 关键字限定其作用域在当前文件内。如:1
2
3static void Foobar() {
DoSomething();
}
5. 类
类是 C++ 中代码的基本单元。 显然, 它们被广泛使用。 本节列举了在写一个类时的主要注意事项。
5.1 构造函数
- 总述
不要在构造函数中调用虚函数, 也不要在构造函数中抛异常。
-说明
这两个问题需要花点篇幅才能讲明白,这里不讨论,请参考 永远不要在构造函数和析构函数中调用虚函数 和 不要在构造函数和析构函数中抛异常
5.2 存取控制
- 总述
将所有数据成员声明为 private, 除非是 static const 类型成员 (遵循 常量命名规则)。
5.3 声明顺序
- 总述
将相似的声明放在一起, 将 public 部分放在最前。 - 说明
类定义一般应以 public 开始, 后跟 protected, 最后是 private。
在各个部分中, 建议将类似的声明放在一起, 并且建议以如下的顺序: 类型 (包括 typedef, using 和嵌套的结构体与类), 常量, 工厂函数, 构造函数, 赋值运算符, 析构函数, 其它函数, 数据成员。 - 注意
不要将大段的函数定义内联在类定义中。 通常,只有那些普通的, 或性能关键且短小的函数可以内联在类定义中。
6. 函数
6.1 参数顺序
- 总述
函数的参数顺序为: 输入参数在先, 输出参数在后。 - 说明
C/C++ 中的函数参数或者是函数的输入, 或者是函数的输出, 或兼而有之。 输入参数通常是值参或 const 引用, 输出参数或输入/输出参数则一般为非 const 指针。 在排列参数顺序时, 将所有的输入参数置于输出参数之前。 特别要注意, 在加入新参数时不要因为它们是新参数就置于参数列表最后, 而是仍然要按照前述的规则, 即将新的输入参数也置于输出参数之前。 - 示例
1
bool foobar(const int foo_in, const MyClass &bar_in, int *result_out);
6.2 编写简短函数
- 总述
我们倾向于编写简短, 凝练的函数。 - 说明
我们承认长函数有时是合理的, 因此并不硬性限制函数的长度。 如果函数超过 40 行, 可以思索一下能不能在不影响程序结构的前提下对其进行分割。
即使一个长函数现在工作的非常好, 一旦有人对其修改, 有可能出现新的问题, 甚至导致难以发现的 bug。 使函数尽量简短, 以便于他人阅读和修改代码。
在处理代码时, 你可能会发现复杂的长函数。 不要害怕修改现有代码: 如果证实这些代码使用 / 调试起来很困难, 或者你只需要使用其中的一小段代码, 考虑将其分割为更加简短并易于管理的若干函数。
6.3 引用参数
- 总述
所有按引用传递的参数必须加上 const。 - 定义
在 C 语言中, 如果函数需要修改变量的值, 参数必须为指针, 如 int foo(int *pval)。 在 C++ 中, 函数还可以声明为引用参数: int foo(int &val)。 - 优点
定义引用参数可以防止出现 (*pval)++ 这样丑陋的代码。 引用参数对于拷贝构造函数这样的应用也是必需的, 同时也更明确地不接受空指针。 - 缺点
容易引起误解, 因为引用在语法上是值变量却拥有指针的语义。 - 结论
函数参数列表中, 所有引用参数都必须是 const:
1 | void Foo(const string &in, string *out); |
事实上这在 Google Code 是一个硬性约定: 输入参数是值参或 const 引用, 输出参数为指针。 输入参数可以是 const 指针, 但决不能是非 const 的引用参数, 除非特殊要求, 比如 swap()。
有时候, 在输入形参中用 const T* 指针比 const T& 更明智, 比如:
- 可能会传递空指针。
- 函数要把指针或对地址的引用赋值给输入形参。
总而言之, 大多时候输入形参往往是 const T&。 若用 const T* 则说明输入另有处理。 所以若要使用 const T*, 则应给出相应的理由, 否则会使得读者感到迷惑。
6.4 函数重载
- 总述
若要使用函数重载, 则必须能让读者一看调用点就胸有成竹, 而不用花心思猜测调用的重载函数到底是哪一种。 这一规则也适用于构造函数。 - 定义
你可以编写一个参数类型为 const string& 的函数, 然后用另一个参数类型为 const char* 的函数对其进行重载:1
2
3
4
5class MyClass {
public:
void Analyze(const string &text);
void Analyze(const char *text, size_t textlen);
}; - 优点
通过重载参数不同的同名函数, 可以令代码更加直观。 模板化代码需要重载, 这同时也能为使用者带来便利。 - 缺点
如果函数单靠不同的参数类型而重载 (acgtyrant 注:这意味着参数数量不变), 读者就得十分熟悉 C++ 五花八门的匹配规则, 以了解匹配过程具体到底如何。 另外, 如果派生类只重载了某个函数的部分变体, 继承语义就容易令人困惑。 - 结论
如果打算重载一个函数, 可以试试改在函数名里加上参数信息。 例如, 用 AppendString() 和 AppendInt() 等, 而不是一口气重载多个 Append()。 如果重载函数的目的是为了支持不同数量的同一类型参数, 则优先考虑使用 std::vector 以便使用者可以用 列表初始化 指定参数。
6.5 缺省参数
- 总述
只允许在非虚函数中使用缺省参数, 且必须保证缺省参数的值始终一致。 缺省参数与 函数重载 遵循同样的规则。 一般情况下建议使用函数重载, 尤其是在缺省函数带来的可读性提升不能弥补下文中所提到的缺点的情况下。 - 优点
有些函数一般情况下使用默认参数, 但有时需要又使用非默认的参数。 缺省参数为这样的情形提供了便利, 使程序员不需要为了极少的例外情况编写大量的函数。和函数重载相比, 缺省参数的语法更简洁明了, 减少了大量的样板代码, 也更好地区别了 “必要参数” 和 “可选参数”。 - 缺点
- 缺省参数实际上是函数重载语义的另一种实现方式, 因此所有
不应当使用函数重载的理由
也都适用于缺省参数。 - 虚函数调用的缺省参数取决于目标对象的静态类型, 此时无法保证给定函数的所有重载声明的都是同样的缺省参数。
- 缺省参数是在每个调用点都要进行重新求值的, 这会造成生成的代码迅速膨胀。 作为读者, 一般来说也更希望缺省的参数在声明时就已经被固定了, 而不是在每次调用时都可能会有不同的取值。
- 缺省参数会干扰函数指针, 导致函数签名与调用点的签名不一致. 而函数重载不会导致这样的问题。
- 结论
对于虚函数, 不允许使用缺省参数, 因为在虚函数中缺省参数不一定能正常工作。如果在每个调用点缺省参数的值都有可能不同, 在这种情况下缺省函数也不允许使用。 例如, 不要写像void f(int n = counter++);
这样的代码。
在其他情况下, 如果缺省参数对可读性的提升远远超过了以上提及的缺点的话, 可以使用缺省参数。 如果仍有疑惑, 就使用函数重载。
7. 注释
注释虽然写起来很痛苦, 但对保证代码可读性至关重要。 下面的规则描述了如何注释以及在哪儿注释。 当然也要记住: 注释固然很重要, 但最好的代码应当本身就是文档。 有意义的类型名和变量名, 要远胜过要用注释解释的含糊不清的名字。
你写的注释是给代码读者看的, 也就是下一个需要理解你的代码的人。 所以慷慨些吧, 下一个读者可能就是你!
7.1 注释风格
- 总述
统一使用//
注释风格。 - 说明
除非是更改遗留代码,遗留代码的风格是使用/* */
,那就跟着用/* */
。
除非你的英文很好,可以描述清楚,否则还是使用中文注释吧。
7.2 文件注释
- 总述
在每一个文件开头加入版权公告。
文件注释描述了该文件的内容. 如果一个文件只声明, 或实现, 或测试了一个对象, 并且这个对象已经在它的声明处进行了详细的注释, 那么就没必要再加上文件注释。 除此之外的其他文件都需要文件注释。 - 说明
- 作者信息
版权信息里面包含作者信息,可以炫耀你的成就, 也是为了出问题别人可以找你。
如果你对原始作者的文件做了重大修改, 请考虑删除原作者信息,增加你自己的信息。 - 文件内容
如果一个 .h 文件声明了多个概念, 则文件注释应当对文件的内容做一个大致的说明, 同时说明各概念之间的联系。 一个一到两行的文件注释就足够了, 对于每个概念的详细文档应当放在各个概念中, 而不是文件注释中。
不要在 .h 和 .cc 之间复制注释, 这样的注释偏离了注释的实际意义。
具体的文件注释格式,请参考项目中现有的格式。
7.3 类注释
- 总述
每个类的定义都要附带一份注释, 描述类的功能和用法, 除非它的功能相当明显。比如:1
2
3
4
5
6
7
8
9
10// 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;
class GargantuanTableIterator {
...
}; - 说明
类注释应当为读者理解如何使用与何时使用类提供足够的信息, 同时应当提醒读者在正确使用此类时应当考虑的因素。 如果类有任何同步前提, 请用文档说明。 如果该类的实例可被多线程访问, 要特别注意文档说明多线程环境下相关的规则和常量使用。
如果你想用一小段代码演示这个类的基本用法或通常用法, 放在类注释里也非常合适。
如果类的声明和定义分开了(例如分别放在了 .h 和 .cc 文件中), 此时, 描述类用法的注释应当和接口定义放在一起。 描述类的操作和实现的注释应当和实现放在一起。
7.4 函数注释
- 总述
函数声明处的注释描述函数功能; 定义处的注释描述函数实现。 - 说明
- 函数声明
基本上每个函数声明处前都应当加上注释, 描述函数的功能和用途。 只有在函数的功能简单而明显时才能省略这些注释(例如, 简单的取值和设值函数)。
函数声明处注释的内容:- 函数的输入输出.
- 对类成员函数而言: 函数调用期间对象是否需要保持引用参数, 是否会释放这些参数.
- 函数是否分配了必须由调用者释放的空间.
- 参数是否可以为空指针.
- 是否存在函数使用上的性能隐患.
- 如果函数是可重入的, 其同步前提是什么?
举例如下:
1 | // Returns an iterator for this table. It is the client's |
但也要避免罗罗嗦嗦, 或者对显而易见的内容进行说明. 下面的注释就没有必要加上 “否则返回 false”, 因为已经暗含其中了:
1 | // Returns true if the table cannot hold any more entries. |
注释函数重载时, 注释的重点应该是函数中被重载的部分, 而不是简单的重复被重载的函数的注释。 多数情况下, 函数重载不需要额外的文档, 因此也没有必要加上注释。
注释构造/析构函数时, 切记读代码的人知道构造/析构函数的功能, 所以 “销毁这一对象” 这样的注释是没有意义的。 你应当注明的是注明构造函数对参数做了什么 (例如, 是否取得指针所有权) 以及析构函数清理了什么。 如果都是些无关紧要的内容, 直接省掉注释。 析构函数前没有注释是很正常的。
2. 函数定义
如果函数的实现过程中用到了很巧妙的方式, 那么在函数定义处应当加上解释性的注释。 例如, 你所使用的编程技巧, 实现的大致步骤, 或解释如此实现的理由。 举个例子, 你可以说明为什么函数的前半部分要加锁而后半部分不需要。
不要从 .h 文件或其他地方的函数声明处直接复制注释。 简要重述函数功能是可以的, 但注释重点要放在如何实现上。
7.5 变量注释
- 总述
通常变量名本身足以很好说明变量用途。 某些情况下, 也需要额外的注释说明. - 说明
- 类数据成员
每个类数据成员 (也叫实例变量或成员变量) 都应该用注释说明用途。 如果有非变量的参数(例如特殊值, 数据成员之间的关系, 生命周期等)不能够用类型与变量名明确表达, 则应当加上注释。 然而, 如果变量类型与变量名已经足以描述一个变量, 那么就不再需要加上注释。
特别地, 如果变量可以接受 NULL 或 -1 等警戒值, 须加以说明. 比如:1
2
3
4private:
// Used to bounds-check table accesses. -1 means
// that we don't yet know how many entries the table has.
int num_total_entries_; - 全局变量
和数据成员一样, 所有全局变量也要注释说明含义及用途, 以及作为全局变量的原因。 比如:1
2// The total number of tests cases that we run through in this regression test.
const int kNumTestCases = 6;
- 类数据成员
7.6 实现注释
- 总述
对于代码中巧妙的, 晦涩的, 有趣的, 重要的地方加以注释。 - 说明
- 代码前注释
巧妙或复杂的代码段前要加注释. 比如:1
2
3
4
5
6
7// Divide result by two, taking into account that x
// contains the carry from the add.
for (int i = 0; i < result->size(); i++) {
x = (x << 8) + (*result)[i];
(*result)[i] = x >> 1;
x &= 1;
} - 行注释
比较隐晦的地方要在行尾加入注释, 在行尾空两格进行注释. 比如:注意, 这里用了两段注释分别描述这段代码的作用, 和提示函数返回时错误已经被记入日志.1
2
3
4// If we have enough memory, mmap the data portion too.
mmap_budget = max<int64>(0, mmap_budget - index_->length());
if (mmap_budget >= data_size_ && !MmapData(mmap_chunk_bytes, mlock))
return; // Error already logged.
如果你需要连续进行多行注释, 可以使之对齐获得更好的可读性:1
2DoSomething(); // Comment here so the comments line up.
DoSomethingElseThatIsLonger(); // Two spaces between the code and the comment. - 函数参数注释
如果函数参数的意义不明显, 考虑用下面的方式进行弥补:- 如果参数是一个字面常量, 并且这一常量在多处函数调用中被使用, 用以推断它们一致, 你应当用一个常量名让这一约定变得更明显, 并且保证这一约定不会被打破.
- 考虑更改函数的签名, 让某个 bool 类型的参数变为 enum 类型, 这样可以让这个参数的值表达其意义.
- 如果某个函数有多个配置选项, 你可以考虑定义一个类或结构体以保存所有的选项, 并传入类或结构体的实例. 这样的方法有许多优点, 例如这样的选项可以在调用处用变量名引用, 这样就能清晰地表明其意义. 同时也减少了函数参数的数量, 使得函数调用更易读也易写. 除此之外, 以这样的方式, 如果你使用其他的选项, 就无需对调用点进行更改.
- 用具名变量代替大段而复杂的嵌套表达式.
- 万不得已时, 才考虑在调用点用注释阐明参数的意义.
比如下面的示例的对比:和1
2// What are these arguments?
const DecimalNumber product = CalculateProduct(values, 7, false, nullptr);哪个更清晰一目了然。1
2
3
4ProductOptions options;
options.set_precision_decimals(7);
options.set_use_cache(ProductOptions::kDontUseCache);
const DecimalNumber product = CalculateProduct(values, options, /*completion_callback=*/nullptr);
- 不允许的行为
不要描述显而易见的现象, 永远不要 用自然语言翻译代码作为注释, 除非即使对深入理解 C++ 的读者来说代码的行为都是不明显的。 要假设读代码的人 C++ 水平比你高, 即便他/她可能不知道你的用意。
你所提供的注释应当解释代码为什么要这么做和代码的目的, 或者最好是让代码自文档化.
比较这样的注释:和这样的注释:1
2
3
4
5// Find the element in the vector. <-- 差: 这太明显了!
auto iter = std::find(v.begin(), v.end(), element);
if (iter != v.end()) {
Process(element);
}自文档化的代码根本就不需要注释. 上面例子中的注释对下面的代码来说就是毫无必要的:1
2
3
4
5// Process "element" unless it was already processed.
auto iter = std::find(v.begin(), v.end(), element);
if (iter != v.end()) {
Process(element);
}1
2
3if (!IsAlreadyProcessed(element)) {
Process(element);
}
- 代码前注释
7.7 TODO 注释
- 总述
对那些临时的, 短期的解决方案, 或已经够好但仍不完美的代码使用 TODO 注释。 - 说明
TODO 注释要使用全大写的字符串 TODO, 在随后的圆括号里写上你的名字, 邮件地址, bug ID, 或其它身份标识和与这一 TODO 相关的 issue。 主要目的是让添加注释的人 (也是可以请求提供更多细节的人) 可根据规范的 TODO 格式进行查找. 添加 TODO 注释并不意味着你要自己来修正, 因此当你加上带有姓名的 TODO 时, 一般都是写上自己的名字。如果加 TODO 是为了在 “将来某一天做某事”, 可以附上一个非常明确的时间 (“Fix by November 2005”), 或者一个明确的事项 (“Remove this code when all clients can handle XML responses.”)。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
7.8 弃用注释
- 总述
通过弃用注释(DEPRECATED comments)以标记某接口点已弃用。 - 说明
您可以写上包含全大写的 DEPRECATED 的注释, 以标记某接口为弃用状态. 注释可以放在接口声明前, 或者同一行。
在 DEPRECATED 一词后, 在括号中留下您的名字, 邮箱地址以及其他身份标识。
弃用注释应当包涵简短而清晰的指引, 以帮助其他人修复其调用点。 在 C++ 中, 你可以将一个弃用函数改造成一个内联函数, 这一函数将调用新的接口。
仅仅标记接口为 DEPRECATED 并不会让大家不约而同地弃用, 您还得亲自主动修正调用点(callsites), 或是找个帮手。
修正好的代码应该不会再涉及弃用接口点了, 着实改用新接口点。 如果您不知从何下手, 可以找标记弃用注释的当事人一起商量。
8. 格式
每个人都可能有自己的代码风格和格式, 但如果一个项目中的所有人都遵循同一风格的话, 这个项目就能更顺利地进行。 每个人未必能同意下述的每一处格式规则, 而且其中的不少规则需要一定时间的适应, 但整个项目服从统一的编程风格是很重要的, 只有这样才能让所有人轻松地阅读和理解代码。
8.1 缩进
- 总述
统一使用 4 个空格。 - 说明
不要使用 Tab 键来缩进,不然其他同学看你的代码的时候,缩进可能就是乱七八糟的。虽然大多数文本编辑器上默认 Tab 键的长度是 4 个空格,你自己看起来没问题。但也有部分编辑器默认 tab 是 2 个空格或者 8 个空格的。比如 gerrit 的默认配置好像就是 tab 的显示宽度是 8 个空格。我在 gerrit 上 review 代码的时候,经常看到乱七八糟的缩进。所有的文本编辑器都是可以设置 tab 转换成 4 个空格的。最好设置一下自动转换,这样当你不小心使用了 tab 来缩进的时候,编辑器会帮你改成 4 个空格。
Attention 例外:Makefile 要求命令是以 tab 缩进的
8.2 函数声明与定义
- 总述
返回类型和函数名在同一行, 参数也尽量放在同一行, 如果放不下就对形参分行。 - 说明
函数看上去像这样:如果同一行文本太多, 放不下所有参数:1
2
3
4ReturnType ClassName::FunctionName(Type par_name1, Type par_name2) {
DoSomething();
...
}甚至连第一个参数都放不下:1
2
3
4
5ReturnType ClassName::ReallyLongFunctionName(Type par_name1, Type par_name2,
Type par_name3) {
DoSomething();
...
}1
2
3
4
5
6
7ReturnType LongClassName::ReallyReallyReallyLongFunctionName(
Type par_name1,
Type par_name2,
Type par_name3) {
DoSomething();
...
} - 注意以下几点:
- 使用好的参数名,参考命名规范。
- 只有在参数未被使用或者其用途非常明显时, 才能省略参数名。
- 如果返回类型和函数名在一行放不下, 分行。
- 如果返回类型与函数声明或定义分行了, 不要缩进。
- 左圆括号总是和函数名在同一行。
- 函数名和左圆括号间永远没有空格。
- 圆括号与参数间没有空格。
- 左大括号总在最后一个参数同一行的末尾处, 不另起新行。
- 右大括号总是单独位于函数最后一行, 或者与左大括号同一行。
- 右圆括号和左大括号间总是有一个空格。
8.3 函数调用
- 总述
要么一行写完函数调用, 要么在圆括号里对参数分行, 要么参数另起一行且缩进四格。 如果没有其它顾虑的话, 尽可能精简行数, 比如把多个参数适当地放在同一行里. - 说明
函数调用遵循如下形式:如果同一行放不下, 可断为多行, 后面每一行都和第一个实参对齐, 左圆括号后和右圆括号前不要留空格:1
bool retval = DoSomething(argument1, argument2, argument3);
参数也可以放在次行, 缩进四格:1
2bool retval = DoSomething(averyveryveryverylongargument1,
argument2, argument3);如果一系列参数本身就有一定的结构, 可以酌情地按其结构来决定参数格式:1
2
3
4
5
6
7
8if (...) {
...
...
if (...) {
DoSomething(
argument1, argument2, // 4 空格缩进
argument3, argument4);
}1
2
3
4// 通过 3x3 矩阵转换 widget.
my_widget.Transform(x1, x2, x3,
y1, y2, y3,
z1, z2, z3);
8.4 条件/循环语句
- 总述
以下规则适用于 if 条件判断,和 for, while 循环。
if 后面即使只有一行代码,也一定要加{}
if
与()
之间有空格,()
与条件之间不需要空格。 - 说明
有些同学觉得 if 条件后面就只有一行代码而已,加不加 {} 无所谓:但我们强制要加 {},因为我们经历过了几次不加 {}, 引起的惨痛教训。假如有以上代码,最初的时候只有一行,不加 {},但是后来人维护的时候,需要在满足这个条件的情况下,做些其他操作,于是他加了一行代码:1
2if (condition)
doSomething();于是坑就被埋下了。1
2
3if (condition)
doSomething();
doOtherThing();doOtherThing();
在任何条件下都会执行的。 - 示例
1
2
3
4if(condition) // 差 - IF 后面没空格.
if (condition){ // 差 - { 前面没空格.
if(condition){ // 变本加厉地差.
if (condition) { // 好 - IF 和 { 都与空格紧邻.
8.5 指针和引用表达式
- 总述
句点或箭头前后不要有空格。 指针/地址操作符 (*, &) 之后不能有空格。 - 说明
下面是指针和引用表达式的正确使用范例:在声明指针变量或参数时, 星号统一与变量名紧挨:1
2
3
4x = *p;
p = &x;
x = r.y;
x = r->y;1
2
3
4
5
6
7
8
9
10
11
12
13
14// 好, 星号统变量名紧挨
char *c;
const string &str;
// 差, 星号与类型紧挨
char* c;
const string& str;
// 差, */& 两边都有空格
char * c;
const string & str;
// 不允许 - 在多重声明中不能使用 & 或 *
int x, *y;
8.6 预处理指令
- 总述
预处理指令不要缩进, 从行首开始。 - 说明
即使预处理指令位于缩进代码块中, 指令也应从行首开始。 - 示例
1
2
3
4
5
6
7// 好 - 指令从行首开始
if (lopsided_score) {
DropEverything();
BackToNormal();
}1
2
3
4
5
6
7// 差 - 指令缩进
if (lopsided_score) {
DropEverything();
BackToNormal();
}
8.7 类格式
- 总述
访问控制块的声明依次序是 public, protected, private。public 这些关键字不需要缩进。 - 说明
类声明 (下面的代码中缺少注释, 参考 类注释) 的基本格式如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24class MyClass : public OtherClass {
public: // 不需要空格缩进
// 先构造/析构函数
MyClass(); // 四空格缩进
explicit MyClass(int var);
~MyClass() {}
// 对外提供的方法
void SomeFunction();
void SomeFunctionThatDoesNothing() {
}
// set/get 方法
void set_some_var(int var) { some_var_ = var; }
int some_var() const { return some_var_; }
private:
// 内部处理,不对外提供的方法
bool SomeInternalFunction();
// 成员变量,所有成员变量都要是 private 的
int some_var_;
int some_other_var_;
};
8.8 命名空间格式化
- 总述
命名空间内容不缩进. - 说明
命名空间 不要增加额外的缩进层次, 例如:不要在命名空间内缩进:1
2
3
4
5
6
7namespace {
void foo() { // 正确. 命名空间内没有额外的缩进.
...
}
} // namespace声明嵌套命名空间时, 每个命名空间都独立成行.1
2
3
4
5
6
7
8namespace {
// 错, 缩进多余了.
void foo() {
...
}
} // namespace1
2namespace foo {
namespace bar {
8.9 水平留白
- 总述
水平留白的使用根据在代码中的位置决定. 永远不要在行尾添加没意义的留白. - 说明
- 通用添加冗余的留白会给其他人编辑时造成额外负担. 因此, 行尾不要留空格. 如果确定一行代码已经修改完毕, 将多余的空格去掉; 或者在专门清理空格时去掉(尤其是在没有其他人在处理这件事的时候). (注: 现在大部分代码编辑器稍加设置后, 都支持自动删除行首/行尾空格, 如果不支持, 考虑换一款编辑器或 IDE)
1
2
3
4
5
6
7
8
9
10
11
12void f(bool b) { // 左大括号前总是有空格.
...
int i = 0; // 分号前不加空格.
// 继承与初始化列表中的冒号前后恒有空格.
class Foo : public Bar {
public:
// 对于单行函数的实现, 在大括号内加上空格
// 然后是函数实现
Foo(int b) : Bar(), baz_(b) {} // 大括号里面是空的话, 不加空格.
void Reset() { baz_ = 0; } // 用括号把大括号与实现分开.
... - 循环和条件语句
1
2
3
4
5
6
7
8
9
10
11
12
13
14if (b) { // if 条件语句和循环语句关键字后均有空格.
} else { // else 前后有空格.
}
while (test) {} // 圆括号内部不紧邻空格.
switch (i) {
for (int i = 0; i < 5; ++i) {
switch ( i ) { // 循环和条件语句的圆括号里可以与空格紧邻.
if ( test ) { // 圆括号, 但这很少见. 总之要一致.
for ( int i = 0; i < 5; ++i ) {
for ( ; i < 5 ; ++i) { // 循环里内 ; 后恒有空格, ; 前可以加个空格.
switch (i) {
case 1: // switch case 的冒号前无空格.
...
case 2: break; // 如果冒号有代码, 加个空格. - 操作符
1
2
3
4
5
6
7
8
9
10
11
12
13
14// 赋值运算符前后总是有空格.
x = 0;
// 其它二元操作符也前后恒有空格, 不过对于表达式的子式可以不加空格.
// 圆括号内部没有紧邻空格.
v = w * x + y / z;
v = w*x + y/z;
v = w * (x + z);
// 在参数和一元操作符之间不加空格.
x = -5;
++x;
if (x && !y)
... - 模板和转换
1
2
3
4
5
6// 尖括号(< and >) 不与空格紧邻, < 前没有空格, > 和 ( 之间也没有.
vector<string> x;
y = static_cast<char*>(x);
// 在类型与指针操作符之间留空格也可以, 但要保持一致.
vector<char *> x;
- 通用
8.10垂直留白
- 总述
垂直留白越少越好. - 说明
这不仅仅是规则而是原则问题了: 不在万不得已, 不要使用空行. 尤其是: 两个函数定义之间的空行不要超过 2 行, 函数体首尾不要留空行, 函数体中也不要随意添加空行.
基本原则是: 同一屏可以显示的代码越多, 越容易理解程序的控制流. 当然, 过于密集的代码块和过于疏松的代码块同样难看, 有时候在不同的逻辑块中添加空行分隔,会显示更有条理。
9. c++ 特性
c++ 11 之后增加了一些新的语法特性,使用这些语法特性我觉得可以改善代码可读性,安全性。
9.1 nullptr
nullptr 是为了补充并替代 NULL 的,由于之前老版本的 NULL 定义一般为 0, 但有时候又被编译器定义为((void*)0)。这样就会出现混乱,特别是进行函数重载的时候,就会让编译器搞不清楚 NULL 的具体类型,因此,引入 nullptr 可以更好的区分 0 和 空指针, 因此,在新版中,尽量使用 nullptr 代表空指针进行初始化。
9.2 initializer_list
在C++11之前,我们无法以花括号的形式初始一个 vector:
1 | std::vector<int> vec = {1, 2, 3, 4, 5}; |
但这在C++11却是可行的,因为其引入了初始化列表 std::initializer_list。
initializer_list 为参数个数不固定的函数提供了一个简单的解决方案:
1 |
|
9.3 auto 类型
在C++中最烦的就算是各种类型声明的编写,太多字母了,而且有时候也会忘记,由于他们的类型定义太多太乱了!因此 C++11 中使用 auto 对数据类型进行自动推导。新版中,已经弃用了之前有类似功能的 register 关键字,变得更加强大,比如下面例子:
1 | for(vector<int>::const_iterator itr = vec.cbegin(); itr != vec.cend(); ++itr) |
9.4 范围for语句
相信学过 python 的同学都很清楚,在 python 中经常使用的 for 语句是 for….in….,十分的方便,而在 C 中 for 循环是又丑又长,C++ 标准为了简化代码量,提供了新的范围 for 语句:for(auto c : str);
1 | // C风格 |
智能指针
我们知道c++的内存管理是让很多人头疼的事,当我们写一个new语句时,一般就会立即把delete语句直接也写了,但是我们不能避免程序还未执行到delete时就跳转了或者在函数中没有执行到最后的delete语句就返回了,如果我们不在每一个可能跳转或者返回的语句前释放资源,就会造成内存泄露。使用智能指针可以很大程度上的避免这个问题。智能指针本质上是使用了引用计数的方法来自动释放内存的。具体细节这里就不讨论了,感兴趣的同学请自行查找相关资料。