Google C++ 编程风格指南(中文翻译)-1

8 篇文章 0 订阅
5 篇文章 0 订阅

Google C++编程风格指南

http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml

Revision 3.180

Benjy Weinberger
Craig Silverstein
Gregory Eitzmann
Mark Mentovai
Tashana Landray

翻译:Hector.lxm@gmail.com

1       背景

C++Google的许多开源项目使用的主要的开发语言。就如每个C++用户所知道的那样,这门语言给我们许多非常强大的功能的同时,也带来了复杂的麻烦,比如,可能是代码里潜伏着难以发现的bug,使代码难以阅读和维护。

本文的目的就是,详细描述在写C++代码时,该做哪些,不该做哪些,从而避免那些复杂的麻烦。这些规则能使代码在具有可维护性的同时,不妨碍编程人员使用C++语言的丰富功能。

编程风格,被称为可读性,也被称为管理C++代码的规范。术语风格(style)并不是很确切,因为这些规范涉及的范围,远远不只是源文件格式。

使代码可维护的一种方法是强制一致性。让任何一个编程人员能很快,很容易的看懂别人的代码是很重要的。维护一个统一的风格,并且遵循同样的规范,也意味着我们可以更容易的通过模式匹配,来推断各种各样的标志符是干什么用的,哪些常量条件为真。制定统一标准、必要的风格、以及模式,能使代码更易于理解。在某些情况下,可能也会有些关于改变某种风格规则的好的论点,尽管如此,为了保证一致性原则,我们仍然保持它。

这份指南指出的另外一个问题是,C++功能的膨胀。C++是一门具有许多高级功能的,高深莫测的,庞大的编程语言。在某些情况下,我们约束,甚至禁止使用某些功能。我们这样做是为了使我们的代码尽量简洁,以避免那些高深莫测的功能可能引起的各种总常见的错误或问题。这份指南列出了这些功能,并解释了限制使用他们的原因。

Google的开源项目都遵循了这份指南的要求。

要注意的是,这份指南并不是C++的辅导材料,我们假定读者已经很熟悉这门语言了。

2       头文件

一般来说,每个.cc(或.cpp,看来Google只要用GCC)文件都应该有相应的.h文件。也有一些常见的特例,比如单元测试或者有些非常小的.cc文件只有一个main()函数。

正确使用头文件能显著改变你的代码的可读性、大小、以及效率。

下面的规则将指导你认识各种使用头文件的陷阱。

2.1     #define 保护符

所有的头文件都应该有#define保护来防止多重引入。#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_

 

2.2     头文件依赖

如果能用前置声明,就不要用#include

一旦你在代码中包含了一个头文件,你也就引入了对它的依赖:当那个头文件发生改变时,你就要重新编译自己的代码。如果你的头文件包含了其他的头文件,那些头文件的改动都将导致的代码要重新编译。所以,我们要尽量使头文件最小化,特别是那些被其他头文件包含的头文件。

你可以在自己的头文件中使用前置声明,从而极大地减少需要包含的头文件的数量。例如,如果得头文件需要用到File类,然而并不需要访问File类的声明,你就可以在头文件中使用前置声明 class File,而不用 #include “file/base/file.h”

我们怎样才能在头文件中使用一个Foo类,而不必去访问它的声明呢?

我们可以声明数据成员为指针类型或引用类型(Foo*Foo&)。

我们可以声明(绝不是定义)函数的参数和/或返回值为类型Foo。(一个例外是如果传值参数Foo或者应用型参数const Foo&,而Foo有一个隐式的单参数的构造函数,那么这时就需要Foo类的定义来支持自动类型转换。)

我们可以声明静态成员变量Foo。这是因为静态成员变量时在类外部定义的。

另一方面,如果你的类是Foo类的子类,或你有类型Foo的数据成员,那么你必须包含Foo的头文件。

有时候,使用指针(最好是scoped_ptr)成员而不是对象成员往往是讲得通的。然而,这又会使代码可读性变得复杂,并且损失性能;所以如果你仅仅是为了最小化引入的头文件,那么建议你不要做这种转换。

当然,.cc文件往往需要那些他们用到的类的定义,而且往往必须包含许多头文件。

住:如果你在源文件中用到了符号Foo,你就需要或者使用#include、或者使用前置声明来引入Foo的定义。不要依赖于那些被间接引入的头文件引入(比如头文件中引入的头文件)的符号。一个例外是,Foomyfile.cc文件中被用到,在myfile.h而不是在myfile.cc#include(或使用前置声明)Foo,是可以的。

2.3     内联函数

只有当函数很小(少于10行代码)的时候才把它定义为内联函数。

Ø  定义

你可以声明一些函数,使编译器把他们扩展为内联的,而不是通过普通的函数调用机制。

Ø  正面的

内联函数可以生成更有效率的代码,只要他足够小。你可以尽管放心地去把那些访问函数(get_()),设值函数(set_()),和那些短小的,性能敏感的函数声明为内联函数。

Ø  反面的

事实上过度使用内联会使你的程序变得更慢。函数的大小,会影响内联后的代码大小。内联一个很小的访问函数往往减小代码大小;而内联一个非常大的函数会极大的增大代码大小。在大多数处理器上,更小的代码往往运行的更快,因为他更好的使用了指令高速缓存处理器。

Ø  结论

有一个很棒的准则:不要内联一个函数,如果它的代码多余10行。要注意析构函数,它们的代码往往比我们看到的要大,这是因为还有很多隐式的成员析构和基类析构调用!

还有一个很有用的准则:把含有循环或分支语句的函数内联并不划算(除非,在大部分情况下,这个循环或分支语句从不会被执行到)。

函数并不总会被内联即使你把他们声明为内联函数,直到这一点很重要。例如,虚函数和递归函数往往不会被内联。通常递归函数是不应该被内联的。把徐函数内联的主要原因是把它的定义放到类中,要么是为了方便,要么为了记录它的行为,比如访问函数,设值函数。

2.4     -inl.h 文件

如果有必要你可以使用带有-inl.h后缀的文件名来定义复杂的内联函数。

内联函数的定义应该放入头文件,所以编译器在调用现场就可以找到内联的定义。但是实现代码还是应该放入.cc文件,而且我们不喜欢把过多的代码放入.h文件,除非那样做带来更好的可读性或性能优势。

如果内联函数的定义非常短,并且只有非常少的逻辑,那么你可以把它放入你的.h文件。例如,存取函数完全应该放入类的定义中。为了实现和调用的方便,一些比较复杂的内联函数也应该被放入.h文件中。当然这会导致这个.h文件变得不灵活,因而我可以把这些内联函数放入单独的-inl.h文件中。这样就把实现从类的定义中分离了出来,然而在需要的地方我们仍然可以随时包含这些实现。

-inl.h文件的另一种使用方法是定义函数模板。这样可以使你的模板定义更易于阅读。

不要忘记-inl.h文件也需要#define保护符( 2.1 ),就像其它的头文件一样。

2.5     函数参数顺序

当定义函数时,参数的顺序是:输入参数,然后输出参数。

C++函数的参数,要么是函数的输入,要么是 函数的输出,后者两者都是。输入参数往往是值传递,或者是常量引用;而输出参数或者输入/输出参数应该是非常量的指针。当对函数参数排序时,应该把只输入的参数放在前面。特别的,不要只是简单的把新加入的参数放在最后,把新的只是输入的参数放在输出参数前。

这并不是硬性规定。那些既是输入又是输出的参数(往往是类或结构体)总是把事情搞乱,结合相关的函数也许需要你违反这一原则。

2.6     包含文件的名字和顺序

使用标准的顺序,这样可以便于阅读,并且避免隐藏依赖。这个顺序是:C库,C++库,其它的库的头文件,你自己工程的头文件。

一个工程的所有头文件应该被列为工程源文件目录的子项,不要用UNIX的文件夹的快捷方式(.—当前文件夹,或者..父文件夹)。例如,google-awesome-project/src/base/logging.h应该被包含为

         #include “base/logging.h”

dir/foo.cc文件中,如果你想实现或测试dir2/foo2.h中的内容,你的包含顺序应该如下:

1.       dir2/foo2.h (首选位置具体看下面)。

2.       C的系统文件

3.       C++的系统文件

4.       其它的库头文件

5.       你的工程的头文件

首选的顺序可以减小隐藏依赖的可能性。我们想让每个头文件能够独立编译。达到这一目的的最简单的办法是,确保它们在一些.cc文件中是被包含的第一个.h文件。

dir/foo.ccdir2/foo2.h经常在同一文件夹下(例如:base/basictype_test.ccbase/basictypes.h),但也可能会在不同的文件夹下。

对于每个部分来说(上面的1~5的每个部分),可以按字母排序来包含头文件。

例如,在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”

3       作用域

3.1     名字空间 namespace

鼓励在.cc文件中使用无名名字空间。对于有名名字空间来说,其名字应该基于他的工程,以及路径来定。不要用使用指令using std::cout—using declaration, 使用声明using namespace std—using directive使用指令)。

Ø  定义

名字空间把全局作用域细分为不同的,具有名称的作用域,因而对于防止在全局作用域里的名字冲突非常有用。

Ø  正面的

名字空间提供了一个水平的名字轴坐标,除了类提供的名字坐标轴(分层的)。

例如,如果两个工程在全局作用域里都有一个类Foo,那么在编译时或运行时,它们可能就会引起冲突。如果每个工程把它们的代码放入名字空间,project1::Fooproject2::Foo,现在就是明显不同的符合了,因而不会引起冲突。

Ø  反面的

名字空间可能会让人迷惑,因为它们提供了一个名字额外(分层的)的维度,除了类的名字维度(也是分层的)外。

在头文件中使用无名名字空间极易违反C++一次定义规则ODR—One Definition Rule)。

Ø  结论

根据以下的方针来使用名字空间。

3.1.1  无名名字空间

无名名字空间是允许的,尤其鼓励在.cc文件中使用,来避免运行时冲突:

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

 

// 这个名字空间的内容没有缩进

enum { kUnused, kEOF, kError };       // 常用标识符

bool AtEof() { return pos_ == kEOF; }  // 使用我们自己名字空间的EOF

 

}  // namespace

但是,跟一个特定的类联系在一起的文件作用域声明,可能把类声明为类型,静态数据成员或静态成员函数而不是作为无名名字空间的成员。以注释//namespace结束一个无名名字空间。

不要再.h文件中使用无名名字空间。

3.1.2  有名名字空间

有名名字空间应该像下面这样使用:

在使用包含,gflags定义/声明,以及其它的命名空间中的类的前置声明后,名字空间把它们的整个源文件包进来。(译者注:因为往往一个名字空间就是一个头文件的全部。)

// In the .h file
namespace mynamespace {
 
// 所有的声明都在名字空间的作用域内.
// 注意没有缩进编排
class MyClass {
 public:
  ...
  void Foo();
};
 
}  // namespace mynamespace
// In the .cc file
namespace mynamespace {
 
// 函数的定义在名字空间的作用域内
void MyClass::Foo() {
  ...
}
 
}  // namespace mynamespace

典型的.cc文件可能有更复杂的细节,比如要从其它的名字空间引用类。

#include "a.h"
 
DEFINE_bool(someflag, false, "dummy flag");
 
class C;  //在全局名字域中的类C的前置声明.
namespace a { class A; }  // a::A的前置声明.
 
namespace b {
 
...code for b...         // 代码从左边开始.
 
}  // namespace b

不要再std的名字空间中声明任何东西,即便是标准库类的前置声明。在名字空间stfd中声明实体是未定义行为,有可能无法移植。要从标准库中声明实体,就应包含适当的头文件。

不要用使用指令using-directive),这会使一个名字空间中的所有名字都变成可见的。

// Forbidden -- This pollutes the namespace.
using namespace foo;

可以在.cc文件的任何地方用使用声明(using-declaration),在.h文件中的函数,方法,或类的里面也可以。

// OK in .cc files.
// .h文件中,必须用在函数,方法或类的里面.
using ::foo::bar;

名字空间的别名可以在.cc文件的任何地方使用;在.h文件中,可以在有名名字空间的任何地方(这个有名名字空间必须是整个.h文件的内容),在函数和方法的任何地方。

// 简化一些在.cc文件中被普遍使用的名字.
namespace fbz = ::foo::bar::baz;
 
//简化一些在.h文件中被普遍使用的名字.
namespace librarian {
// 接下来的别名对所有包含了这个头文件的文件都是可见的(在名字空间librarian中)
// 因此在一个项目中名字空间别名应该一致的选择.
namespace pd_s = ::pipeline_diagnostics::sidetable;
 
inline void my_inline_function() {
  // 函数(或方法)局部的名字空间别名
  namespace fbz = ::foo::bar::baz;
  ...
}
}  // namespace librarian

:一个.h文件中的名字空间别名对于那些包含了这个.h文件的地方都是可见的。所以对于公共头文件(那些在工程外可见的)和那些被包含进来的头文件,应该避免使用名字空间别名,这样做是为了达到一个一般的目标使暴露出去的公共API尽可能地小。

3.2     嵌套类

虽然你可能使用到公有(public)的嵌套类,尤其是当它们是借口的一部分的时候,你仍然应该考虑使用名字空间来把这些声明放在全局作用域外(private)。

Ø  定义

在一个类的内部可以定义另一个类;这被称为成员类。

class Foo {
 
 private:
  // Bar是一个成员类,嵌套在Foo
  class Bar {
    ...
  };
 
};

Ø  正面的

这很有用,尤其是当这个嵌套(或成员)类只被这个封闭类用到;把它作为成员放在封闭类(外层的包含了嵌套类的那个类)的作用域内,而不是用它的类名污染外部的作用域。嵌套类可以在这个封闭类内被前置声明,然后在.cc文件内定义,从而避免在封闭类内包含嵌套类的定义,因为嵌套类定义一般只是实现相关的。

Ø  反面的

嵌套类可以进行前置声明,仅当是在封闭类的定义内。因此,任何头文件操作一个Foo::Bar*的指针将不得不进入Foo的整个类的声明。

Ø  结论

不要把嵌套类变成共有的,除非它们是接口的一部分,例如,一个类,它持有为一些方法而准备的一系列选项。

3.3     非成员函数,静态成员函数和全局函数

在一个名字空间内,尽量选择非成员函数或者静态成员函数,而不是全局函数;尽量不适用全局函数。

Ø  正面的

在许多情况下,非成员函数和静态成员函数会非常有用。在一个名字空间内使用非成员函数可以避免污染全局的名字空间。

Ø  反面的

非成员函数和静态函数可能作为一个新类的成员更有道理,特别是当它们访问外部的资源或有非常明显的依赖关系(译者认为这里的意思是这些资源在一个新类内部)。

Ø  结论

有时,定义一个没有绑定到一个类实例的函数是很有用的,甚至是必须的。这个函数既可以是静态成员函数,也可以是非成员函数。非成员函数最好没有依赖于外部的变量,而且应该在名字空间内一直存在。不要生成一些类只是为了把静态成员函数(这些静态成员函数没有公共静态数据)分组,这种情况应该使用名字空间。

在同一个编译单元(比如一些生产类)内定义的函数,当被从其它的编译单元内直接调用的时候,可能引入一些不必要的耦合和链接时的依赖;静态成员函数尤其如此。考虑提取一个新类,或把这些函数放入一个名字空间(也可能放入一个独立的库中)。

如果你必须定义一个非成员函数,而且它只在它的.cc文件中使用,那么就用无名名字空间,或者静态链接(比如static int Foo() {...})来限制它的作用域。

3.4     局部变量

把一个函数的变量放入尽可能小的作用域内,并且在声明时就初始化。

C++允许你在函数的任何地方声明变量。我们鼓励你把它们尽量声明为本地局部作用域内,尽量接近第一次使用它们的地方。这样更方便于读者找到它的定义,查看它的类型,以及用什么值去初始化的。特别情况下,应该使用初始化,而不是声明和赋值。比如:

int i;
i = f();      // 不好先声明,然后初始化(先调默认构造函数,然后拷贝赋值函数)

 

int j = g();  // 不错声明并且初始化 (直接调用拷贝构造函数)

注:gcc正确地实现了for (int i = 0; i < 10; ++i)i的作用域只在for循环内),因此,你可以在另一个for里面重用i,尽管这两个for在同一个作用域。它对ifwhile的声明的域也是正确的。例如:

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

有一个警告:如果变量是一个对象,那么它的构造函数在每次进入作用域的时候都会被调用到,然后对象被建立,它的析构函数在每次出了作用域的时候被调用。

// 低效率的实现:
for (int i = 0; i < 1000000; ++i) {
  Foo f;  // 构造函数和析构函数被调用了 1000000 .
  f.DoSomething(i);
}

这时候,把这个变量声明在循环外面是更有效率的。

Foo f;  // 构造函数和析构函数只被调用了一次.
for (int i = 0; i < 1000000; ++i) {
  f.DoSomething(i);
}

3.5     静态和全局变量

以类声明/定义的静态变量和全局变量是被禁止的:它们造成那些难以找到的bug,这时由于构造和析构的顺序不确定造成的。

具有静态存储周期的对象,包括全局变量,静态变量,静态类成员变量,函数的静态变量,必须是原始数据类型(POD—Plain Old Data):也就是仅限于intcharfloat,或者是POD的指针,或POD构成的数组/结构体。

对于静态变量(类对象)来说,类的构造函数,初始化被调用的顺序在C++中只有部分被定义了,而且这个顺序还可能因不同的build而异,这回造成非常难于发现的bug。因此,除了禁止全局的类类型,我们还不允许用一个函数的结果去初始化静态的原始数据类型(POD)变量,除非这个函数(比如getenv()getpid())自身不依赖于其他的全局域。

一般的情况是,析构函数被调用的顺序跟构造函数被调用的顺序是相反的。由于构造的顺序是未定义的,那么析构的顺序也同样。例如,在程序结束时,一个静态变量可能已经被销毁了,但是代码还在运行可能在另一个线程内还在试图访问它,并且失败了。或者一个静态的‘string’变量的析构函数可能在另一个变量的析构函数之前运行,而那个变量内有一个对string的引用。

最终,我们只允许含有POD数据的静态变量。这一原则不适用于vector (用的是C数组),  string (用的是 const char [])。

如果你需要一个class类型的静态或全局变量,考虑使用初始化过的指针(这个指针永远不会被释放),你可以在你的main()pthread_once()内这么做。要注意的是,这个指针必须是原始类型,不能是智能指针,因为智能指针的析构将会带来我们试图避免的析构顺序问题。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值