Felomeng翻译:Google C++ 编程规范——作用域

1       作用域

1.1     命名空间(namespace)

在.cc中提倡使用匿名命名空间(unnamed namespace[i])。而定名命名空间(named namespace)的命名应该以项目及(如果不在根目录的话)项目中的路径来命名。不要使用using关键字。

定义:命名空间将作用域分割为相互独立的,具有特定名称的作用域。这样就可以避免在全局作用域中容易产生的同名冲突。

优点:在类提供的(有层次的)名称隔离方法基础上提供了另一套(有层次的)命名隔离方法[ii]。

例如,两个不同的工程中都含有全局类Foo,这两个类名可能在编译时或者运行时产生同名冲突。但是,如果在每个工程中都将他们放进相应的命名空间中,project1::Foo和project2::Foo就是两个完全不同的类名,而不会再产生同名冲突了。

劣势:命名空间容易让人感到迷惑,因为类本身已经提供了一套(有层次的)名称隔离方法,而命名空间在这个基础上又添加了一套(有层次的)名称隔离方法。

在头文件中使用匿名命名空间,很容易违反C++的单一定义规则(One Definition Rule[iii])。

结论:根据下述原则使用命名空间。

匿名命名空间

在.cc文件中,不仅允许使用匿名命名空间,并且,为了避免运行时同名冲突,还提倡使用匿名命名空间:

·         namespace {                           // This is in a .cc file.

·         

·         // The content of a namespace is not indented

·         enum { kUnused, kEOF, kError };       // Commonly used tokens.

·         bool AtEof() { return pos_ == kEOF; }  // Uses our namespace's EOF.

·         

}  // namespace

但是,文件内的与某个特定类相关的实体,应当在该类内声明为类型、静态成员或静态成员函数,而不是在类外声明为匿名命名空间的一个成员。如上例所示,在匿名空间的结尾处要添加注释“// namespace”。

在.h文件中不要使用匿名命名空间。

定名命名空间

定名命名空间用法如下:

除文件最前端的包含语句、gflags[iv]定义(声明)以及前置声明的别的命名空间中的类之外,命名空间包含整个源代码文件的内容:

·         // 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_bool(someflag, false, "dummy flag");

 

class C;  // Forward declaration of class C in the global namespace.

namespace a { class A; }  // Forward declaration of a::A.

 

namespace b {

 

...code for b...         // Code goes against the left margin.

 

}  // namespace b

不要在std命名空间内定义声明内容,包括标准库中类的前置声明。在命名空间中声明任何实体都是未定义行为(undefined behavior [v]),就是说不可移植。声明标准库中实体的方法是包含相应的头文件。

没有必要使用using语句让命名空间内所有的实体名都可用。

·         // Forbidden -- This pollutes the namespace.

using namespace foo;

在.cc文件中的任何位置都可以使用using语句,在.h文件中的函数、方法和类内可以使用using语句。

·         // OK in .cc files.

·         // Must be in a function, method or class in .h files.

using ::foo::bar;

在.cc文件内的任何地方,包含整体文件的定名命名实体内的任何地方,以及函数和方法内部都可以使用命名实体别名(namespace aliases)。

·         // 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

注意,在头文件中定义的别名,在任何包含这个头文件的文件中都是可见的。所以在公共头文件中(这些头文件可以在工程外被包含),以及间接地被这些公共头文件包含的头文件中,应当避免使用别名。这也是保持API尽量简洁总目标的一部分。

1.2     内嵌类(Nested Class)

虽然,当内嵌类是接口的一部分时,可以声明为public,但最好还是使用命名空间将内嵌类声明与全局作用域隔离。

定义:在类内部定义的另一个类;这个内部的类也称为成员类(member class)。

class Foo {

 

 private:

  // Bar is a member class, nested within Foo.

  class Bar {

    ...

  };

 

};

优点:当只有内嵌类的外层类(enclosing class)使用它时很有用,这时可以将它定义为外层类的成员,从而把它的作用域限定在外层类的内部,避免声明在外部作用域所带来的名称污染(即不会影响到用不到其他的类)。内嵌类可以在外层类中前置声明,然后在.cc文件中定义。这样一来,就可以避免在外层类的定义中定义整个内嵌类,而且内嵌类一般也只与实现相关。

劣势:内嵌类只能在外层类的定义内前置声明。所以,任何使用Foo::Bar*指针的头文件都需要包含Foo类的完整声明。

结论:除非内嵌类是接口的一部分,否则不要声明为public的,例如,一个类包含某方法的多个选项。

1.3     非成员函数(Nonmember)、静态成员函数( Static Member)、全局函数(Global Functions)

优先使用命名空间内的非成员函数或静态成员函数,尽量不要使用全局函数。

优点:有时,非成员函数和静态成员函数很有用。将非成员函数放在某命名空间内,可以防止其对全局命名空间的名称污染。

劣势:将非成员函数和静态函数作为一个新类的成员函数可能更合理,尤其当这些函数访问大量外部资源或具有大量依赖时。

结论:有时,定义一个类外函数是有用的,甚至是必须的。这样的函数可以是一个静态成员函数或者非成员函数。非成员函数不应依赖外部变量,并且应当总是只存在于一个命名空间内。不要用类把不含共享静态数据的静态成员函数封装起来,把这样的函数放到命名空间中

和其他类在同一编译单元内定义的函数,如果在别的编译单元中直接调用,可能会引起额外的耦合和连接依赖,静态成员函数尤其明显。这时,应当考虑建立一个新类来封装这些函数,也可以将这些函数封装到独立类库的命名空间中。

如果必须定义一个非成员函数,并且只在当前.cc文件中使用,使用匿名命名空间或者static连接关键字(比如static int Foo() {...})来限定它的作用域。

1.4     局部变量(Local Variable)

将函数中的变量作用域限定在尽量小的范围内,并且在定义时就初始化。

C++允许在函数内任何地方声明变量。但是,要在尽量小的作用域中声明变量,最好在第一次使用前声明。这样,读代码的人就很容易找到定义,从而知道这个变量是什么类型,初始值是多少。这里要特别强调:定义和初始化应放在同一语句,而不是先定义后再单独进行赋值。

int i;

i = f();      // Bad -- initialization separate from declaration.

int j = g();  // Good -- declaration has initialization.

值得注意的是,gcc中对for (int i = 0; i < 10; ++i)语句的实现是正确的(变量i的作用域限定在for循环内)。因此,在与这个for循环处于同一作用域中的其他for循环中还可以使用变量i。gcc对if语句和while语句中声明的变量的作用域的实现也是正确的。比如:

while (const char* p = strchr(str, '/')) str = p + 1;

警告:如果变量是一个对象,那么在每次进入作用域时都调用构造函数并创建这个对象,而在出了作用域之后又会调用析构函数来销毁这个对象。

// Inefficient implementation:

for (int i = 0; i < 1000000; ++i) {

  Foo f;  // My ctor and dtor get called 1000000 times each.

  f.DoSomething(i);

}

可以将这样的变量声明在循环体外来提高效率。

Foo f;  // My ctor and dtor get called once each.

for (int i = 0; i < 1000000; ++i) {

  f.DoSomething(i);

}

1.5     静态变量(Static Variable)和全局变量(Global Variable)

禁止使用静态或全局的类类型变量:它们会引起不易找到的bug,因为它们的构造和析构的顺序是不固定的。

对象如果存储在内存中的静态区(包括全局变量、静态变量、静态类成员变量和函数内部静态变量),必须是简单旧式数据(Plain Old Data[vi]),即整型、字符型、单精度型或者简单旧式数据类型的指针、数组、结构体。

但是,C++中并未明确规定静态变量的构造函数和析构函数的调用顺序,甚至每次编译生成后的调用顺序可能都是不一样的。这样就可能导致不易发现的bug。因此,不但禁止将类类型作为全局变量,还禁止使用函数返回值对静态简单旧式数据变量进行初始化,除非这个函数(如getenv(),或getpid())本身并不依赖于其他的全局变量。

按规定,析构函数的调用顺序与构造函数的调用顺序应当是正好相反的。因为构造函数的调用顺序是不确定的,所以析构函数的调用顺序也是不确定的。例如:在程序运行过程中,一个静态变量已经被销毁了,但是程序依然在运行,也许正好有另外一个线程试图去使用这个变量,这当然会失败。抑或是在调用某“string”类型静态变量析构函数后,才去调用另一个变量的析构函数,而这个变量内包含有对此string的引用。

因此,规定静态变量只能是简单旧式数据类型。这条规则明确禁止把vector (可以使用C语言中的数组代替)和string (可以使用const char [])声明为静态。

如果需要使用静态的或全局的类类型变量,那么请在main函数或pthread_once函数中初始化为一个指针(这个指针将不会被释放)。注意,这个指针必须是裸指针(raw pointer[vii]),而不能是智能指针(smart pointer),因为智能指针的析构函数也有上文讨论过的调用顺序问题。

 

--------------------------------------------------------------------------------

[i] 译者注:unnamed namespace原意为未命名的命名空间。在C++中,命名空间分为命名的命名空间和未命名的命名空间。实际上,未命名的命名空间在编译时,编译器也会为它分配一个自动生成的唯一名称,以与其他的命名空间相区分。故这里译为匿名命名空间(不是没有名,而是人看不到它的名字)。与此对应,named namespace译为定名命名空间。

[ii] 译者注:这里读者需要对C++的历史有一些了解。C++是由C语言进化来的。在C语言中,所有的方法、函数以及变量等都是全局作用域命名的。但是随着软件工程规模的扩大,人们发现这样很不利于管理(很容易产生同名冲突)。于是有人提出了类的概念(当然,这只是类概念产生的众多原因中的一个)。这样每个类中有自己的方法、函数和变量,与其他类中(以及全局)的相应内容相互区分。故,类与命名空间的共同点就是对方法、函数和变量等名称的隔离(当然,命名空间还可以用来隔离类名)。

[iii] 译者注:One Definition Rule (缩写:ODR)定义为:

1.       任何编译单元中,模板、类型、函数、对象等最多都只能有一次定义。当然,它们中有的可以在多处声明。定义就是构造一个实例。

2.       在整个程序中,一个对象或内联函数,只能有一处定义。如果使用了某对象或函数,那么这个对象或函数就必须得有唯一的定义。可以声明一个对象或函数,但却不使用它,这时不需要对它进行定义。任何情况下,都不能有多于一次的定义。

3.       有些内容,如类型、模板、外部内联函数(extern关键字修饰的内联函数),可以在多个编译单元中定义。对于一个给定的实体,所有的定义必须一致。不同编译单元中,同名且同类型(同参数)的非外部对象和函数是不同的实体。

[iv] 译者注:gflags是一个库文件包,主要用来处理命令行中的标识。从这点上讲,它可以替代getopt()函数。不同的是,它更具灵活性。不仅支持C++的类型(如string),还可以在使用标识的源代码文件中定义新的标识。

[v] 译者注:undefined behavior是指在C/C++中,为了提高实现编译器的灵活性,对某些特定行为,在语言的标准中并没有规定他们的具体表现。所以,不同的编译器中,这些行为的表现可能是不同的。也是下面说不可移植的原因。

[vi] 译者注:Plain Old Data,是未经包装,且不含任何面向对象特性的变量类型构成的数据结构。

[vii] 译者注:raw pointer,即普通指针,与智能指针概念相对应。我们知识,智能指针是经过封装的指针,所以普通指针被译为裸指针。

 

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/felomeng/archive/2011/03/24/6273974.aspx

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值