目录
1. 头文件
通常每一个 .cc
文件都有一个对应的 .h
文件. 也有一些常见例外, 如单元测试代码和只包含 main()
函数的 .cc
文件.
正确使用头文件可令代码在可读性、文件大小和性能上大为改观.
下面的规则将引导你规避使用头文件时的各种陷阱.
1.1. Self-contained 头文件
Tip
头文件应该能够自给自足(self-contained,也就是可以作为第一个头文件被引入),以 .h
结尾。至于用来插入文本的文件,说到底它们并不是头文件,所以应以 .inc
结尾。不允许分离出 -inl.h
头文件的做法.
所有头文件要能够自给自足。换言之,用户和重构工具不需要为特别场合而包含额外的头文件。详言之,一个头文件要有 1.2. #define 保护,统统包含它所需要的其它头文件,也不要求定义任何特别 symbols.
不过有一个例外,即一个文件并不是 self-contained 的,而是作为文本插入到代码某处。或者,文件内容实际上是其它头文件的特定平台(platform-specific)扩展部分。这些文件就要用 .inc
文件扩展名。
如果 .h
文件声明了一个模板或内联函数,同时也在该文件加以定义。凡是有用到这些的 .cc
文件,就得统统包含该头文件,否则程序可能会在构建中链接失败。不要把这些定义放到分离的 -inl.h
文件里(译者注:过去该规范曾提倡把定义放到 -inl.h 里过)。
有个例外:如果某函数模板为所有相关模板参数显式实例化,或本身就是某类的一个私有成员,那么它就只能定义在实例化该模板的 .cc
文件里。
1.2. #define 保护
Tip
所有头文件都应该使用 #define
来防止头文件被多重包含, 命名格式当是: <PROJECT>_<PATH>_<FILE>_H_
.
为保证唯一性, 头文件的命名应该基于所在项目源代码树的全路径. 例如, 项目 foo
中的头文件 foo/src/bar/baz.h
可按如下方式保护:
#ifndef FOO_BAR_BAZ_H_ #define FOO_BAR_BAZ_H_ ... #endif // FOO_BAR_BAZ_H_
1.3. 前置声明
Tip
尽可能地避免使用前置声明。使用 #include
包含需要的头文件即可。
定义:
所谓「前置声明」(forward declaration)是类、函数和模板的纯粹声明,没伴随着其定义.
优点:
- 前置声明能够节省编译时间,多余的
#include
会迫使编译器展开更多的文件,处理更多的输入。- 前置声明能够节省不必要的重新编译的时间。
#include
使代码因为头文件中无关的改动而被重新编译多次。
缺点:
前置声明隐藏了依赖关系,头文件改动时,用户的代码会跳过必要的重新编译过程。
前置声明可能会被库的后续更改所破坏。前置声明函数或模板有时会妨碍头文件开发者变动其 API. 例如扩大形参类型,加个自带默认参数的模板形参等等。
前置声明来自命名空间
std::
的 symbol 时,其行为未定义。很难判断什么时候该用前置声明,什么时候该用
#include
。极端情况下,用前置声明代替#include
甚至都会暗暗地改变代码的含义:// b.h: struct B {}; struct D : B {}; // good_user.cc: #include "b.h" void f(B*); void f(void*); void test(D* x) { f(x); } // calls f(B*)如果
#include
被B
和D
的前置声明替代,test()
就会调用f(void*)
.
- 前置声明了不少来自头文件的 symbol 时,就会比单单一行的
include
冗长。- 仅仅为了能前置声明而重构代码(比如用指针成员代替对象成员)会使代码变得更慢更复杂.
结论:
- 尽量避免前置声明那些定义在其他项目中的实体.
- 函数:总是使用
#include
.- 类模板:优先使用
#include
.
至于什么时候包含头文件,参见 1.5. #include 的路径及顺序 。
1.4. 内联函数
Tip
只有当函数只有 10 行甚至更少时才将其定义为内联函数.
定义:
当函数被声明为内联函数之后, 编译器会将其内联展开, 而不是按通常的函数调用机制进行调用.
优点:
只要内联的函数体较小, 内联该函数可以令目标代码更加高效. 对于存取函数以及其它函数体比较短, 性能关键的函数, 鼓励使用内联.
缺点:
滥用内联将导致程序变得更慢. 内联可能使目标代码量或增或减, 这取决于内联函数的大小. 内联非常短小的存取函数通常会减少代码大小, 但内联一个相当大的函数将戏剧性的增加代码大小. 现代处理器由于更好的利用了指令缓存, 小巧的代码往往执行更快。
结论:
一个较为合理的经验准则是, 不要内联超过 10 行的函数. 谨慎对待析构函数, 析构函数往往比其表面看起来要更长, 因为有隐含的成员和基类析构函数被调用!
另一个实用的经验准则: 内联那些包含循环或
switch
语句的函数常常是得不偿失 (除非在大多数情况下, 这些循环或switch
语句从不被执行).有些函数即使声明为内联的也不一定会被编译器内联, 这点很重要; 比如虚函数和递归函数就不会被正常内联. 通常, 递归函数不应该声明成内联函数.(YuleFox 注: 递归调用堆栈的展开并不像循环那么简单, 比如递归层数在编译时可能是未知的, 大多数编译器都不支持内联递归函数). 虚函数内联的主要原因则是想把它的函数体放在类定义内, 为了图个方便, 抑或是当作文档描述其行为, 比如精短的存取函数.
1.5. #include
的路径及顺序
Tip
使用标准的头文件包含顺序可增强可读性, 避免隐藏依赖: 相关头文件, C 库, C++ 库, 其他库的 .h, 本项目内的 .h.
项目内头文件应按照项目源代码目录树结构排列, 避免使用 UNIX 特殊的快捷目录 .
(当前目录) 或 ..
(上级目录). 例如, google-awesome-project/src/base/logging.h
应该按如下方式包含:
#include "base/logging.h"
又如, dir/foo.cc
或 dir/foo_test.cc
的主要作用是实现或测试 dir2/foo2.h
的功能, foo.cc
中包含头文件的次序如下:
dir2/foo2.h
(优先位置, 详情如下)- C 系统文件
- C++ 系统文件
- 其他库的
.h
文件- 本项目内
.h
文件
这种优先的顺序排序保证当 dir2/foo2.h
遗漏某些必要的库时, dir/foo.cc
或 dir/foo_test.cc
的构建会立刻中止。因此这一条规则保证维护这些文件的人们首先看到构建中止的消息而不是维护其他包的人们。
dir/foo.cc
和 dir2/foo2.h
通常位于同一目录下 (如 base/basictypes_unittest.cc
和 base/basictypes.h
), 但也可以放在不同目录下.
按字母顺序分别对每种类型的头文件进行二次排序是不错的主意。注意较老的代码可不符合这条规则,要在方便的时候改正它们。
您所依赖的符号 (symbols) 被哪些头文件所定义,您就应该包含(include)哪些头文件,前置声明 (forward declarations) 情况除外。比如您要用到 bar.h
中的某个符号, 哪怕您所包含的 foo.h
已经包含了 bar.h
, 也照样得包含 bar.h
, 除非 foo.h
有明确说明它会自动向您提供 bar.h
中的 symbol. 不过,凡是 cc 文件所对应的「相关头文件」已经包含的,就不用再重复包含进其 cc 文件里面了,就像 foo.cc
只包含 foo.h
就够了,不用再管后者所包含的其它内容。
举例来说, google-awesome-project/src/foo/internal/fooserver.cc
的包含次序如下:
#include "foo/public/fooserver.h" // 优先位置 #include <sys/types.h> #include <unistd.h> #include <hash_map> #include <vector> #include "base/basictypes.h" #include "base/commandlineflags.h" #include "foo/public/bar.h"
例外:
有时,平台特定(system-specific)代码需要条件编译(conditional includes),这些代码可以放到其它 includes 之后。当然,您的平台特定代码也要够简练且独立,比如:
#include "foo/public/fooserver.h" #include "base/port.h" // For LANG_CXX11. #ifdef LANG_CXX11 #include <initializer_list> #endif // LANG_CXX11
2. 作用域
2.1. 命名空间
Tip
鼓励在 .cc
文件内使用匿名命名空间或 static
声明. 使用具名的命名空间时, 其名称可基于项目名或相对路径. 禁止使用 using 指示(using-directive)。禁止使用内联命名空间(inline namespace)。
定义:
命名空间将全局作用域细分为独立的, 具名的作用域, 可有效防止全局作用域的命名冲突.
优点:
虽然类已经提供了(可嵌套的)命名轴线 (YuleFox 注: 将命名分割在不同类的作用域内), 命名空间在这基础上又封装了一层.
举例来说, 两个不同项目的全局作用域都有一个类
Foo
, 这样在编译或运行时造成冲突. 如果每个项目将代码置于不同命名空间中,project1::Foo
和project2::Foo
作为不同符号自然不会冲突.内联命名空间会自动把内部的标识符放到外层作用域,比如:
namespace X { inline namespace Y { void foo(); } // namespace Y } // namespace X
X::Y::foo()
与X::foo()
彼此可代替。内联命名空间主要用来保持跨版本的 ABI 兼容性。
缺点:
命名空间具有迷惑性, 因为它们使得区分两个相同命名所指代的定义更加困难。
内联命名空间很容易令人迷惑,毕竟其内部的成员不再受其声明所在命名空间的限制。内联命名空间只在大型版本控制里有用。
有时候不得不多次引用某个定义在许多嵌套命名空间里的实体,使用完整的命名空间会导致代码的冗长。
在头文件中使用匿名空间导致违背 C++ 的唯一定义原则 (One Definition Rule (ODR)).
结论:
根据下文将要提到的策略合理使用命名空间.
遵守 命名空间命名 中的规则。
像之前的几个例子中一样,在命名空间的最后注释出命名空间的名字。
用命名空间把文件包含, gflags 的声明/定义, 以及类的前置声明以外的整个源文件封装起来, 以区别于其它命名空间:
// .h 文件 namespace mynamespace { // 所有声明都置于命名空间中 // 注意不要使用缩进 class MyClass { public: ... void Foo(); }; } // namespace mynamespace// .cc 文件 namespace mynamespace { // 函数定义都置于命名空间中 void MyClass::Foo() { ... } } // namespace mynamespace更复杂的
.cc
文件包含更多, 更复杂的细节, 比如 gflags 或 using 声明。#include "a.h" DEFINE_FLAG(bool, someflag, false, "dummy flag"); namespace a { ...code for a... // 左对齐 } // namespace a不要在命名空间
std
内声明任何东西, 包括标准库的类前置声明. 在std
命名空间声明实体是未定义的行为, 会导致如不可移植. 声明标准库下的实体, 需要包含对应的头文件.不应该使用 using 指示 引入整个命名空间的标识符号。
// 禁止 —— 污染命名空间 using namespace foo;不要在头文件中使用 命名空间别名 除非显式标记内部命名空间使用。因为任何在头文件中引入的命名空间都会成为公开API的一部分。
// 在 .cc 中使用别名缩短常用的命名空间 namespace baz = ::foo::bar::baz;// 在 .h 中使用别名缩短常用的命名空间 namespace librarian { namespace impl { // 仅限内部使用 namespace sidetable = ::pipeline_diagnostics::sidetable; } // namespace impl inline void my_inline_function() { // 限制在一个函数中的命名空间别名 namespace baz = ::foo::bar::baz; ... } } // namespace librarian禁止用内联命名空间
2.2. 匿名命名空间和静态变量
Tip
在 .cc
文件中定义一个不需要被外部引用的变量时,可以将它们放在匿名命名空间或声明为 static
。但是不要在 .h
文件中这么做。
定义:
所有置于匿名命名空间的声明都具有内部链接性,函数和变量可以经由声明为
static
拥有内部链接性,这意味着你在这个文件中声明的这些标识符都不能在另一个文件中被访问。即使两个文件声明了完全一样名字的标识符,它们所指向的实体实际上是完全不同的。
结论:
推荐、鼓励在
.cc
中对于不需要在其他地方引用的标识符使用内部链接性声明,但是不要在.h
中使用。匿名命名空间的声明和具名的格式相同,在最后注释上
namespace
:namespace { ... } // namespace
2.3. 非成员函数、静态成员函数和全局函数
Tip
使用静态成员函数或命名空间内的非成员函数, 尽量不要用裸的全局函数. 将一系列函数直接置于命名空间中,不要用类的静态方法模拟出命名空间的效果,类的静态方法应当和类的实例或静态数据紧密相关.
优点:
某些情况下, 非成员函数和静态成员函数是非常有用的, 将非成员函数放在命名空间内可避免污染全局作用域.
缺点:
将非成员函数和静态成员函数作为新类的成员或许更有意义, 当它们需要访问外部资源或具有重要的依赖关系时更是如此.
结论:
有时, 把函数的定义同类的实例脱钩是有益的, 甚至是必要的. 这样的函数可以被定义成静态成员, 或是非成员函数. 非成员函数不应依赖于外部变量, 应尽量置于某个命名空间内. 相比单纯为了封装若干不共享任何静态数据的静态成员函数而创建类, 不如使用 2.1. 命名空间 。举例而言,对于头文件
myproject/foo_bar.h
, 应当使用namespace myproject { namespace foo_bar { void Function1(); void Function2(); } // namespace foo_bar } // namespace myproject而非
namespace myproject { class FooBar { public: static void Function1(); static void Function2(); }; } // namespace myproject定义在同一编译单元的函数, 被其他编译单元直接调用可能会引入不必要的耦合和链接时依赖; 静态成员函数对此尤其敏感. 可以考虑提取到新类中, 或者将函数置于独立库的命名空间内.
如果你必须定义非成员函数, 又只是在
.cc
文件中使用它, 可使用匿名 2.1. 命名空间 或static
链接关键字 (如static int Foo() {...}
) 限定其作用域.
2.4. 局部变量
Tip
将函数变量尽可能置于最小作用域内, 并在变量声明时进行初始化.
C++ 允许在函数的任何位置声明变量. 我们提倡在尽可能小的作用域中声明变量, 离第一次使用越近越好. 这使得代码浏览者更容易定位变量声明的位置, 了解变量的类型和初始值. 特别是,应使用初始化的方式替代声明再赋值, 比如:
int i; i = f(); // 坏——初始化和声明分离int j = g(); // 好——初始化时声明vector<int> v; v.push_back(1); // 用花括号初始化更好 v.push_back(2);vector<int> v = {1, 2}; // 好——v 一开始就初始化
属于 if
, while
和 for
语句的变量应当在这些语句中正常地声明,这样子这些变量的作用域就被限制在这些语句中了,举例而言:
while (const char* p = strchr(str, '/')) str = p + 1;
Warning
有一个例外, 如果变量是一个对象, 每次进入作用域都要调用其构造函数, 每次退出作用域都要调用其析构函数. 这会导致效率降低.
// 低效的实现 for (int i = 0; i < 1000000; ++i) { Foo f; // 构造函数和析构函数分别调用 1000000 次! f.DoSomething(i); }
在循环作用域外面声明这类变量要高效的多:
Foo f; // 构造函数和析构函数只调用 1 次 for (int i = 0; i < 1000000; ++i) { f.DoSomething(i); }
2.5. 静态和全局变量
Tip
禁止定义静态储存周期非POD变量,禁止使用含有副作用的函数初始化POD全局变量,因为多编译单元中的静态变量执行时的构造和析构顺序是未明确的,这将导致代码的不可移植。
禁止使用类的 静态储存周期 变量:由于构造和析构函数调用顺序的不确定性,它们会导致难以发现的 bug 。不过 constexpr
变量除外,毕竟它们又不涉及动态初始化或析构。
静态生存周期的对象,即包括了全局变量,静态变量,静态类成员变量和函数静态变量,都必须是原生数据类型 (POD : Plain Old Data): 即 int, char 和 float, 以及 POD 类型的指针、数组和结构体。
静态变量的构造函数、析构函数和初始化的顺序在 C++ 中是只有部分明确的,甚至随着构建变化而变化,导致难以发现的 bug. 所以除了禁用类类型的全局变量,我们也不允许用函数返回值来初始化 POD 变量,除非该函数(比如 getenv()
或 getpid()
)不涉及任何全局变量。函数作用域里的静态变量除外,毕竟它的初始化顺序是有明确定义的,而且只会在指令执行到它的声明那里才会发生。
3. 函数
3.1. 参数顺序
总述
函数的参数顺序为: 输入参数在先, 后跟输出参数.
说明
C/C++ 中的函数参数或者是函数的输入, 或者是函数的输出, 或兼而有之. 输入参数通常是值参或 const
引用, 输出参数或输入/输出参数则一般为非 const
指针. 在排列参数顺序时, 将所有的输入参数置于输出参数之前. 特别要注意, 在加入新参数时不要因为它们是新参数就置于参数列表最后, 而是仍然要按照前述的规则, 即将新的输入参数也置于输出参数之前.
这并非一个硬性规定. 输入/输出参数 (通常是类或结构体) 让这个问题变得复杂. 并且, 有时候为了其他函数保持一致, 你可能不得不有所变通.
3.2. 编写简短函数
总述
我们倾向于编写简短, 凝练的函数.
说明
我们承认长函数有时是合理的, 因此并不硬性限制函数的长度. 如果函数超过 40 行, 可以思索一下能不能在不影响程序结构的前提下对其进行分割.
即使一个长函数现在工作的非常好, 一旦有人对其修改, 有可能出现新的问题, 甚至导致难以发现的 bug. 使函数尽量简短, 以便于他人阅读和修改代码.
在处理代码时, 你可能会发现复杂的长函数. 不要害怕修改现有代码: 如果证实这些代码使用 / 调试起来很困难, 或者你只需要使用其中的一小段代码, 考虑将其分割为更加简短并易于管理的若干函数.
3.3. 引用参数
总述
所有按引用传递的参数必须加上 const
.
定义
在 C 语言中, 如果函数需要修改变量的值, 参数必须为指针, 如 int foo(int *pval)
. 在 C++ 中, 函数还可以声明为引用参数: int foo(int &val)
.
优点
定义引用参数可以防止出现 (*pval)++
这样丑陋的代码. 引用参数对于拷贝构造函数这样的应用也是必需的. 同时也更明确地不接受空指针.
缺点
容易引起误解, 因为引用在语法上是值变量却拥有指针的语义.
结论
函数参数列表中, 所有引用参数都必须是 const
:
void Foo(const string &in, string *out);
事实上这在 Google Code 是一个硬性约定: 输入参数是值参或 const
引用, 输出参数为指针. 输入参数可以是 const
指针, 但决不能是非 const
的引用参数, 除非特殊要求, 比如 swap()
.
有时候, 在输入形参中用 const T*
指针比 const T&
更明智. 比如:
- 可能会传递空指针.
- 函数要把指针或对地址的引用赋值给输入形参.
总而言之, 大多时候输入形参往往是 const T&
. 若用 const T*
则说明输入另有处理. 所以若要使用 const T*
, 则应给出相应的理由, 否则会使得读者感到迷惑
3.4. 函数返回类型后置语法
总述
只有在常规写法 (返回类型前置) 不便于书写或不便于阅读时使用返回类型后置语法.
定义
C++ 现在允许两种不同的函数声明方式. 以往的写法是将返回类型置于函数名之前. 例如:
int foo(int x);
C++11 引入了这一新的形式. 现在可以在函数名前使用 auto
关键字, 在参数列表之后后置返回类型. 例如:
auto foo(int x) -> int;
后置返回类型为函数作用域. 对于像 int
这样简单的类型, 两种写法没有区别. 但对于复杂的情况, 例如类域中的类型声明或者以函数参数的形式书写的类型, 写法的不同会造成区别.
4. 命名约定
最重要的一致性规则是命名管理. 命名的风格能让我们在不需要去查找类型声明的条件下快速地了解某个名字代表的含义: 类型, 变量, 函数, 常量, 宏, 等等, 甚至. 我们大脑中的模式匹配引擎非常依赖这些命名规则.
命名规则具有一定随意性, 但相比按个人喜好命名, 一致性更重要, 所以无论你认为它们是否重要, 规则总归是规则.
4.1. 通用命名规则
总述
函数命名, 变量命名, 文件命名要有描述性; 少用缩写.
说明
尽可能使用描述性的命名, 别心疼空间, 毕竟相比之下让代码易于新读者理解更重要. 不要用只有项目开发者能理解的缩写, 也不要通过砍掉几个字母来缩写单词.
int price_count_reader; // 无缩写 int num_errors; // "num" 是一个常见的写法 int num_dns_connections; // 人人都知道 "DNS" 是什么
int n; // 毫无意义. int nerr; // 含糊不清的缩写. int n_comp_conns; // 含糊不清的缩写. int wgc_connections; // 只有贵团队知道是什么意思. int pc_reader; // "pc" 有太多可能的解释了. int cstmr_id; // 删减了若干字母.
注意, 一些特定的广为人知的缩写是允许的, 例如用 i
表示迭代变量和用 T
表示模板参数.
模板参数的命名应当遵循对应的分类: 类型模板参数应当遵循 类型命名 的规则, 而非类型模板应当遵循 变量命名 的规则.
4.2. 文件命名
总述
文件名要全部小写, 可以包含下划线 (_
) 或连字符 (-
), 依照项目的约定. 如果没有约定, 那么 “_
” 更好.
说明
可接受的文件命名示例:
my_useful_class.cc
my-useful-class.cc
myusefulclass.cc
myusefulclass_test.cc
//_unittest
和_regtest
已弃用.
C++ 文件要以 .cc
结尾, 头文件以 .h
结尾. 专门插入文本的文件则以 .inc
结尾, 参见 头文件自足.
不要使用已经存在于 /usr/include
下的文件名 (Yang.Y 注: 即编译器搜索系统头文件的路径), 如 db.h
.
通常应尽量让文件名更加明确. http_server_logs.h
就比 logs.h
要好. 定义类时文件名一般成对出现, 如 foo_bar.h
和 foo_bar.cc
, 对应于类 FooBar
.
内联函数必须放在 .h
文件中. 如果内联函数比较短, 就直接放在 .h
中.
4.3. 类型命名
总述
类型名称的每个单词首字母均大写, 不包含下划线: MyExcitingClass
, MyExcitingEnum
.
说明
所有类型命名 —— 类, 结构体, 类型定义 (typedef
), 枚举, 类型模板参数 —— 均使用相同约定, 即以大写字母开始, 每个单词首字母均大写, 不包含下划线. 例如:
// 类和结构体 class UrlTable { ... class UrlTableTester { ... struct UrlTableProperties { ... // 类型定义 typedef hash_map<UrlTableProperties *, string> PropertiesMap; // using 别名 using PropertiesMap = hash_map<UrlTableProperties *, string>; // 枚举 enum UrlTableErrors { ...
4.4. 变量命名
总述
变量 (包括函数参数) 和数据成员名一律小写, 单词之间用下划线连接. 类的成员变量以下划线结尾, 但结构体的就不用, 如: a_local_variable
, a_struct_data_member
, a_class_data_member_
.
说明
普通变量命名
举例:
string table_name; // 好 - 用下划线. string tablename; // 好 - 全小写. string tableName; // 差 - 混合大小写
类数据成员
不管是静态的还是非静态的, 类数据成员都可以和普通变量一样, 但要接下划线.
class TableInfo { ... private: string table_name_; // 好 - 后加下划线. string tablename_; // 好. static Pool<TableInfo>* pool_; // 好. };
结构体变量
不管是静态的还是非静态的, 结构体数据成员都可以和普通变量一样, 不用像类那样接下划线:
struct UrlTableProperties { string name; int num_entries; static Pool<UrlTableProperties>* pool; };
4.5. 常量命名
总述
声明为 constexpr
或 const
的变量, 或在程序运行期间其值始终保持不变的, 命名时以 “k” 开头, 大小写混合. 例如:
const int kDaysInAWeek = 7;
说明
所有具有静态存储类型的变量 都应当以此方式命名. 对于其他存储类型的变量, 如自动变量等, 这条规则是可选的. 如果不采用这条规则, 就按照一般的变量命名规则.
4.6. 函数命名
总述
常规函数使用大小写混合, 取值和设值函数则要求与变量名匹配: MyExcitingFunction()
, MyExcitingMethod()
, my_exciting_member_variable()
, set_my_exciting_member_variable()
.
说明
一般来说, 函数名的每个单词首字母大写 (即 “驼峰变量名” 或 “帕斯卡变量名”), 没有下划线. 对于首字母缩写的单词, 更倾向于将它们视作一个单词进行首字母大写 (例如, 写作 StartRpc()
而非 StartRPC()
).
AddTableEntry() DeleteUrl() OpenFileOrDie()
(同样的命名规则同时适用于类作用域与命名空间作用域的常量, 因为它们是作为 API 的一部分暴露对外的, 因此应当让它们看起来像是一个函数, 因为在这时, 它们实际上是一个对象而非函数的这一事实对外不过是一个无关紧要的实现细节.)
取值和设值函数的命名与变量一致. 一般来说它们的名称与实际的成员变量对应, 但并不强制要求. 例如 int count()
与 void set_count(int count)
.
4.7. 命名规则的特例
总述
如果你命名的实体与已有 C/C++ 实体相似, 可参考现有命名策略.
bigopen()
: 函数名, 参照 open()
的形式
uint
: typedef
bigpos
: struct
或 class
, 参照 pos
的形式
sparse_hash_map
: STL 型实体; 参照 STL 命名约定
LONGLONG_MAX
: 常量, 如同 INT_MAX
5. 注释
注释虽然写起来很痛苦, 但对保证代码可读性至关重要. 下面的规则描述了如何注释以及在哪儿注释. 当然也要记住: 注释固然很重要, 但最好的代码应当本身就是文档. 有意义的类型名和变量名, 要远胜过要用注释解释的含糊不清的名字.
你写的注释是给代码读者看的, 也就是下一个需要理解你的代码的人. 所以慷慨些吧, 下一个读者可能就是你!
5.1. 注释风格
总述
使用 //
或 /* */
, 统一就好.
说明
//
或 /* */
都可以; 但 //
更 常用. 要在如何注释及注释风格上确保统一.
5.2. 文件注释
总述
在每一个文件开头加入版权公告.
文件注释描述了该文件的内容. 如果一个文件只声明, 或实现, 或测试了一个对象, 并且这个对象已经在它的声明处进行了详细的注释, 那么就没必要再加上文件注释. 除此之外的其他文件都需要文件注释.
说明
法律公告和作者信息
每个文件都应该包含许可证引用. 为项目选择合适的许可证版本.(比如, Apache 2.0, BSD, LGPL, GPL)
如果你对原始作者的文件做了重大修改, 请考虑删除原作者信息.
文件内容
如果一个 .h
文件声明了多个概念, 则文件注释应当对文件的内容做一个大致的说明, 同时说明各概念之间的联系. 一个一到两行的文件注释就足够了, 对于每个概念的详细文档应当放在各个概念中, 而不是文件注释中.
不要在 .h
和 .cc
之间复制注释, 这样的注释偏离了注释的实际意义.
5.3. 类注释
总述
每个类的定义都要附带一份注释, 描述类的功能和用法, 除非它的功能相当明显.
// 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
文件中), 此时, 描述类用法的注释应当和接口定义放在一起, 描述类的操作和实现的注释应当和实现放在一起.
5.4. 函数注释
总述
函数声明处的注释描述函数功能; 定义处的注释描述函数实现.
说明
函数声明
基本上每个函数声明处前都应当加上注释, 描述函数的功能和用途. 只有在函数的功能简单而明显时才能省略这些注释(例如, 简单的取值和设值函数). 注释使用叙述式 (“Opens the file”) 而非指令式 (“Open the file”); 注释只是为了描述函数, 而不是命令函数做什么. 通常, 注释不会描述函数如何工作. 那是函数定义部分的事情.
函数声明处注释的内容:
- 函数的输入输出.
- 对类成员函数而言: 函数调用期间对象是否需要保持引用参数, 是否会释放这些参数.
- 函数是否分配了必须由调用者释放的空间.
- 参数是否可以为空指针.
- 是否存在函数使用上的性能隐患.
- 如果函数是可重入的, 其同步前提是什么?
举例如下:
// 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;
但也要避免罗罗嗦嗦, 或者对显而易见的内容进行说明. 下面的注释就没有必要加上 “否则返回 false”, 因为已经暗含其中了:
// Returns true if the table cannot hold any more entries. bool IsTableFull();
注释函数重载时, 注释的重点应该是函数中被重载的部分, 而不是简单的重复被重载的函数的注释. 多数情况下, 函数重载不需要额外的文档, 因此也没有必要加上注释.
注释构造/析构函数时, 切记读代码的人知道构造/析构函数的功能, 所以 “销毁这一对象” 这样的注释是没有意义的. 你应当注明的是注明构造函数对参数做了什么 (例如, 是否取得指针所有权) 以及析构函数清理了什么. 如果都是些无关紧要的内容, 直接省掉注释. 析构函数前没有注释是很正常的.
函数定义
如果函数的实现过程中用到了很巧妙的方式, 那么在函数定义处应当加上解释性的注释. 例如, 你所使用的编程技巧, 实现的大致步骤, 或解释如此实现的理由. 举个例子, 你可以说明为什么函数的前半部分要加锁而后半部分不需要.
不要 从 .h
文件或其他地方的函数声明处直接复制注释. 简要重述函数功能是可以的, 但注释重点要放在如何实现上.
5.5. 变量注释
总述
通常变量名本身足以很好说明变量用途. 某些情况下, 也需要额外的注释说明.
说明
类数据成员
每个类数据成员 (也叫实例变量或成员变量) 都应该用注释说明用途. 如果有非变量的参数(例如特殊值, 数据成员之间的关系, 生命周期等)不能够用类型与变量名明确表达, 则应当加上注释. 然而, 如果变量类型与变量名已经足以描述一个变量, 那么就不再需要加上注释.
特别地, 如果变量可以接受 NULL
或 -1
等警戒值, 须加以说明. 比如:
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_;
全局变量
和数据成员一样, 所有全局变量也要注释说明含义及用途, 以及作为全局变量的原因. 比如:
// The total number of tests cases that we run through in this regression test. const int kNumTestCases = 6;
5.6. 实现注释
总述
对于代码中巧妙的, 晦涩的, 有趣的, 重要的地方加以注释.
说明
代码前注释
巧妙或复杂的代码段前要加注释. 比如:
// 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; }
行注释
比较隐晦的地方要在行尾加入注释. 在行尾空两格进行注释. 比如:
// 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.
注意, 这里用了两段注释分别描述这段代码的作用, 和提示函数返回时错误已经被记入日志.
如果你需要连续进行多行注释, 可以使之对齐获得更好的可读性:
DoSomething(); // Comment here so the comments line up. DoSomethingElseThatIsLonger(); // Two spaces between the code and the comment. { // One space before comment when opening a new scope is allowed, // thus the comment lines up with the following comments and code. DoSomethingElse(); // Two spaces before line comments normally. } std::vector<string> list{ // Comments in braced lists describe the next element... "First item", // .. and should be aligned appropriately. "Second item"}; DoSomething(); /* For trailing block comments, one space is fine. */
函数参数注释
如果函数参数的意义不明显, 考虑用下面的方式进行弥补:
- 如果参数是一个字面常量, 并且这一常量在多处函数调用中被使用, 用以推断它们一致, 你应当用一个常量名让这一约定变得更明显, 并且保证这一约定不会被打破.
- 考虑更改函数的签名, 让某个
bool
类型的参数变为enum
类型, 这样可以让这个参数的值表达其意义. - 如果某个函数有多个配置选项, 你可以考虑定义一个类或结构体以保存所有的选项, 并传入类或结构体的实例. 这样的方法有许多优点, 例如这样的选项可以在调用处用变量名引用, 这样就能清晰地表明其意义. 同时也减少了函数参数的数量, 使得函数调用更易读也易写. 除此之外, 以这样的方式, 如果你使用其他的选项, 就无需对调用点进行更改.
- 用具名变量代替大段而复杂的嵌套表达式.
- 万不得已时, 才考虑在调用点用注释阐明参数的意义.
比如下面的示例的对比:
// What are these arguments? const DecimalNumber product = CalculateProduct(values, 7, false, nullptr);
和
ProductOptions options; options.set_precision_decimals(7); options.set_use_cache(ProductOptions::kDontUseCache); const DecimalNumber product = CalculateProduct(values, options, /*completion_callback=*/nullptr);
哪个更清晰一目了然.
不允许的行为
不要描述显而易见的现象, 永远不要 用自然语言翻译代码作为注释, 除非即使对深入理解 C++ 的读者来说代码的行为都是不明显的. 要假设读代码的人 C++ 水平比你高, 即便他/她可能不知道你的用意:
你所提供的注释应当解释代码 为什么 要这么做和代码的目的, 或者最好是让代码自文档化.
比较这样的注释:
// Find the element in the vector. <-- 差: 这太明显了! auto iter = std::find(v.begin(), v.end(), element); if (iter != v.end()) { Process(element); }
和这样的注释:
// Process "element" unless it was already processed. auto iter = std::find(v.begin(), v.end(), element); if (iter != v.end()) { Process(element); }
自文档化的代码根本就不需要注释. 上面例子中的注释对下面的代码来说就是毫无必要的:
if (!IsAlreadyProcessed(element)) { Process(element); }
5.7. 标点, 拼写和语法
总述
注意标点, 拼写和语法; 写的好的注释比差的要易读的多.
说明
注释的通常写法是包含正确大小写和结尾句号的完整叙述性语句. 大多数情况下, 完整的句子比句子片段可读性更高. 短一点的注释, 比如代码行尾注释, 可以随意点, 但依然要注意风格的一致性.
虽然被别人指出该用分号时却用了逗号多少有些尴尬, 但清晰易读的代码还是很重要的. 正确的标点, 拼写和语法对此会有很大帮助.
5.8. TODO 注释
总述
对那些临时的, 短期的解决方案, 或已经够好但仍不完美的代码使用 TODO
注释.
TODO
注释要使用全大写的字符串 TODO
, 在随后的圆括号里写上你的名字, 邮件地址, bug ID, 或其它身份标识和与这一 TODO
相关的 issue. 主要目的是让添加注释的人 (也是可以请求提供更多细节的人) 可根据规范的 TODO
格式进行查找. 添加 TODO
注释并不意味着你要自己来修正, 因此当你加上带有姓名的 TODO
时, 一般都是写上自己的名字.
// 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
如果加 TODO
是为了在 “将来某一天做某事”, 可以附上一个非常明确的时间 “Fix by November 2005”), 或者一个明确的事项 (“Remove this code when all clients can handle XML responses.”).
5.9. 弃用注释
总述
通过弃用注释(DEPRECATED
comments)以标记某接口点已弃用.
您可以写上包含全大写的 DEPRECATED
的注释, 以标记某接口为弃用状态. 注释可以放在接口声明前, 或者同一行.
在 DEPRECATED
一词后, 在括号中留下您的名字, 邮箱地址以及其他身份标识.
弃用注释应当包涵简短而清晰的指引, 以帮助其他人修复其调用点. 在 C++ 中, 你可以将一个弃用函数改造成一个内联函数, 这一函数将调用新的接口.
仅仅标记接口为 DEPRECATED
并不会让大家不约而同地弃用, 您还得亲自主动修正调用点(callsites), 或是找个帮手.
修正好的代码应该不会再涉及弃用接口点了, 着实改用新接口点. 如果您不知从何下手, 可以找标记弃用注释的当事人一起商量.
6. 格式
每个人都可能有自己的代码风格和格式, 但如果一个项目中的所有人都遵循同一风格的话, 这个项目就能更顺利地进行. 每个人未必能同意下述的每一处格式规则, 而且其中的不少规则需要一定时间的适应, 但整个项目服从统一的编程风格是很重要的, 只有这样才能让所有人轻松地阅读和理解代码.
为了帮助你正确的格式化代码, 我们写了一个 emacs 配置文件.
6.1. 行长度
总述
每一行代码字符数不超过 80.
我们也认识到这条规则是有争议的, 但很多已有代码都遵照这一规则, 因此我们感觉一致性更重要.
优点
提倡该原则的人认为强迫他们调整编辑器窗口大小是很野蛮的行为. 很多人同时并排开几个代码窗口, 根本没有多余的空间拉伸窗口. 大家都把窗口最大尺寸加以限定, 并且 80 列宽是传统标准. 那么为什么要改变呢?
缺点
反对该原则的人则认为更宽的代码行更易阅读. 80 列的限制是上个世纪 60 年代的大型机的古板缺陷; 现代设备具有更宽的显示屏, 可以很轻松地显示更多代码.
结论
80 个字符是最大值.
如果无法在不伤害易读性的条件下进行断行, 那么注释行可以超过 80 个字符, 这样可以方便复制粘贴. 例如, 带有命令示例或 URL 的行可以超过 80 个字符.
包含长路径的 #include
语句可以超出80列.
头文件保护 可以无视该原则.
6.2. 非 ASCII 字符
总述
尽量不使用非 ASCII 字符, 使用时必须使用 UTF-8 编码.
说明
即使是英文, 也不应将用户界面的文本硬编码到源代码中, 因此非 ASCII 字符应当很少被用到. 特殊情况下可以适当包含此类字符. 例如, 代码分析外部数据文件时, 可以适当硬编码数据文件中作为分隔符的非 ASCII 字符串; 更常见的是 (不需要本地化的) 单元测试代码可能包含非 ASCII 字符串. 此类情况下, 应使用 UTF-8 编码, 因为很多工具都可以理解和处理 UTF-8 编码.
十六进制编码也可以, 能增强可读性的情况下尤其鼓励 —— 比如 "\xEF\xBB\xBF"
, 或者更简洁地写作 u8"\uFEFF"
, 在 Unicode 中是 零宽度 无间断 的间隔符号, 如果不用十六进制直接放在 UTF-8 格式的源文件中, 是看不到的.
(Yang.Y 注: "\xEF\xBB\xBF"
通常用作 UTF-8 with BOM 编码标记)
使用 u8
前缀把带 uXXXX
转义序列的字符串字面值编码成 UTF-8. 不要用在本身就带 UTF-8 字符的字符串字面值上, 因为如果编译器不把源代码识别成 UTF-8, 输出就会出错.
别用 C++11 的 char16_t
和 char32_t
, 它们和 UTF-8 文本没有关系, wchar_t
同理, 除非你写的代码要调用 Windows API, 后者广泛使用了 wchar_t
.
6.3. 空格还是制表位
总述
只使用空格, 每次缩进 2 个空格.
说明
我们使用空格缩进. 不要在代码中使用制表符. 你应该设置编辑器将制表符转为空格.
6.4. 函数声明与定义
总述
返回类型和函数名在同一行, 参数也尽量放在同一行, 如果放不下就对形参分行, 分行方式与 函数调用 一致.
说明
函数看上去像这样:
ReturnType ClassName::FunctionName(Type par_name1, Type par_name2) { DoSomething(); ... }
如果同一行文本太多, 放不下所有参数:
ReturnType ClassName::ReallyLongFunctionName(Type par_name1, Type par_name2, Type par_name3) { DoSomething(); ... }
甚至连第一个参数都放不下:
ReturnType LongClassName::ReallyReallyReallyLongFunctionName( Type par_name1, // 4 space indent Type par_name2, Type par_name3) { DoSomething(); // 2 space indent ... }
注意以下几点:
- 使用好的参数名.
- 只有在参数未被使用或者其用途非常明显时, 才能省略参数名.
- 如果返回类型和函数名在一行放不下, 分行.
- 如果返回类型与函数声明或定义分行了, 不要缩进.
- 左圆括号总是和函数名在同一行.
- 函数名和左圆括号间永远没有空格.
- 圆括号与参数间没有空格.
- 左大括号总在最后一个参数同一行的末尾处, 不另起新行.
- 右大括号总是单独位于函数最后一行, 或者与左大括号同一行.
- 右圆括号和左大括号间总是有一个空格.
- 所有形参应尽可能对齐.
- 缺省缩进为 2 个空格.
- 换行后的参数保持 4 个空格的缩进.
未被使用的参数, 或者根据上下文很容易看出其用途的参数, 可以省略参数名:
class Foo { public: Foo(Foo&&); Foo(const Foo&); Foo& operator=(Foo&&); Foo& operator=(const Foo&); };
未被使用的参数如果其用途不明显的话, 在函数定义处将参数名注释起来:
class Shape { public: virtual void Rotate(double radians) = 0; }; class Circle : public Shape { public: void Rotate(double radians) override; }; void Circle::Rotate(double /*radians*/) {}
// 差 - 如果将来有人要实现, 很难猜出变量的作用. void Circle::Rotate(double) {}
属性, 和展开为属性的宏, 写在函数声明或定义的最前面, 即返回类型之前:
MUST_USE_RESULT bool IsOK();
6.5. Lambda 表达式
总述
Lambda 表达式对形参和函数体的格式化和其他函数一致; 捕获列表同理, 表项用逗号隔开.
说明
若用引用捕获, 在变量名和 &
之间不留空格.
int x = 0; auto add_to_x = [&x](int n) { x += n; };
短 lambda 就写得和内联函数一样.
std::set<int> blacklist = {7, 8, 9}; std::vector<int> digits = {3, 9, 1, 8, 4, 7, 1}; digits.erase(std::remove_if(digits.begin(), digits.end(), [&blacklist](int i) { return blacklist.find(i) != blacklist.end(); }), digits.end());
6.6. 函数调用
总述
要么一行写完函数调用, 要么在圆括号里对参数分行, 要么参数另起一行且缩进四格. 如果没有其它顾虑的话, 尽可能精简行数, 比如把多个参数适当地放在同一行里.
说明
函数调用遵循如下形式:
bool retval = DoSomething(argument1, argument2, argument3);
如果同一行放不下, 可断为多行, 后面每一行都和第一个实参对齐, 左圆括号后和右圆括号前不要留空格:
bool retval = DoSomething(averyveryveryverylongargument1, argument2, argument3);
参数也可以放在次行, 缩进四格:
if (...) { ... ... if (...) { DoSomething( argument1, argument2, // 4 空格缩进 argument3, argument4); }
把多个参数放在同一行以减少函数调用所需的行数, 除非影响到可读性. 有人认为把每个参数都独立成行, 不仅更好读, 而且方便编辑参数. 不过, 比起所谓的参数编辑, 我们更看重可读性, 且后者比较好办:
如果一些参数本身就是略复杂的表达式, 且降低了可读性, 那么可以直接创建临时变量描述该表达式, 并传递给函数:
int my_heuristic = scores[x] * y + bases[x]; bool retval = DoSomething(my_heuristic, x, y, z);
或者放着不管, 补充上注释:
bool retval = DoSomething(scores[x] * y + bases[x], // Score heuristic. x, y, z);
如果某参数独立成行, 对可读性更有帮助的话, 那也可以如此做. 参数的格式处理应当以可读性而非其他作为最重要的原则.
此外, 如果一系列参数本身就有一定的结构, 可以酌情地按其结构来决定参数格式:
// 通过 3x3 矩阵转换 widget. my_widget.Transform(x1, x2, x3, y1, y2, y3, z1, z2, z3);
6.7. 列表初始化格式
总述
您平时怎么格式化函数调用, 就怎么格式化 列表初始化.
说明
如果列表初始化伴随着名字, 比如类型或变量名, 格式化时将将名字视作函数调用名, {} 视作函数调用的括号. 如果没有名字, 就视作名字长度为零.
// 一行列表初始化示范. return {foo, bar}; functioncall({foo, bar}); pair<int, int> p{foo, bar}; // 当不得不断行时. SomeFunction( {"assume a zero-length name before {"}, // 假设在 { 前有长度为零的名字. some_other_function_parameter); SomeType variable{ some, other, values, {"assume a zero-length name before {"}, // 假设在 { 前有长度为零的名字. SomeOtherType{ "Very long string requiring the surrounding breaks.", // 非常长的字符串, 前后都需要断行. some, other values}, SomeOtherType{"Slightly shorter string", // 稍短的字符串. some, other, values}}; SomeType variable{ "This is too long to fit all in one line"}; // 字符串过长, 因此无法放在同一行. MyType m = { // 注意了, 您可以在 { 前断行. superlongvariablename1, superlongvariablename2, {short, interior, list}, {interiorwrappinglist, interiorwrappinglist2}};
6.8. 条件语句
总述
倾向于不在圆括号内使用空格. 关键字 if
和 else
另起一行.
说明
对基本条件语句有两种可以接受的格式. 一种在圆括号和条件之间有空格, 另一种没有.
最常见的是没有空格的格式. 哪一种都可以, 最重要的是 保持一致. 如果你是在修改一个文件, 参考当前已有格式. 如果是写新的代码, 参考目录下或项目中其它文件. 还在犹豫的话, 就不要加空格了.
if (condition) { // 圆括号里没有空格. ... // 2 空格缩进. } else if (...) { // else 与 if 的右括号同一行. ... } else { ... }
如果你更喜欢在圆括号内部加空格:
if ( condition ) { // 圆括号与空格紧邻 - 不常见 ... // 2 空格缩进. } else { // else 与 if 的右括号同一行. ... }
注意所有情况下 if
和左圆括号间都有个空格. 右圆括号和左大括号之间也要有个空格:
if(condition) // 差 - IF 后面没空格. if (condition){ // 差 - { 前面没空格. if(condition){ // 变本加厉地差.
if (condition) { // 好 - IF 和 { 都与空格紧邻.
如果能增强可读性, 简短的条件语句允许写在同一行. 只有当语句简单并且没有使用 else
子句时使用:
if (x == kFoo) return new Foo(); if (x == kBar) return new Bar();
如果语句有 else
分支则不允许:
// 不允许 - 当有 ELSE 分支时 IF 块却写在同一行 if (x) DoThis(); else DoThat();
通常, 单行语句不需要使用大括号, 如果你喜欢用也没问题; 复杂的条件或循环语句用大括号可读性会更好. 也有一些项目要求 if
必须总是使用大括号:
if (condition) DoSomething(); // 2 空格缩进. if (condition) { DoSomething(); // 2 空格缩进. }
但如果语句中某个 if-else
分支使用了大括号的话, 其它分支也必须使用:
// 不可以这样子 - IF 有大括号 ELSE 却没有. if (condition) { foo; } else bar; // 不可以这样子 - ELSE 有大括号 IF 却没有. if (condition) foo; else { bar; }
// 只要其中一个分支用了大括号, 两个分支都要用上大括号. if (condition) { foo; } else { bar; }
6.9. 循环和开关选择语句
总述
switch
语句可以使用大括号分段, 以表明 cases 之间不是连在一起的. 在单语句循环里, 括号可用可不用. 空循环体应使用 {}
或 continue
.
说明
switch
语句中的 case
块可以使用大括号也可以不用, 取决于你的个人喜好. 如果用的话, 要按照下文所述的方法.
如果有不满足 case
条件的枚举值, switch
应该总是包含一个 default
匹配 (如果有输入值没有 case 去处理, 编译器将给出 warning). 如果 default
应该永远执行不到, 简单的加条 assert
:
switch (var) { case 0: { // 2 空格缩进 ... // 4 空格缩进 break; } case 1: { ... break; } default: { assert(false); } }
在单语句循环里, 括号可用可不用:
for (int i = 0; i < kSomeNumber; ++i) printf("I love you\n"); for (int i = 0; i < kSomeNumber; ++i) { printf("I take it back\n"); }
空循环体应使用 {}
或 continue
, 而不是一个简单的分号.
while (condition) { // 反复循环直到条件失效. } for (int i = 0; i < kSomeNumber; ++i) {} // 可 - 空循环体. while (condition) continue; // 可 - contunue 表明没有逻辑.
while (condition); // 差 - 看起来仅仅只是 while/loop 的部分之一.
6.10. 指针和引用表达式
总述
句点或箭头前后不要有空格. 指针/地址操作符 (*, &
) 之后不能有空格.
说明
下面是指针和引用表达式的正确使用范例:
x = *p; p = &x; x = r.y; x = r->y;
注意:
- 在访问成员时, 句点或箭头前后没有空格.
- 指针操作符
*
或&
后没有空格.
在声明指针变量或参数时, 星号与类型或变量名紧挨都可以:
// 好, 空格前置. char *c; const string &str; // 好, 空格后置. char* c; const string& str;
int x, *y; // 不允许 - 在多重声明中不能使用 & 或 * char * c; // 差 - * 两边都有空格 const string & str; // 差 - & 两边都有空格.
在单个文件内要保持风格一致, 所以, 如果是修改现有文件, 要遵照该文件的风格.
6.11. 布尔表达式
总述
如果一个布尔表达式超过 标准行宽, 断行方式要统一一下.
说明
下例中, 逻辑与 (&&
) 操作符总位于行尾:
if (this_one_thing > this_other_thing && a_third_thing == a_fourth_thing && yet_another && last_one) { ... }
注意, 上例的逻辑与 (&&
) 操作符均位于行尾. 这个格式在 Google 里很常见, 虽然把所有操作符放在开头也可以. 可以考虑额外插入圆括号, 合理使用的话对增强可读性是很有帮助的. 此外, 直接用符号形式的操作符, 比如 &&
和 ~
, 不要用词语形式的 and
和 compl
.
6.12. 函数返回值
总述
不要在 return
表达式里加上非必须的圆括号.
说明
只有在写 x = expr
要加上括号的时候才在 return expr;
里使用括号.
return result; // 返回值很简单, 没有圆括号. // 可以用圆括号把复杂表达式圈起来, 改善可读性. return (some_long_condition && another_condition);
return (value); // 毕竟您从来不会写 var = (value); return(result); // return 可不是函数!
6.13. 变量及数组初始化
总述
用 =
, ()
和 {}
均可.
说明
您可以用 =
, ()
和 {}
, 以下的例子都是正确的:
int x = 3; int x(3); int x{3}; string name("Some Name"); string name = "Some Name"; string name{"Some Name"};
请务必小心列表初始化 {...}
用 std::initializer_list
构造函数初始化出的类型. 非空列表初始化就会优先调用 std::initializer_list
, 不过空列表初始化除外, 后者原则上会调用默认构造函数. 为了强制禁用 std::initializer_list
构造函数, 请改用括号.
vector<int> v(100, 1); // 内容为 100 个 1 的向量. vector<int> v{100, 1}; // 内容为 100 和 1 的向量.
此外, 列表初始化不允许整型类型的四舍五入, 这可以用来避免一些类型上的编程失误.
int pi(3.14); // 好 - pi == 3. int pi{3.14}; // 编译错误: 缩窄转换.
6.14. 预处理指令
总述
预处理指令不要缩进, 从行首开始.
说明
即使预处理指令位于缩进代码块中, 指令也应从行首开始.
// 好 - 指令从行首开始 if (lopsided_score) { #if DISASTER_PENDING // 正确 - 从行首开始 DropEverything(); # if NOTIFY // 非必要 - # 后跟空格 NotifyClient(); # endif #endif BackToNormal(); }
// 差 - 指令缩进 if (lopsided_score) { #if DISASTER_PENDING // 差 - "#if" 应该放在行开头 DropEverything(); #endif // 差 - "#endif" 不要缩进 BackToNormal(); }
6.15. 类格式
总述
访问控制块的声明依次序是 public:
, protected:
, private:
, 每个都缩进 1 个空格.
说明
类声明 (下面的代码中缺少注释, 参考 类注释) 的基本格式如下:
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_; };
注意事项:
- 所有基类名应在 80 列限制下尽量与子类名放在同一行.
- 关键词
public:
,protected:
,private:
要缩进 1 个空格. - 除第一个关键词 (一般是
public
) 外, 其他关键词前要空一行. 如果类比较小的话也可以不空. - 这些关键词后不要保留空行.
public
放在最前面, 然后是protected
, 最后是private
.- 关于声明顺序的规则请参考 声明顺序 一节.
6.16. 构造函数初始值列表
总述
构造函数初始化列表放在同一行或按四格缩进并排多行.
说明
下面两种初始值列表方式都可以接受:
// 如果所有变量能放在同一行: MyClass::MyClass(int var) : some_var_(var) { DoSomething(); } // 如果不能放在同一行, // 必须置于冒号后, 并缩进 4 个空格 MyClass::MyClass(int var) : some_var_(var), some_other_var_(var + 1) { DoSomething(); } // 如果初始化列表需要置于多行, 将每一个成员放在单独的一行 // 并逐行对齐 MyClass::MyClass(int var) : some_var_(var), // 4 space indent some_other_var_(var + 1) { // lined up DoSomething(); } // 右大括号 } 可以和左大括号 { 放在同一行 // 如果这样做合适的话 MyClass::MyClass(int var) : some_var_(var) {}
6.17. 命名空间格式化
总述
命名空间内容不缩进.
说明
命名空间 不要增加额外的缩进层次, 例如:
namespace { void foo() { // 正确. 命名空间内没有额外的缩进. ... } } // namespace
不要在命名空间内缩进:
namespace { // 错, 缩进多余了. void foo() { ... } } // namespace
声明嵌套命名空间时, 每个命名空间都独立成行.
namespace foo { namespace bar {
6.19. 水平留白
水平留白的使用根据在代码中的位置决定. 永远不要在行尾添加没意义的留白.
通用
void f(bool b) { // 左大括号前总是有空格. ... int i = 0; // 分号前不加空格. // 列表初始化中大括号内的空格是可选的. // 如果加了空格, 那么两边都要加上. int x[] = { 0 }; int x[] = {0}; // 继承与初始化列表中的冒号前后恒有空格. class Foo : public Bar { public: // 对于单行函数的实现, 在大括号内加上空格 // 然后是函数实现 Foo(int b) : Bar(), baz_(b) {} // 大括号里面是空的话, 不加空格. void Reset() { baz_ = 0; } // 用空格把大括号与实现分开. ...
添加冗余的留白会给其他人编辑时造成额外负担. 因此, 行尾不要留空格. 如果确定一行代码已经修改完毕, 将多余的空格去掉; 或者在专门清理空格时去掉(尤其是在没有其他人在处理这件事的时候). (Yang.Y 注: 现在大部分代码编辑器稍加设置后, 都支持自动删除行首/行尾空格, 如果不支持, 考虑换一款编辑器或 IDE)
循环和条件语句
if (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; // 如果冒号有代码, 加个空格.
操作符
// 赋值运算符前后总是有空格. x = 0; // 其它二元操作符也前后恒有空格, 不过对于表达式的子式可以不加空格. // 圆括号内部没有紧邻空格. v = w * x + y / z; v = w*x + y/z; v = w * (x + z); // 在参数和一元操作符之间不加空格. x = -5; ++x; if (x && !y) ...
模板和转换
// 尖括号(< and >) 不与空格紧邻, < 前没有空格, > 和 ( 之间也没有. vector<string> x; y = static_cast<char*>(x); // 在类型与指针操作符之间留空格也可以, 但要保持一致. vector<char *> x;
6.19. 垂直留白
总述
垂直留白越少越好.
说明
这不仅仅是规则而是原则问题了: 不在万不得已, 不要使用空行. 尤其是: 两个函数定义之间的空行不要超过 2 行, 函数体首尾不要留空行, 函数体中也不要随意添加空行.
基本原则是: 同一屏可以显示的代码越多, 越容易理解程序的控制流. 当然, 过于密集的代码块和过于疏松的代码块同样难看, 这取决于你的判断. 但通常是垂直留白越少越好.
下面的规则可以让加入的空行更有效:
- 函数体内开头或结尾的空行可读性微乎其微.
- 在多重 if-else 块里加空行或许有点可读性.