3 作用域
3.1 命名空间
命名空间使用规则:
- 命名空间的命名规则请参照
- 命名空间结束的地方应该有注释,如下面的例子所示。
- 一般情况下,命名空间应包含源代码中除 include 语句、gflags 声明/定义以及在其它命名空间中定义的类的前置声明以外的所有内容。
// In the .h file
namespace mynamespace {
// All declarations are within the namespace scope.
// Notice the lack of indentation.
class MyClass {
public:
...
void Foo();
};
} // namespace mynamespace
// In the .cc file
namespace mynamespace {
// Definition of functions is within scope of the namespace.
void MyClass::Foo() {
...
}
} // namespace mynamespace
更复杂的.cc 文件可能包含更多的细节,比如引用其他命名空间的类等。
#include "a.h"
DEFINE_FLAG(bool, someflag, false, "dummy flag");
namespace a {
using ::foo::bar;
...code for a...
// Code goes against the left margin.
} // namespace a
- 不要在命名空间 std 内声明任何东西,包括标准库中类的前置声明。在 std 命名空间声明实体会导致不确定的问题,比如不可移植。
- 不要使用 using 语句来导出一个命名空间中所有可用的名字。
// Forbidden -- This pollutes the namespace.
using namespace foo;
- 除了明确标注只在内部使用的命名空间外,不要在命名空间的范围内使用命名空间别名,因为导入到头文件中命名空间的任何内容都将成为该文件输出的公共 API 的一部分。
// 减少对.cc 文件一些常用名称的访问
namespace baz = ::foo::bar::baz;
// 减少对.h 文件一些常用名称的访问
namespace librarian {
namespace impl { // Internal, not part of the API.
namespace sidetable = ::pipeline_diagnostics::sidetable;
} // namespace impl
inline void my_inline_function() {
// namespace alias local to a function (or method).
namespace baz = ::foo::bar::baz;
...
}
} // namespace librarian
- 不要使用内联(inline)命名空间。
3.2 匿名命名空间
规则 : .cc 文件中无需外部引用的定义,应放到匿名命名空间中或者声明为 static,但不要在.h 文件中这样用。
定义 : 匿名命名空间内的声明具有内链接的性质,函数和变量的内链接可通过将其声明为static 来实现。具有内链接性质的实体都不能在文件外访问。如果不同文件声明相同名称的具有内链接性质的实体,那么这两个实体是完全独立的。
规则 : 像命名空间那样格式化匿名命名空间,匿名空间结束时用注释 // namespace 标识.
namespace {
...
} // namespace
3.3 非成员函数、静态成员函数和全局函数
规则 : 非成员函数应放在命名空间中,尽量避免使用全局函数。函数应该用命名空间来分组,而不是用类来分组。
优点 : 某些情况下,非成员函数和静态成员函数会非常有用,将非成员函数放在命名空间内可避免污染全局作用域。
缺点 : 将非成员函数和静态成员函数作为新类的成员或许更有意义,尤其是当它们需要访问外部资源或具有严重的外部依赖关系时更是如此。
结论 : 有时,把函数的定义同类的实例脱钩是有益的,甚至是必要的。这样的函数可以被定义成静态成员,或是非成员函数。非成员函数不应依赖于外部变量,应尽量置于某个命名空间内。不共享任何静态数据的函数应放入到命名空间中,而不是作为静态成员函数用类来封装它们。
例如:在 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 文件中使用它,应使用内链接限定其作用域。
3.4 局部变量
规则 : 函数中的变量尽可能置于最小作用域内,并在变量声明时进行初始化。
规则 : 虽然 C++允许在函数的任何位置声明变量,不过还是提倡在尽可能小的作用域中声明变量,离第一次使用越近越好。这会使代码浏览者更容易定位变量声明的位置,了解变量的类型和初始值。
规则 : 应在变量声明时对其初始化,而不是先声明再赋值。
例如:
int i;
i = f();
//坏 -- 初始化和声明分离
int j = g(); // 好 -- 初始化时声明
std::vector<int> v;
v.push_back(1); // 用花括号初始化更好
v.push_back(2);
std::vector<int> v = {1, 2}; // 好 -- v 一开始就初始化
在 if, while 和 for 语句中所需的变量通常也是在这些语句中声明,因此这限制了这些变量的作用域。如下
while(const char* p = strchr(str, '/')) str = p + 1;
特别强调:如果变量是一个对象,每次进入作用域都要调用其构造函数,每次退出作用域都要调用其析构函数。
// 低效的实现:
for (int i = 0; i < 1000000; ++i) {
Foo f; // My ctor and dtor get called 1000000 times each.
f.DoSomething(i);
}
在循环作用域外面声明这类变量要高效的多:
Foo f; // 构造函数和析构函数只调用 1 次.
for (int i = 0; i < 1000000; ++i) {
f.DoSomething(i);
3.5 静态和全局变量
规则:禁止使用 class 类型的静态或全局变量,这会导致难以发现的 bug 和不确定的构造和析构函数调用顺序。不过 constexpr 变量除外,因为这种方式不涉及动态初始化或析构。
规则:静态生存周期的对象(包括:全局变量、静态变量、静态类成员变量和函数静态变量)都必须是原生数据类型 (POD : Plain Old Data),POD 类型包括:int、char、float、指针、POD 类型的数组和结构体。
规则:由于类的构造函数和静态变量的初始化的顺序在 C++定义不完整,甚至会随着Build 而改变,这会导致难以发现的 Bug。因此禁止使用函数返回值来初始化非局部变量,除非该函数(比如:getenv()或者 getpid())不依赖于任何其它全局变量。例外,函数作用域内的静态 POD 变量可用函数返回值来初始化,因为这种情形的初始化定义是明确的,只有在指令执行到变量的声明那里才会发生函数调用。
规则:程序从 main()返回或调用 exit()时,全局变量和静态变量会被析构。析构顺序与构造顺序相反,C++中析构顺序定义不完整。例如,程序结束时某个静态变量已经被析构了,但其它线程的代码还在运行,并且很有可能会访问已被释放的静态变量;再例如,一个静态 string 类型变量的析构函数可能会在引用了它的那个变量被析构前被调用。因此,请使用 quick_exit()来终止程序,因为这个函数不会执行任何析构,也不会执行atexit()所绑定的任何处理程序。
规则:只使用 POD 类型的静态变量,用 C 数组代替 std::vector,用 const char []代替string。
规则:如果确实需要一个 class 类型的静态或全局变量,可在 main()函数或pthread_once()内初始化一个指针且永不回收。注意只能用原始指针,勿使用智能指针,因为智能指针的析构函数会涉及到析构顺序问题。