Google开源项目风格指南
v3.133
原作: Benjy Weinberger, Craig Silverstein, Gergory Eitzmann, Mark Mentovai, Tashana Landray
翻译: YuleFox, brantyoung
修改: YCR
0 扉页
0.1 译者前言
Google经常发布一些开源项目, 因此发布这份编程风格, 使所有提交代码的人能获知Google的编程风格;
规则的作用是避免混乱, 但规则本身要权威, 有说服力, 并且是理性的, 大部分编程规范缺乏严谨, 或阐述过于简单, 带有一定武断性;
Google保持其一贯严谨, 指南的价值不仅仅局限于它罗列出的规范, 更具参考意义的是它为了列出规范巍峨做的谨慎权衡过程;
指南不仅列出要怎么做, 并且告诉你为什么要这么做, 哪些情况下可以不这么做, 以及如何权衡利弊, 其他团队未必要完全遵照指南, 这份指南是Google根据自身实际情况打造, 适用于其主导的开源项目, 其他团队可以参考该指南, 获取灵感, 建立适合自身情况的规范;
Artistic License/GPL 开源许可;
0.2 背景
C++是Google大部分开源项目的主要编程语言, C++有很多强大特性, 但这种强大不可避免的导致它走向复杂, 使代码更容易产生bug, 难以阅读和维护;
本指南的目的是通过详细阐述C++注意事项来驾驭其复杂性, 这些规则在保证代码易于管理的同时, 高效使用C++语言特性;
风格, 也可称为可读性, 即指导C++编程的约定; 使用术语"风格"有些用词不当, 因为这些习惯远不止源代码文件格式化这么简单;
使代码易于管理的方法之一是加强代码一致性(consistency), 让程序员可以快速读懂代码这点很重要; 保持统一编程风格并遵守约定意味着可以很容易根据"模式匹配"规则来推断各种标识符的含义; 创建通用, 必需的习惯用语和模式(pattern-matching)可以使代码更容易理解, 在一些情况下可能有充分的理由改变某些编程风格, 但还是应该遵循一致性原则, 尽量不这么做;
本指南的另一个观点是C++特性的臃肿; C++是包含大量高级特性的庞大语言; 某些情况下, 会限制甚至禁用某些特性; 这么做是为了保持代码清爽, 避免这些特性可能导致的各种问题; 指南针列举了这些特性, 并解释为什么这些特性被限制使用;
Google主导的开源项目均符合本指南的规定;
1 头文件 Header Files
通常每个 .cc[.cpp]文件都有一个对应的 .h; 也有一些常见例外, 如单元测试(unit test)代码和只包含 main()函数的 .cc文件;
正确使用头文件可令代码在可读性, 文件大小和性能上大为改观;
下面的规则引导你规避使用头文件时的各种陷阱;
[Add]
独立自足头文件 Self-contained Headers
头文件应该是独立自足的, 以 .h结尾; 那些是为了内容包含, 不是header(头文件)的, 应该以 inc 结尾; 不允许单独的 -inc.h头文件;
所有头文件都应该是独立的; 换句话说, 用户和重构根据不应该在包含头文件之后还需要特殊的附加; 特别地, 一个头文件应该有 header guards, 应该包含所有它需要的其它header, 而且不需要一些特定符号的定义;
有极少数的情况下一个文件不是独立自足的, 但是作为一个特定情况下放在代码中被当作文本包含进去; Eample: 文件需要被包含多次, 或者对于特定平台的扩展, 它实际上是其他头文件的一部分; 这样的文件应该使用 .inc后缀;
如果一个template或inline函数被声明在 .h文件中, 要在相同的文件中定义它; 这些结构的定义必须被包含到每个使用它们的 .cc文件中, 否则程序在某些构建配置下会链接失败; 不要把这些定义移动到单独的 -inc.h文件中;
作为例外, 如果一个函数模板为所有相关的模板参数集合做显式的实例化, 或者作为一个class的私有成员, 只可以在实例化那个模板的 .cc文件中定义;
<<<
1.1 #define保护 The #define Guard
Tip 所有头文件都应该使用 #define防止头文件被多重包含; 命名格式: <PROJECT>_<PATH>_<FILE>_H_
[大多数IDE会自动生成头文件#define, Windows下的VS可能为 #program once, 但不能跨平台使用]
为保证惟一性, 文件的命名应该依据所在项目源代码树的全路径; 例如, 项目foo中的头文件 foo/src/bar/baz.h可按如下方式保护:
1
2
3
4
|
#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_
…
#endif // FOO_BAR_BAZ_H_
|
[Add]
前置声明 Forward Declarations
为了防止不必要的#include, 可以对一般的类进行前置声明;
定义: 前置声明是对一个没有定义的class, function, 或template进行的一个声明; 对于客户端代码中使用的, #include 行常常可以被前置声明来替换;
优点:
- 没必要让 #include文件来强制让编译打开更多的文件以及进行更多的输入;
- 如果头文件改变了, 你的代码也会需要随之重新编译;
缺点:
- 在一些特性像 template, typedef, default parameter和using declare的时候, 很难确定前置声明的正确形式;
- 对一段代码来讲很难确定是需要前置声明还是完全地#include, 特别是对于隐式转换操作符存在的时候; 极端情况下, 将一个#include替换为前置声明会无声地改变代码的含义;
- 函数和模板的前置声明可以防止头文件所有者的API的改变所产生的不兼容; example: 扩展一个参数类型, 或者添加一个有默认值的模板参数;
- 在namespace std::中添加前置声明符号, 通常会导致未定义的错误;
- 组织代码来实现前置声明(e.g. 使用指针成员代替对象成员)可能会让代码更慢更复杂;
- 前置声明对效率有益处的情况没有被证实过;
决定:
- 如果用到了定义在一个头文件中的函数, 总是要 #include那个头文件;
- 如果使用了类模板, 建议 #include它的头文件;
- 使用一般的class时, 依赖一个前置声明没有问题, 但注意一个前置声明可能不够或者不正确; 不确定的时候, 就#include相应的那个头文件;
- 不要仅仅为了防止一个#include就把数据成员替换成指针;
参阅 Names and Order of Includes 了解何时#include一个头文件的规则;
<<<
[Remove]
1.2 头文件依赖
Tip 能用前置声明的地方尽量不使用 #include
当一个头文件被包含的地方也引入和新的依赖, 一旦该头文件被修改, 代码就会被重新编译; 如果这个头文件又包含了其他头文件, 这些头文件的任何改变都将导致所有包含了该头文件的代码被重新编译; 因此, 倾向于减少包含头文件, 尤其是在头文件中包含头文件;
使用前置声明可以显著减少需要包含的头文件数量, 举例: 如果头文件中用到类 File, 但不需要访问 File类的声明, 头文件中只需前置声明 class File; 而无需 #include "file/base/file.h"
不允许访问类的定义的前提下, 我们在一个头文件中能对类Foo做哪些操作?
- 可以将数据成员类型声明为 Foo* 或 Foo&;
- 可以将函数参数/返回值的类型声明为 Foo(但不能定义实现); [因为返回值类型不属于函数签名, 编译器不需要返回值类型具体定义?]
- 可以将静态数据成员的类型声明为 Foo, 因为静态数据成员的定义在类定义之外;
反之, 如果类是 Foo的子类; 或者含有类型为 Foo的非静态数据成员, 则必须包含 Foo所在的头文件;
有时, 使用指针成员(如果是 scoped_ptr更好 [各种smart_ptr])替代对象成员的确是明智之选; 然而这会降低代码可读性及执行效率[堆 vs 栈? 堆需要new/malloc, 访问数据需要通过指针找寻地址内容]; 因此如果仅仅为了少包含头文件, 还是不要这么做的好;
[根据聚合has-a, 组合contains-a, 选择合适的方案]
当然, .cc文件无论如何都需要所使用类的定义部分, 自然就会包含若干头文件;
[还可以利用预编译头文件 precompile_header, 既可以加快编译速度, 也可以帮助集中管理头文件; pch, stdafx(windows)]
<<<
1.3 内联函数 Inline Functions
Tip 只有当函数只有10行甚至更少时才将其定义为内联函数;
定义: 当函数被声明为内联函数之后, 编译器会将其内联展开, 而不是按通常的函数调用机制进行调用;
优点: 当函数体比较小的时候, 内联该函数可以令目标代码更加高效, 对于存取函数accessors/mutators以及其他函数体比较短, 性能关键的函数, 鼓励使用内联;
缺点: 滥用内联将导致程序变慢, 内联可能使目标代码量或增或减, 取决于内联函数的大小; 内联非常短小的存取函数通常会减少代码大小, 但内联一个相当大的函数将戏剧性的增大代码大小, 现代处理器由于更好的利用了指令缓存 [http://en.wikipedia.org/wiki/CPU_cache], 小巧的代码往往执行更快;
[类体中的函数会自动内联不需要inline关键字, 没有检测到瓶颈, inline不是强制符号, 现代编译器可以拒绝过大函数的内联, 一旦内联失败, 有可能会产生更大的目标代码]
结论:
一个较为合理的经验准则是, 不要内联超过10行的函数; 谨慎对待析构函数, 析构函数往往比其表面看起来要更长; 因为有隐含的成员和基类析构函数被调用!
另一个实用的经验准则: 内联那些包含循环或 switch语句的函数常常是得不偿失(除非在大多数情况下, 这些循环或 switch语句从不被执行);
有些函数即使声明为内联的也不一定会被编译器内联, 这点很重要; 比如虚函数和递归函数就不会被正常内联; 通常, 递归函数不应该声明为内联函数; (译注: 递归调用堆栈的展开并不像循环那么简单, 比如递归层数在编译时可能是未知的, 大多数编译器都不支持内联递归函数); 虚函数做内联的主要原因则是想把它的函数体放在类定义内, 或者是为了图方便, 当作文档描述其行为, 比如精短的存取函数;
[虚函数不能内联展开的一个原因是在动态调用/多态的机制下, 编译时无法确定调用的是哪个类中的函数, 需要运行时通过vptr/vtbl来确定;]
[Remove]
1.4 -inl.h文件
Tip 复杂的内联函数的定义, 应放在后缀名为 -inl.h的头文件中;
内联函数定义必须放在头文件中, 编译器才能在调用点内联展开定义; 然而, 实现代码理论上应该放在 .cc文件中, 我们不希望 .h文件中有太多实现代码, 除非在可读性和性能上有明显优势;
如果内联函数的定义比较短小, 逻辑比较简单, 实现代码放在 .h中没有问题; 比如, 存取函数的实现应该放在类定义内; 出于编写者和调用者的方便, 较复杂的内联函数也可以放到 .h文件中; 如果你觉得这样会使头文件显得笨重, 也可以把它萃取到单独的 -inl.h中; 这样把实现和类定义分离开来, 当需要时包含对应的 -inl.h即可; [当 .cc使用到这些较大的内联函数时再包含?]
-inl.h文件还可以用于函数模板的定义, 从而增强模板定义的可读性;
别忘了 -inl.h和其他头文件一样, 也需要 #define保护;
<<<
1.5 函数参数的顺序 Function Parameter Ordering
Tip 定义函数时, 参数顺序依次为: 输入参数, 然后是输出参数;
C/C++函数参数分为输入参数, 输出参数, 和输入/输出参数三种; 输入参数一般传值或传 const引用, 输出参数或输入/输出参数则是非 const指针; 对参数排序时, 将只输入的参数放在所有输出参数之前, 尤其是不要仅仅因为是新加的参数就把它放在最后, 即使是新加的只输入参数也要放在输出参数之前;
这条规则并不需要严格遵守, 输入/输出两用参数(通常是类/结构体变量)把事情变得复杂, 为保持和相关函数的一致性, 有时不得不有所变通;
1.6 #include的路径及顺序 Names and Order of Includes
Tip 使用标准的头文件包含顺序可增强可读性, 避免隐藏的依赖: C库, C++库, 其他库的 .h, 本项目内的 .h;
项目内头文件应按照项目源代码目录树结构排列, 避免使用UNIX特殊的快捷目录 . (当前目录) 或 .. (上级目录); 例如 google-project/src/base/logging.h应该按如下方式包含:
1
|
#include “base/logging.h”
|
又如, dir/foo.cc的主要作用是实现或测试 dir2/foo2.h的功能, foo.cc中包含头文件的次序如下:
1) dir2/foo2.h (优先位置, 详情如下)
2) C系统文件
3) C++系统文件
4) 其他库的 .h文件
5) 本项目的 .h文件
这种排序方式可有效减少隐藏依赖; [如果缺少任何一个include, 编译就会失败] 我们希望每个头文件都是可被独立编译的; <译注: 即该头文件本身已包含所有必要的显式依赖> 最简单的方法是将其作为第一个 .h文件 #include进对应的 .cc;
dir/foo.cc和dir2/foo2.h通常位于同一目录下(如 base/basictypes_unittest.cc 和 base/basictypes.h), 但也可以放在不同目录下; [比如作为sdk, 分为public include和private include]
按字母顺序对文件包含进行二次排序是不错的主意 <译注: 之前已经按头文件类别拍过序> [每个类别中的 .h按字母序排列]
[Add] 这种预设的次序下, 如果dir2/foo2.h缺少了任何必须的包含, dir/foo.cc或dir/foo_test.cc就会构建失败; 这样保证了编译首先会在编写这些文件的人手上构建失败, 而不是等到无辜的人在其他package中使用的时候才失败;
你应当将定义了你所需的符号的头文件include进来(除了前置声明); 如果有符号是定义在bar.h, 不要include包含了bar.h的foo.h, 而应该自己include bar.h, 除非foo.h的意图就是帮你包含bar.h的符号; 不管怎样, 在头文件中包含的头就不需要在.cc中再次inlcude; (e.g. foo.cc可以依赖foo.h的includes);
<<<
举例, google-project/src/foo/internal/fooserver.cc的包含次序如下:
1
2
3
4
5
6
7
8
9
10
|
#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"
|
[Add] Exception
有时, 系统特定的代码需要按条件include; 这些代码要在条件之后include; 当然, 要把系统特定的代码做得尽量小而局部化;
1
2
3
4
5
6
7
|
#include "foo/public/fooserver.h"
#include "base/port.h" // For LANG_CXX11.
#ifdef LANG_CXX11
#include <initializer_list>
#endif // LANG_CXX11
|
<<<
译者笔记
1) 避免多重包含是编程时最基本的要求;
2) 前置声明是为了降低编译依赖, 防止修改一个头文件引发多米诺效应;
3) 内联函数的合理使用可提高代码执行效率;
4) -inl.h可提高代码可读性(不常用);
5) 标准化函数参数顺序可以提高可读性和易维护性(对函数参数的堆栈空间有轻微影响, 也有将相同类型放在一起的) [???]
6) 包含文件的名称和使用 . 和 .. 虽然方便却容易混乱, 使用比较完整的项目路径更清晰, 有条理, 包含文件的次序除了美观外, 最重要的是可以减少隐藏依赖, 使每个头文件在"最需要编译"(对应源文件处)的地方编译, 有人提出库文件放在最后, 这样出错先是项目内的文件, 头文件都放在对应源文件的最前面, 这一点足以保证内部错误的及时发现;
2 作用域 Scoping
2.1 名字空间 Namespace
Tip 鼓励在 .cc文件内使用匿名名字空间, 使用具名的名字空间时, 其名称可基于项目或相对路径, 不要使用 using关键字; [不要在头文件使用, cpp中适当使用]
定义: 名字空间将全局作用域细分为独立的, 具名的作用域, 可有效防止全局作用域的命名冲突;
优点:
虽然类已经提供了(可嵌套)的命名轴线(译注: 将命名分割在不同类的作用域内), 名字空间在这基础上又封装了一层;
举例来说, 两个不同项目的全局作用域都有一个类Foo; 这样在编译或运行时造成冲突; 如果每个项目将代码置于不同名字空间中 project::Foo和 project2::Foo作为不同符号就不会冲突;
[Add] 内联名字空间
[c++11 http://en.cppreference.com/w/cpp/language/namespace] 自动地将名字放置到作用域
1
2
3
4
5
|
namespace
X {
inline
namespace
Y {
void
foo();
}
}
|
表达式 X::Y::foo()和 X::foo()是可互换的interchangeable; 内联名字空间主要目的是为了版本间的ABI兼容;
<<<
缺点:
名字空间具有迷惑性; 因为它们和类一样提供了额外的(可嵌套的)命名轴线;
[Add] 内联名字空间, 有时候容易混淆, 因为使用的名字和限定的声明名字空间并不完全一致; 它们只在一些大型的版本方案中使用; <<<
在头文件中使用匿名空间导致违背C++的唯一定义原则(One Definition Rule--ODR); [可以在cpp中使用以替代static的使用]
结论: 根据下文将要提到的策略合理使用命名空间;
2.1.1 匿名名字空间 Unnamed Namespaces
- 在 .cc文件中, 允许甚至鼓励使用匿名名字空间, 以避免运行时的命名冲突;
1
2
3
4
5
6
7
|
namespace
{
// .cc 文件中
// 名字空间的内容无需缩进
enum
{ kUNUSED, kEOF, kERROR };
// 经常使用的符号
bool
AtEof() {
return
pos_ == kEOF; }
// 使用本名字空间内的符号 EOF
}
// namespace
|
然而, 与特定类关联的文件作用域声明会在该类中被声明为类型, 静态数据成员或静态成员函数, 而不是匿名名字空间的成员; 匿名空间结束时使用注释 // namespace标识;
[Add]
1
2
3
4
5
6
7
8
9
10
11
12
|
namespace
{
// This is in a .cc file.
// The content of a namespace is not indented.
//
// This function is guaranteed not to generate a colliding symbol
// with other symbols at link time, and is only visible to
// callers in this .cc file.
bool
UpdateInternals(Frobber* f,
int
newval) {
...
}
}
// namespace
|
<<<
- 不要在 .h文件中使用匿名名字空间;
2.1.2 具名的名字空间 Named Namespaces
具名的名字空间使用方式如下:
- 用名字空间把文件包含, gflags [https://code.google.com/p/gflags/] 的声明/定义, 以及类的前置声明以外的整个源文件封装起来, 以区别于其他名字空间;
.h文件
1
2
3
4
5
6
7
8
9
10
11
|
namespace
mynamespace {
// 所有声明都置于命名空间中
// 注意不要使用缩进
class
MyClass {
public
:
…
void
Foo();
};
}
// namespace mynamespace
|
.cc文件
1
2
3
4
5
6
7
8
|
namespace
mynamespace {
// 函数定义都置于命名空间中
void
MyClass::Foo() {
…
}
}
// namespace mynamespace
|
典型的 .cc文件包含更多, 更复杂的细节, 比如引用其他名字空间的类等; [很多地方直接使用 using namespace...]
1
2
3
4
5
6
7
8
9
10
11
12
|
#include “a.h”
DEFINE_bool(someflag,
false
, “dummy flag”);
class
C;
// 全局名字空间中类 C 的前置声明
namespace
a {
class
A; }
// a::A 的前置声明
namespace
b {
…code
for
b…
// b 中的代码
}
// namespace b
|
- 不要在名字空间std内声明任何东西, 包括标准库的类前置声明; 在std名字空间声明实体会导致未定义的行为, 比如不可移植not portable; 要声明标准库下的实体, 应该包含对应的头文件;
- 最好不要使用 using关键字using-directive, 而使得名字空间下的所有名称都可以使用:
1
2
|
// 禁止 —— 污染名字空间
using
namespace
foo;
|
- 在 .cc文件, .h文件的函数, 方法或类中, 可以使用 using关键字;
1
2
3
|
// 允许: .cc 文件中
// .h 文件的话, 必须在函数, 方法或类的内部使用
using
::foo::bar;
|
[Add]
在.cc中任何地方都允许名字空间别名, 在包围了整个.h文件的具名空间以及方法和函数中中的任何地方也是如此;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// Shorten access to some commonly used names in .cc files.
namespace
fbz = ::foo::bar::baz;
// Shorten access to some commonly used names (in a .h file).
namespace
librarian {
// The following alias is available to all files including
// this header (in namespace librarian):
// alias names should therefore be chosen consistently
// within a project.
namespace
pd_s = ::pipeline_diagnostics::sidetable;
inline
void
my_inline_function() {
// namespace alias local to a function (or method).
namespace
fbz = ::foo::bar::baz;
...
}
}
// namespace librarian
|
Note: 在.h中的别名对于任何#include这个头的文件中也是可见的, 因此公共的头文件(在项目外可用的)和那些被交叉包含transitively #included的头文件中, 应该避免定义别名, 一部分的原因是为了保持公共的API尽量小;
- 不要使用内联名字空间;
<<<
2.2 嵌套类 Nested Classes
Tip 当公有嵌套类作为接口的一部分时, 虽然可以直接将他们保持在全局作用域中, 但将嵌套类的声明置于名字空间内是更好的选择;
定义:
在一个类内部定义另一个类; 嵌套类也被称为 成员类(member class)
1
2
3
4
5
6
7
8
9
|
class
Foo {
private
:
// Bar是嵌套在Foo中的成员类
class
Bar {
…
};
};
|
优点: 当嵌套(或成员)类只被外围类使用时非常有用; 把它作为外围类作用域内的成员, 而不是去污染外部作用域的同名类; 嵌套类可以在外围类中做前置声明, 然后在 .cc文件中定义, 这样避免在外围类的声明中定义嵌套类, 因为嵌套类的定义通常只与实现相关;
缺点: 嵌套类只能在外围类的内部做前置声明; 因此, 任何使用了 Foo::Bar*指针的头文件不得不包含Foo的整个声明;
结论: 不要将嵌套类定义成公有, 除非它们是接口的一部分, 比如, 嵌套类含有某些方法的一组选项;
2.3 非成员函数, 静态成员函数和全局函数 Nonmember, Static Member, and Global Functions
Tip 使用静态成员函数或名字空间的非成员函数, 尽量不要用裸的全局函数;
优点: 某些情况下, 非成员函数和静态成员函数是非常有用的, 将非成员函数放在名字空间内可避免污染全局作用域;
缺点: 将非成员函数和静态成员函数作为新类的成员或许更有意义, 当它们需要访问外部资源或具有重要的依赖关系时更是如此;
结论:
有时, 把函数的定义同类的实例脱钩是有益的, 甚至是必要的; 这样的函数可以被定义成静态成员, 或是非成员函数; 非成员函数不应依赖于外部变量, 应尽量置于某个名字空间内; 相比单纯为了封装若干不共享任何静态数据的静态成员函数而创建类, 不如使用命名空间;
定义在同一编译单元的函数, 被其他编译单元直接调用可能会引入不必要的耦合coupling和链接时依赖; 静态成员函数对此尤其敏感; 可以考虑提取到新类中, 或者将函数置于独立库的名字空间内;
如果必须定义非成员函数, 又只是在 .cc文件中使用它, 可以使用匿名名字空间或 static linkage链接关键字(如 static int Foo() {...})限定其作用域;
2.4 局部变量 Local Variables
Tip 将函数变量尽可能置于最小作用域内; 并在变量声明时进行初始化;
C++允许在函数的任何位置声明变量, 我们提倡在尽可能小的作用域中声明变量; 离第一次使用越近越好, 这使得代码阅读者更容易定位变量声明的位置, 了解变量的类型和初始值; 特别是, 应使用初始化的方式替代声明再赋值; 比如:
1
2
3
|
int
i;
i = f();
// 坏——初始化和声明分离
int
j = g();
// 好——初始化时声明
|
[Add]e.g.
1
2
3
|
vector<
int
> v;
v.push_back(1);
// Prefer initializing using brace initialization.
v.push_back(2);
|
Good
1
|
vector<
int
> v = {1, 2};
// Good -- v starts initialized
|
通常被if, while和 for语句使用的变量正常应该是在这些语句中被声明, 这样这些变量可以限制在这些作用域内;
1
|
while
(
const
char
* p =
strchr
(str,
'/'
)) str = p + 1;
|
[Remove]
Note GCC可正确实现了 for(int i=0; i<10; ++i) (i的作用域仅限for循环内), 所以其他 for循环中可以重新使用i; 在 if和 while等语句中的作用域声明也是正确的; 如:
1
|
while
(
const
char
* p =
strchr
(str, ‘/’)) str = p + 1;
|
<<
WARNING 如果变量是一个对象, 每次进入作用域都要调用其构造函数, 每次退出作用域就会调用其析构函数;
1
2
3
4
5
|
// 低效的实现
for
(
int
i = 0; i < 1000000; ++i) {
Foo f;
// 构造函数和析构函数分别调用 1000000 次!
f.DoSomething(i);
}
|
在循环作用域外面声明这类变量要高效得多:
1
2
3
4
|
Foo f;
// 构造函数和析构函数只调用 1 次
for
(
int
i = 0; i < 1000000; ++i) {
f.DoSomething(i);
}
|
2.5 静态和全局变量 Static and Global Variables
Tip 禁止使用 class类型的静态或全局变量: 它们会导致很难发现的bug和不确定的构造和析构函数调用顺序;
[Add] 如果这类变量是 constexpr的就允许, 它们没有动态的初始化或析构;
[c++11 constexpr http://www.devbean.net/2012/05/cpp11_constexpr/ 类似static inline; http://en.cppreference.com/w/cpp/language/constexpr ]
<<<
静态生存周期的对象, 包括全局变量, 静态变量, 静态成员变量,以及函数静态变量, 都必须是原生数据类型(POD: Plain Old Data); 只能是 int, char, float, void, 以及POD类型的数组/结构体/指针; 永远不要使用函数返回值初始化静态变量; 不要在多线程代码中使用非const的静态变量;
不幸的是, 静态变量的构造函数, 析构函数以及初始化操作的调用顺序在C++标准中未明确定义, 甚至每次编译构建都有可能会发生变化, 从而导致难以发现的bug; 比如, 结束程序时, 某个静态变量已经被析构了; 但代码还在运行 -- 其他线程很可能--试图访问该变量, 直接导致崩溃;
[Add]
静态变量的ctor和初始化调用在C++内没有明确定义, 每次构建都有可能不同, 这样会造成bug难以定位; 因此除了弃用全局class类型, 我们也不允许使用function执行结果来初始化static的POD变量, 除非那个方法(getenv(), getpid())不依赖其他任何的全局变量; (这个禁令不包含在function作用域内的static变量, 因为这种情况下它的初始化次序有很好的定义, 而且初始化只有在控制流程到达它的声明是才开始;
类似地, global和static变量在程序终止时会被析构, 不管是从main()还是从exit()终止的; 析构被调用的顺序应该和构造被调用的顺序相反; 由于ctor和dtor的次序是不确定的indeterminate; 利润, 在程序终止时一个static变量可能已经被析构, 但是程序依然运行--可能在另一个线程中--试着去获取它就会造成fails; 或者, 一个static的string变量的dtor可能优先被另一个变量的析构调用, 是因为那个变量包含了这个string的引用;
<<<
所以我们只允许POD类型的静态变量; 本条规则完全禁止 vector(使用C数组替代), string(使用 const char*, const char[]), 以及它以任意方式包含或指向类实例的东西成为静态变量; 出于同样的理由, 不允许用函数返回值来初始化静态变量;
如果你确实需要一个class类型的静态或全局变量, 可以考虑在 main()函数或 pthread_once()内初始化一个永远不会回收的指针;
[Add] 注意必须是一个原始raw指针, 不能是smart pionter, 因为smart pointer的析构会出现析构次序order-of-destructor的问题, 那是我们要避免的;<<<
NOTE 译注 上文提及的静态变量泛指静态生存周期的对象, 包括: 全局变量, 静态变量, 静态成员变量, 以及函数静态变量;
译者笔记
1) .cc中的匿名名字空间可避免命名冲突, 限定作用域; 避免直接使用 using关键字污染命名空间;
2) 嵌套类符合局部使用原则, 只是不能在其他头文件中前置声明, 尽量不要public;
3) 尽量不用全局函数和全局变量, 考虑作用域和命名空间限制, 尽量单独形成编译单元;
4) 多线程中的全局变量(含静态成员变量)不要使用 class类型(含STL容器), 避免不明确行为导致的bug;
5) 作用域的使用, 除了考虑名称污染, 可读性之外, 主要是为降低耦合, 提高编译/执行效率;
---TBC---YCR