谷歌C++编程风格指南

谷歌C++编程风格指南

                                                               

[版本:3.180]

 

BenjyWeinberger

CraigSilverstein

GregoryEitzmann

MarkMentovai

TashanaLandray



C++ 是很多谷歌开源项目的主开发语言。正如每一个C++程序员所知,C++拥有很多强大的特性,但与此同时带也来了很大的复杂性,这就导致C++代码极易出现问题且很难阅读和维护。

本指南的目标就是根据大量经验,描述C++编码过程中建议和不提倡的编码规则,以便控制其复杂性。这些规则的运用使你在高效而创造性地使用C++的同时,又能很好是保持代码的可维护性。

风格,或者说可读性,也就是我们C++编码的惯例。使用“风格”似乎有些用词不当,毕竟惯例远远不止源文件的格式。

保持代码可维护性的一种方法就是强调编码的一致性。程序员能快速查看并理解其他程序员的代码是很重要的。维持一种统一风格并遵照惯例意味着我们可以简单地使用“模式匹配”来推断大量符号的含义和其不变性。建立通用、惯例和模式使代码更容易理解。也许有时有必要改变我们的一贯风格,尽管如此,我们还是尽量保持一致性的好。

本指南的另一个论点是C++特性臃肿。C++是一门包含有大量高级特性的巨型语言。某些情况下,我们限制甚至禁止使用某些特性。我们这样做仅仅是想使代码简单并避免由这些特性引起的大量常见错误和问题。本指南将列出这些特性并指出为什么限制它们的使用。

谷歌所有开源项目都符合本指南中的要求。

注意:本指南不是C++入门指导,我们假设读者对C++已经非常熟悉。

二、  正文

1. 头文件(HeaderFiles)

通常,每个源文件(.cc file)文件都应该有一个与之关联的头文件(.h file)文件。当然,有一些常见例外,比如只有一个main()函数的单元测试和小源文件。

正确地使用头文件可以使代码的可读性、体积和性能有一个大的提升。

通过下面的规则,你将了解大量使用头文件的缺陷。

每个头文件都应该有一个#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.2      头文件的依赖关系(Header File Dependencies)

当一个前置声明足够时,不要使用#include。

当你包含一个头文件时,便引入了头文件依赖关系,每当这个头文件改变时,代码都需要重新编译。而且,如果这些头文件还包含其他头文件,任何改变都将导致包含该头文件的代码重新编译。因此,我们提倡最小化包含,尤其是头文件包含头文件的情形。

你可以通过使用前置声明来明显地减少自定义头文件包含其他头文件的数量。比如,如果你的头文件使用File类而不需要知道File类的声明,就可以前置声明File类,而不需要使用#include “file/base/file.h”

如何在不访问其定义的情况下使用类Foo呢?

1.        声明数据成员Foo* 或者Foo&;

2.        声明以Foo为参数,和/或返回值的函数。(有一个例外:如果参数是Foo或者constFoo&,有一个隐式单参数构造函数,这种情况下我们需要引入完全定义来支持自动类型转换)。

3.        声明静态的Foo数据成员,这是因为静态数据成员在类定义外定义。

另一方面,如果你的类是Foo的子类或者包括一个Foo类型的数据成员,你必须包含该头文件。

有时使用指针成员(或使用更好的智能指针)代替对象成员更合理。然而,这会使代码的可读性复杂化并影响性能,所以,当仅以最小化包含文件数为目的时,尽量不要这么做。

通常,.cc文件需要知道其所用类的具体实现,因此需要包含一些头文件。

注意:如果你在.cc文件中使用Foo标识符,你应该自己定义Foo,要么通过一个#include命令,要么通过一个前置声明。然而有一个例外:如果在myfile.cc中使用Foo,在myfile.h中#include(或者前置声明)Foo也可以。

当函数很小(比如10行或者更少)时才定义函数为内联。

内联定义:通过内联,编译器会在调用处将函数展开为代码,而不通过通常的函数调用机制。

利:由于内联函数通常很小,所以可产生更高效的目标代码。尽量内联类成员访问和修改函数(getters and setter)和其他一些简短,对性能要求关键的函数。

弊:过量使用内联将使程序性能受损。视其规模,内联一个函数即可能使其代码量增加,也可能使其代码量减少。内联一个小的类成员访问函数(getter)通常会减小其代码量,然而内联一个很大的函数则会显著增加其代码量。现代处理器由于使用了指令缓存,在运行小规模代码时将更快。

结论:

一个好的经验法则是,当一个函数超过10行时,不要使用内联。注意析构函数,它们常常因不明显的成员或者基类析构调用比看起来的规模大。

另一个有用的经验法则是,内联一个包含有循环或者开关指令的函数常常是不划算的(除非在极特殊情况才会执行这些循环和开关指令)。

知道这一点很重要:一些函数常常不能被内联,即使被声明成这样,比如虚函数和递归函数常常不能被内联。内联一个虚函数的主要原因是将它们的定义放在类定义中,或者在类定义中说明它们的行为,比如类成员访问器和修改器。

如有必要,你可以使用-inl前缀来定义复杂的内联函数。

应该在头文件中定义内联函数,这样,编译器才能将其代码复制到调用处。然而,实现代码通常应该包含在.cc文件:头文件一般不包含实现代码,除非为了改善可读性或者出于性能的考虑。

如果一个内联函数的定义很短,包含很少(如果有的话)的逻辑判断,则应在头文件中实现它们。比如,类成员访问器和修改器应该在类定义中实现。为方便定义和调用,一些复杂的内联函数也可在头文件中定义,当这些函数使头文件变得太过臃肿时,增加一个-inl.h文件来单独定义它们。-inl.h头文件使内联函数的实现和类的定义分开,同时又允许在需要的时候包含其实现。

-inl.h的另一个用途是定义函数模板。这将使你的函数模板定义可读性更好。

另外,不要忘了,-inl.h文件也是需要#define保护的。

1.5      函数参数次序(Function Parameter Ordering)

当定义一个函数时,其参数次序应该是:输入、输出。

一个C/C++函数的参数无外乎输入、输出或者兼具两者。输入参数常常是数值或者常引用,而输出或者出入参数则是非const指针。定义参数次序时,通常将所有输入参数置于输出参数之前。尤其注意,不要简单在把新参数加在参数列表最后,要将新输入参数置于所有输出参数之前。

然而,这不是一个一成不变的规则,比如出入参数(一般是类或结构体)。

1.6      包含的命名和次序(Names and Order of includes)

为增加可读性并避免隐蔽的依赖关系,请使用标准的包含次序: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文件中是第一次被包含。

dir/foo.cc和dir2/foo2.h通常在一个目录中(比如base/basictypes_test.cc和base/basictypes.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”

2. 作用域(Scoping)

1   

2   

1   

2   

在.cc文件中,通常鼓励匿名名称空间。当需要命名一个名称空间时,可以基于项目或者它的路径。不要使用using指令。

定义:名称空间将全局作用域分成独立且命名的子作用域,因此对于防止命名冲突很有帮助。

利:名称空间提供了一条补充由类提供的分层命名的轴线。

举个例子,如果两个不同的项目有一个同名的全局类Foo,这些符号在编译或者运行时就有可能冲突。如果把它们的代码放在各自项目的名称空间中,project1:Foo和project2::Foo就是不同的符号,也不会发生冲突了。

弊:名称空间可能引起混乱,因为它提供了额外的分层命名轴线以补充由类提供的另一条。

在头文件中使用匿名名称空间很容易违反C++的一次定义规则(OneDefinition Rule, ODR)

结论:

请按下面的规则使用名称空间:

l  匿名名称空间:

1.        在C++中,匿名名称空间是允许甚至是被鼓励的:

在.cc文件中,为防止运行时命名冲突:

namespace{                 // 这通常包含在一个.cc文件中

 // 名称空间的内容不应该缩进

enum{kUnused,kEOF,kError} //常见语句

bool AtEof(){

return pos == kEOF;

}//使用我们的名称空间

EOF.

}// namespace

然尔,与特定类相关的文件作用域声明可能在类中被作为类型、静态数据成员或者静态成员函数而不是在匿名名称空间中定义。通常使用注释// namespace来结束一个匿名空间的定义。

2.        不要在头文件中使用匿名名称空间。

l 命名名称空间:

命名名称空间的使用规则如下:

1.        名称空间应该包围#include后的所有代码,包括gflags(一种调试工具,由微软发布,用于检测内存泄漏)定义、声明和来自其他名称空间的前置类声明。

 // 位于头文件

 namespace mynamespace{

 // 所有的声明都应该在名称空间作用域内

 // 注意这里没有缩进

 class MyClass{

   public:

   ...

   void Foo();

 };

 }// namespace mynamespace

 // 位于.cc文件

 Namespace mynamespace{

 // 所有函数的定义位于名称空间的作用域内

 void MyClass::Foo(){

   ...

 }

 }// namespace mynamespace

通常.cc文件可能包括更复杂的代码,比如从其他名称空间引入的类引用。

#include “a.h”

DEFINE bool(someflag, false,”dummy flag”);

class C;  // 类C的全局前置声明

namespace b{

 

... code for b ...    // 代码应该左对齐

 

}// namespace b

2.        不要在std名称空间中声明任何东西,甚至是标准库类的前置声明。在std名称空间中声明名称是未确定行为或者不可移植的。要从标准库中声明名称,包含相应的头文件即可。

3.        不可以使用using指令(using-directive)将一个名称空间中的所有名称引入。

// 禁止-这有可能引起名称空间冲突

using namespace foo;

4.        在源文件、头文件中的函数、方法或者类中可以使用using声明(using-declaration)

// 在源文件中允许

// 但在头文件中必须在函数、方法或者类中

using ::foo::bar;

5.        名称空间别名可在源文件中、头文件的全局名称空间中,或者函数和方法中任意使用。

// 源文件中常用名称的受限访问

namespace fbz = ::foo::bar::baz;

// 头文件中常用名称的受限访问

Namespace librarian{

// 下面的别名在所有包含此头文件的都可使用(限于librarian

// 名称空间)

// 因此,别名应该与项目保持一致性

namespace pd_s = ::pipeline_diagnostics::sidetable;

 

inline void my_inline_function(){

  // 位于函数或者方法内的名称空间别名

  namespace fbz = ::foo::bar::baz;

  ...

}

}// namespace librarian

注意:头文件中的别名在所有包含它的文件中都可见,所以公共头文件(其他项目可以使用)和那些间接被它们包含的头文件,应该避免定义别名,毕竟,通常的目标是尽量保持公共API简小。

2.2  类嵌套(Nested Classes)

尽管你可以使用公共的嵌套类来提供接口,但请尽量使用名称空间来保持声明的局部性。

定义:类可以在其内部定义另一个类(常称为成员类(Member Class))。

class Foo{

  private:

    // Bar是一个成员类,嵌套定义于Foo

class Bar{

  ...

}

}

利:当嵌套类仅被外部类使用时,这种定义方法很有用,这样可以避免在外部定义类引起的作用域混乱。可以前置声明嵌套类而将其实现放在源文件中来避免其定义出现在外部类的声明中,毕竟嵌套类的定义只与其实现相关。

弊:嵌套类仅可被前置声明于外部类内部。这样,任何操纵Foo::Bar*指针的头文件都必须包含Foo类的全部声明。

结论:除非嵌套类是接口的一部分(比如一个包括若干供选择方法的类),否则不要将其定义为公共成员。

2.3  外部函数、静态成员函数和全局函数(Nonmember,Static Member, and Global Functions)

尽量使用名称空间内的外部函数或者静态成员函数,尽量少使用甚至不使用全局函数。

利:一些情况下,外部和静态成员函数很有用。将一个外部函数放在一个名称空间内可以防止全局名称空间混乱。

弊:外部函数和静态成员函数作为一个新类的成员可能更好,尤其是当它们访问外部资源或者有很大的相关性时。

结论:

有时,定义一个与对象无关的函数(即外部函数或者静态成员函数)是很有用甚至必要的。外部函数不应该依赖全局变量,而且应该定义在一个名称空间内。与类不同,名称空间在定义函数的同时可以共享共变量,而定义静态成员函数的类则不允许共享其静态数据。

共同为某个类提供一项功能而在同一个编译单元内定义的函数可以导致不必要的耦合和连接时相关性,当其他编译单元直接调用时,静态成员函数尤其如此。这时,可以考虑提取出一个新类,或者将这些函数放在不同库中的不同名称空间内。

如果必须定义一个只在其源文件内使用的外部函数,可以使用匿名名称空间或者静态链接(比如static int Foo(){…})来限制其作用域。

2.4  局部变量(Local Variables)

尽量缩小函数内部变量的作用域并在定义它们时执行初始化。

虽然C++允许在函数内部任意定义变量,但我们建议尽量缩小其作用域,尽量在需要时才定义。这样方便读者找到变量声明并知道其类型及初始化情况。特别地,初始化应该代替声明和赋值。例如:

int i;

i = f();      // 糟糕-初始化和声明分开

int j = g(); // 提倡-声明的同时初始化

注意:GCC正确地实现了for循环(for(int i= 0; i < 10; i ++),i的作用域仅限于for循环体内,所以在同一作用域内,你可以重复使用i。if和while语句中的局部变量也一样。比如:

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

警告:如果变量是一个类,它的构造函数将在每次进入作用域时被调用并创建实例,它的析构函数也将在每次退出作用域时被调用。

将一个在循环内使用的变量定义在循环外将更高效:

foo f;       // 这个类的构造函数和析构函数仅被调用一次

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

f.DoSomething();

}

2.5  静态变量和全局变量(Static andGlobal Variables)

类的静态和全局使用是被禁止的:这些变量可能由于不确定的构造和析构次序而引起难于发现的缺陷。

有静态存储期间,包括全局变量、静态变量、静态类成员和静态函数变量的类必须是老式平坦数据类型(POD),包括int, char,float,或者指针,或者数组、结构体。

在C++中,类对静态成员的构造和初始化仅有部分规定,甚至每次编译都有可能不同,这很容易导致难以察觉的缺陷。因此,除了不允许定义全局类变量外,我们也不允许使用一个函数来初始化一个全局变量,除非这个函数(比如geteny(),getpid())不依赖任何其他全局变量。

同样,类的析构顺序恰恰和其构造顺序相反。由于构造顺序尚且不确定,何况析构顺序呢。举个例子,一个程序行将结束的时候,一个静态变量已经被销毁,但代码仍在运行(也许是另一个线程)并试图访问它,但失败了。或者一个静态string变量的析构可能在另一个包含有该变量的引用的变量析构中被执行。

因此,只允许仅仅包含POD数据的静态变量。显然vector(代替C数组),或者string(用const char[]实现)都不行。

如果你确实需要定义一个静态或者全局类变量,考虑从主函数或者pthread_onece()函数初始化一个指针(永远不会被销毁)。注意,这个指针一定是一个普通指针而不是智能指针,因为智能指针的析构将面临析构次序的问题。

3.    类(Classes)

类是C++代码的基本单位。自然,其使用也是广泛的。这一部分将告诉你在使用类时应该和不应该做的。

1   

2   

3   

3   

3.1  在构造函数中完成工作(Doing Workin Constructors)

通常认为构造函数仅仅完成成员变量的初始化。其他复杂的初始化工作则交给Init()函数。

定义:可以在构造函数中实现类的初始化。

利:形式简单,在使用类时不必担心类是否被初始化。

弊:在构造函数中完成初始化工作面临如下问题:

1.        由于缺少异常处理(在构造函数中不允许使用),构造函数很难发现错误;

2.        如果初始化失败,继续使用类将进入不可预知状态;

3.        如果构造函数是虚函数,则其调用不会传至子类的实现。未来对类的修改可能悄悄地引入此问题,甚至类不是其子类时也会引起混乱。

4.        如果创建全局类变量(虽然违反此指南,但仍有人这么做),构造函数将在main()执行之前被调用,这很可能打破在构造函数中的假设。譬如,gflags还未被初始化。

结论:如果初始化工作对于类很重要,考虑使用Init()方法。特别地,构造函数不应该调用虚函数,这很可能引起错误、访问未初始化的全局变量等问题。

3.2  默认构造函数(DefaultConstructor)

如果类定义了成员变量且没有其他构造函数,应定义默认构造函数,这样可以确保新建对象的内部状态一致和有效。否则,编译器将会不安全地初始化类。

定义:当创建一个类而不传入参数时,编译器便会调用默认构造函数来完成初始化。比如使用new[]运算符时总是调用这个构造器。

利:按默认方式初始化结构体,能处理非法值,简化调试工作。

弊:增加代码写书量。

结论:

即使你没有定义默认构造函数,编译器也会自动产生一个来初始化新对象,这种构造函数通常不能正确地完成初始化工作。

继承自其他类且未增加成员变量的子类不需要再定义默认构造函数。

3.3  显式构造函数(ExplicitConstructors)

关键字explicit用于仅有一个参数的构造函数。

定义:通常,只接受一个参数的构造函数可用于类型转换。比如,Foo的构造函数:Foo::Foo(string name),当向以Foo类型为参数的函数传递string参数时,将调用这个构造函数完成string到Foo的转换。有时这很方便,但有时会带来麻烦,比如这种机制在违背你本意的情况下完成类型转换并创建新的对象。声明一个构造函数为显式(explicit)可以避免这种转换。

利:避免不希望的类型转换。

弊:无。

结论:

最好在每一个只有一个参数的构造函数前用explicit进行限制。但复制构造函数例外,在一些极罕见的情况下允许转换。还有一种例外情况是,那些打算作为透明封装的类。这两种情况应该以注释注明。

在必要时才提供复制构造函数和赋值运算符。否则,使用DISALLOW_COPY_AND_ASSIGN来禁用它们。

定义:复制构造函数和赋值运算符用来创建一个对象的副本。复制构造函数在需要时被自动调用,比如以传值方式传递一个对象时。

利:复制构造函数方便对象的复制。C++标准模板库(STL)中的容器内容必须是可复制和可赋值的。复制构造函数比CopyFrom()这种替代方案更高效,因为它将构造和复制进行了结合,某些情况下,编译器会略去它,它也避免了堆分配的开销。

弊:C++对象的显式复制常会导致缺陷和引起性能问题。它也会降低代码的可读性,与引用相比,传值将使找出到底是哪个对象被来回传递变得困难,因此,找出对象在何处被修改也变得不可映射。

结论:

只有少量的类具有可复制性。大多数要么有一个复制构造函数,要么支持赋值运算符。通常,指针和引用起到复制的功能,且性能更好。比如,你可以向函数传递对象的引用或者指针而不是对象本身,你也可以在C++ STL标准容器中保存对象的指针。

如果类需要复制性,最好提供复制方法,比如CopyFrom()或者Clone()而不要使用不能被显式调用的复制构造函数。如果复制方法不满足要求(比如出去性能的要求或者类需要被保存在STL标准容器中),可以提供复制构造函数和赋值运算符。

如果你的类不需要复制构造函数或者赋值运算符,必须显式地禁用它们。将它们在类的private部分声明,且不提供任何相关定义(这样,任何试图调用都将导致连接错误)。方便起见,可以使用DISALLOW_COPY_AND_ASSIGN宏:

// 定义一个宏来禁用复制构造函数和赋值运算符

// 这两个方法的声明应该位于类声明的私有部分

#define DISALLOW_COPY_AND_ASSIGN(TypeName) \

TypeName(const TypeName&);                  \

void operator=(const TypeName&)Foo f; 

}

在Foo类中这样声明:

class Foo{

public:

Foo(int f);

~Foo();

 private:

  DISALLOW_COPY_AND_ASSIGN(Foo);

}

当对象只是用来保存数据时,则使用结构体,其他情况使用类。

在C++中,struct和class几乎是同义词。我们将给它们加上自己的语议,以便于正确地使用它们进行数据定义。

结构体应该被用来承载数据,也可包含相关的常量,除了数据成员的访问/修改方法外不提供其他方法。而且数据成员的访问/修改也是直接对其数据的访问而不通过方法调用。即使它有其他方法,这些方法也只完成数据成员的修改,比如构造器、析构函数,Initialize()、Reset()、Validate()。

如果还需要其他方法,则使用类。

为保持与STL的一致性,函数对象(functor)和类型获取器(traits)可以使用结构体。

注意:在结构体和类中,成员变量的命名方式是不同的。

组合通常比继承更合适。使用继承时,一般为公有继承。

定义:一个子类将继承基类的全部数据和操作。特别地,C++中,继承有两种主要方式:实现继承:实质性代码都被子类继承和接口继承:子类只继承接口名称。

利:实现继承通过重用基类的代码来减小程序规模。由于继承是一个编译时的声明,程序员和编译器可以理解这些操作并检测错误。接口继承则通过编程使一个类对外暴露特定的API。同样,当一个类没有定义必要的API时,编译器可以检测出错误。

弊:对于实现继承,由于子类的实现代码需要在基类和其自身展开,理解这些实现将变得困难。子类不可以覆盖非虚方法,所以它不能改变(基类的)实现。基类也可以定义一些数据成员来规定其物理布局。

结论:

所有继承应该是公有继承。如果使用私有继承,更好的方法是使用一个基类的实例作为成员。

不要滥用实现继承,类组合常常更合适。只有当Bar有充分的理由说明其is a Foo时,才能说Bar是Foo的子类

必要时使析构函数虚化。任何定义了虚函数的类,析构函数都应该被虚化。

子类需要使用的基类方法最好用protected加以限制。注意,基本的数据成员必要是private。

重写继承的虚函数时,在子类中显式地声明其为virtual。一旦漏掉了virtual,读者必须检测其所有基类来确保它是不是虚函数。

3.7  多重继承(MultipleInheritance)

多重实现继承通常罕有其用。只有当仅一个基类被实现继承,而其他类都是纯接口且以Interface作为后缀声明时才允许多重继承。

定义:多重继承允许一个类继承多个基类。请注意区别基类、纯接口和实现接口。

利:多重继承允许你更大限度地重用代码。

弊:多重继承只有一种情况才被允许:除了第一基类,其他类都是以Interface作为后缀结束的纯接口。

注意:在Windows中有一个例外。

3.8  接口(Interface)

作为接口的类可以但不必以Interface后缀结束。

定义:

满足以下条件的类被称为纯接口:

l  只有公共的纯虚函数(“=0”)和静态方法(见下文,析构函数例外);

l  只能有静态数据成员;

l  没有构造函数定义。即使有构造函数,也仅是默认构造函数且被声明为protected;

l  如果是子类,只能继承自满足以上条件且以Interface后缀结束的类;

由于内部都是纯虚函数,接口类不能被直接实例化。为使所有接口的实现都可以被正确地销毁,所有接口类必须定义一个虚析构函数(这与第一条冲突)。详细请参见Stroustup的《The C++ Programming Language》第3版的12.4章节。

利:最好给纯接口类加上Interface后缀以让其他程序员知道此类不能添加任何方法实现和非静态数据成员,这对于多重继承来说很重要。Java程序员可能更了解接口。

弊:Interface后缀使类名变得冗长而难以阅读和理解。而且,接口的特征可能被误解为其具体实现不能暴露给调用者。

结论:满足以上条件的类最好以Interface后缀结束。然而,这不是必须的。

3.9  运算符重载(OperatorOverloading)

只有在很罕见的情况下才会用到运算符重载。

定义:类可以重载诸如+/-的运算符以使其能像内建类型一样操作。

利:这些类可以像内建类型一样操作(比如int),代码看上去更直观。相比于那些呆板的命名函数(比如Equals()、Add()),重载的运算符是一种操作性的命名。为确保某些模板函数的正确性,有时必须重载运算符。

弊:运算符有很多弊端:

l  它可能使我们误以为大开销的操作(运算符重载实际上是函数调用)是小开销的内建操作;

l  找到重载运算符的调用点常常很困难。比如找到函数Equals()的调用处比找到==的简单的多。

l  一些运算符也适用于指针,这很容易造成程序缺陷。举个例子:&Foo+4和Foo+4实现的操作完全不同,但编译器不会报错。

运算符重载也有可能造成歧义。比如,如果一个类重载了单目运算符&,它不可以被安全地前置声明。

结论:

一般情况下不要重载运算符。尤其赋值运算符,通常很隐蔽。如果需要,可以定义Equals()和CopyFrom()等函数。如果一个类需要被前置声明,一定避免危险的单目运算符&的重载。

然而,在很罕见的情况下,你可能需要重载运算符来与模板和C++标准类(如<<(ostream&,const T&))互操作。合理的情况下,可以使用运算符重载,但如果可能,还是应该避免。尤其注意,不要仅仅为了类能在标准容器中作为键而重载==和<,相反,你应该创建相等和比较函数对象。

一些标准模板库算法可能需要你重载==,但必须说明原因。

参见复制构造函数和函数重写。

3.10    访问控制(Access Control)

数据成员应该被定义成私有(private)(静态常数据成员除外),需要时提供访问器(accessor)(出于技术考虑,使用Google Test时,承载测试功能的类可以将其数据成员声明成protected)。通常,名称为foo_的变量其访问函数为foo(),而其修改器(mutator)则为set_foo()。

访问器常在头文件中定义为内联函数。

参见继承和函数命名。

请按下面的规则次序来定义类:公共成员位于私有成员前;方法位于数据成员前(变量)等等。

公共部分位于保护部分前,保护部分位于私有部分前,如果某个部分空,则忽略它。

在每个部分,请按照下面的次序来声明成员:

l  类型定义(Typedefs)和枚举(Enums);

l  常量(staticconst数据成员);

l  构造函数;

l  析构函数;

l  类方法,包括静态方法

l  数据成员(静态常量除外);

友元声明和DISALLOW_COPY_AND_ASSIGN宏调用应该在私有部分。私有部分应该在类定义的最后部分。参见复制构造函数

在相关源文件中,方法的实现次序也应尽量与类声明中一致。

不要将把大函数内联定义在类定义中。通常,只有很短且性能要求高的情况下才将一个函数定义成内联。参见内联函数

3.12    定义简短函数(Write Short Functions)

函数应该尽量简短并功能单一。

不得不承认,某些场合长函数很合适,所以不太容易去限制其长度。如果一个函数超过40行,考虑可否在不改变程序结构的情况下将其拆分。

尽管长函数目前工作良好,也许其他程序员日后会修改并给它添加新功能。这会导致难以发现的缺陷。保持你的函数简短可以方便其他程序员阅读和修改你的代码。

阅读一些代码时,你可能发现长函数。不过,不要害怕修改它们。如果发现使用它们很困难或者调试有难度,或者你想在多处使用部分函数代码,考虑把它拆分成更易管理的片段。

4.    谷歌经验技巧(Google-Specific Magic)

谷歌采用很多技巧和工具来确保C++代码的健壮性,而且,谷歌使用C++的方式和其他地方很不同。

1      

2      

3      

4      

4.1      智能指针(Smart Pointers)

如果使用指针,最好使用受限指针(soped_ptr)。而std::tr1::shared_prt只有在很少的情况下都会用得到,比如对象需要被标准模板库容器包含。任何情况下不要使用自动指针(auto_ptr)。

智能指针是指行为类似于指针但增加额外功能的对象。当一个scoped_ptr被销毁时,它也将删除其指向的对象删除。shared_prt也具有类似功能,但它会实现引用计数直到它指向的最后一个对象删除它。

通俗点说,我们希望定义清楚每个对象的归属。但最清楚的对象归属是此对象被域或者局部变量拥有,而不是使用指针。另一个极端是,在它们被定义时,引用计数指针不被任何对象拥有。这种定义的问题是,这样将很容易循环引用或者对象无法被销毁的奇怪现象。而每个原子操作都进行复制或赋值将影响性能。

即使不提倡,引用计数指针有时却是最简单和幽雅的问题解决方式。

4.2      CPPlint

使用cpplint.py来检测风格错误。

Cpplint.py是一个能读取源文件并识别风格错误的工具。尽管不很完美,有很多优点和缺点,但它仍是一个有用的工具。主动错误信息可以将// NOLINT放在行后来忽略。

有些项目带有如何从项目工具运行cpplint.py的说明。如果没有,你可以单独下载它。

5.    其他C++特性(OtherC++ Fetures)

1      

2      

3      

4      

5      

5.1      引用参数(Reference Arguments)

所有用引用传值的变量应该被const修饰。

定义:在C语言中,如果函数需要修改一个变量,必须使用指针作为其参数。比如int foo(int*pval)。但在C++中,有了另一种方式,即引用:int foo(int &val)。

利:把一个参数定义为引用可以避免丑陋的代码(比如*pval++)。有些程序需要,比如复制构造函数。使程序更明确,不像指针能取得NULL值。

弊:由于引用兼具值表达式和指针的主义,会引起迷惑。

结论:

所有函数引用参数都应该定义为const引用。

void Foo(const string &in, string *out)

实际上,将值或者常引用作为输入参数而将指针作为输出参数是谷歌的一个惯例。输入参数也可以是常指针,但不允许非const(non-const)引用。将常指针作为输入参数的一种情况是,你想强调这个参数将不被复制,它在对象的整个生命周期内必须存在,但最好在注释中说明。标准模板库的适配器(比如bind2nd和mem_fun)不允许引用参数,这时只有用指针了。

5.2      函数重载(Function Overloading)

看到一个函数的调用立即能知道其操作而不是需要首先找出是哪个重载版本被调用了时,才使用重载函数。

定义:

利用重载,你可以定义接收不同参数的同名函数,比如接收const string&和const char*的同名函数。

class MyClass{

  Public:

void Analyze(const string &text);

void Analyze(const char *text,size_t textlen);

};

利:重载可以使代码更直观。对于模板化的代码,重载可能是必须的;对于访问控制器(Visitor)的实现,重载也是很方便的。

弊:如果函数仅以参数类型不同来重载,读者可能需要深入理解C++复杂的参数匹配规则才能知道是怎么回事。在继承中,子类只重写基类函数的某些版本也会引起迷惑。

结论:要想重载函数,考虑根据其参数来命名函数。比如AppendString()、AppendInt()就比Append()好。

5.3      默认参数(Default Arguments)

除非在以下情况下,不允许使用函数默认参数。

利:你经常会写带有很多默认值的函数,但有时又不得不重载这些默认值。默认参数提供了实现它的简单方法,且不用为了少量例外而定义大量函数。

弊:程序员常常通过查看已有代码来找出调用一个API的方法。默认参数将变得更难维护,因为从其他地方复制-粘贴代码,默认参数可能未被显示。当默认参数不适用于新代码时,复制-粘贴部分将引起问题。

结论:

除了以下情况,函数必须明确定义每个参数来强制程序员在调用API时考虑传入参数值,而不是简单地接受默认参数。

一种特殊的例外是当默认参数是用来模拟可变长参数时:

// 通过默认参数,最多支持4个参数

string StrCat(const AlphaNum &a,

                 const AlphaNum &b = gEmptyAlphaNum,

                 const AlphaNum &c = gEmptyAlphaNum,

                 const AlphaNum &d = gEmptyAlphaNum);

5.4      可变长度数组和内存申请(Variable-Length Arrays and alloca())

不允许变长数组和内存申请。

利:可变长度数组语句自然,且与alloca()一样,很高效。

弊:可变长度数组和alloc()不是C++标准的一部分。重要的是,它们根据程序栈容量来申请空间,这可能引起内存覆盖缺陷。“在我的机器上一切正常,但做成产品后却神秘死机。”

结论:使用更安全的内存申请函数,比如scoped_ptr/scoped_array。

5.5      友元(Friends)

适度使用友元类和友元函数是允许的。

友元应该和其友类定义在同一个文件中,这样,读者不必再去另一个文件中查看友元使用了该类中的哪些私有成员。友元的一个常见作用是在不暴露一个类的内部细节时利用友元类来正确地构造其内部状态。比如FooBuilder和Foo。有时,将一个单元测试类定义为其测试类的友元会很有用。

友元仅仅是扩展而不是打破类的封装性。当一个类需要访问另一个类的私有成员时,友元比将这个成员公有化更好。然而,类与类的协作只能通过公共成员。

5.6      异常处理(Excpetions)

通常不使用C++的异常处理。

利:

l  异常处理使在程序的更高层次来处理多层函数嵌套调用的“不可发生”错误成为可能,而且不需要使用隐蔽且容易出错的错误代码簿记;

l  异常处理被大多数其他现代程序设计语言采用。在C++中使用异常处理将使其与其他语言如Python、Java保持一致性。

l  一些第三方库使用异常处理,如果关闭,可能导致很验证使用这些API。

l  异常是唯一能导致构造失败的方式。尽管可以使用工厂函数或者Init()方法来实现构造,但它们分别需要堆申请和“无效”状态。

l  异常处理常被用于架构测试。

弊:

l  当给一个函数加上抛出(throw)语句时,必须检查其调用链。它们要么进行基本的异常处理,要么忽略异常且无视程序由此而终止运行。举个例子,如果f()调用g(),g()又调用f(),h抛出一个异常,f捕捉到了这个异常,那么g必须注意在异常发生时的清理工作。

l  更一般地,异常处理使程序很难从其代码中看出其控制流:程序可能从意想不到的地方返回。这将使维护和调试变得困难重重。你可以通过使用一些异常处理规则来减小这些开销,但一定比开发人员需要了解和理解的多。

l  异常安全性需要资源获取即初始化(RAII)和不同的编码实践的支持。为简化正确开发异常安全代码的工作,也需要很多支持机制。进一步,为避免陷入寻找完整函数调用链的麻烦,异常安全代码必须把用于将状态持久化为“提交”阶段的逻辑进行隔离。这既有好处又有开销(也许在你被迫隐藏代码来隔离提交的地方)。允许异常处理,在不值得的情况下也得付出这些代价。

l  使用异常处理将增加目标代码量,增加编译时间(通常不明显)并可能增大地址空间的压力。

l  异常的可用性可能使开发者在不合时宜时抛出异常或者从异常状态恢复并不安全时使用。比如,无效的用户输入不应该导致异常抛出。

结论:

表面上,使用异常处理的好处比开销多,尤其是新项目。然而,如果为已有代码引入异常处理,将会引起所有相关代码的变动。如果异常在新项目外可被抛出,则其与未进行异常处理的旧项目的互操作将变得困难重重。由于大多数谷歌的C++项目都不准备采用异常处理机制,因而将很难使用产生异常的新代码。

考虑到谷歌已有代码未进行异常处理,异常处理的机制引入的花销将远大于新项目。而且,转换过程也将缓慢而且容易出错。再者,异常处理的替代方式,比如错误处理和断言也不会增加很多编程负担。

我们并不是站在哲学或者道德的立场反对使用异常处理机制,而是站在实用的立场上。因为我们需要使用谷歌开源项目,而且对这些项目引入异常处理很困难,我们不得不在谷歌开源项目中建议不要采用异常处理。如果从头开发这些项目将困难重重。

对于Windows代码来说例外。

5.7      运行时类型信息(Run-Time Type Information, RTTI)

不建议使用RTTI。

定义:RTTI允许程序员在运行时查看一个对象的类类型。

利:

在单元测试时会有用,比如进行工厂类测试时,必须证实一个新创建对象是否是应有动态类型。测试之外罕见其用。

弊:运行时检查类型通常意味着类设计有问题。

结论:

除了单元测试,不要使用RTTI。确实需要基于对象类型来完成不同的功能时,考虑替代方案。虚方法是使子类执行不同代码的首选方案。这将使对象自己来完成自己特定的工作。如果这些工作位于项目的一些处理代码中,考虑双分派(Double-Dispatch),比如访问器设计模式(Visitor Design Pattern)。这允许对象外的设施可以利用系统内建类型来决定一个类的类型。如果你不赞成这些观点,你可以使用RTTI。但请三思J再三思。不要手动实现类似RTTI的变通方案。反对RTTI及其变通方案应用的观点就和反对用类型标签对类分层一样多。

5.8      类型转换(Casting)

在C++中,需要类型转换时请使用static_cast<>(),不要使用诸如int y = (int)x和int y =int(x)的其他形式。

定义:C++提供了一种与C不同的因类型而异的类型转换操作。

利:C语言的问题是转换操作的二义性:有时是转换(比如(int)3.5),而有时却是cast(比如(int)”hello”))。C++的类型转换则不存在这个问题,而且,C++的类型转换是显式的和可追踪的。

弊:语句繁琐。

结论:

l  使用static_cast进行C风格的值转换或者将子类指针提升为基本指针;

l  使用const_cast去掉const修饰(参见const);

l  使用reinterpret_cast进行不安全的指针类型转换(比如转换到或者从整型指针和其他类型指针)。当你确定你的做法及其引用的问题时才进行这种操作;

l  除了在测试中,不要用dynamic_cast。如果在单元测试之外需要使用这种方法来测试一个类的类型,通常意味着你的设计有问题。

5.9      流(Streams)

流只用于日志记录。

定义:流是C中printf()和scanf()的替代实现。

利:使用流,不需要关心输出对象的类型,也不需要像C那样定义一大串格式符了。流的构造和析构函数会自动打开和关闭相关文件。

弊:流不利于随机读取。一些格式(尤其字符串格式习惯用法:%.*s)也不像使用类似printf技巧那样方便。流也不支持有利于国际化的运算符重定序(比如%1s指令)。

结论:

除非需要日志接口,否则不要使用流。使用类似printf的例程。关于流的争论很多,但正如我们一致强调的,不要使用流。

扩展讨论:

关于这些问题的争论此起彼伏,所以这里我们深入讨论。重申一下唯一指南原则:我们希望确保每次对于特定类型的I/O的代码都是一样的。为此,我们不允许使用者在使用流还是printf(加上读/写/等等)之间进行选择,我们的做法是确定一个(即printf)。之所以日志例外,是出于历史原因考虑的,日志是一个很特殊的程序。

流的支持者认为流是明智的选择,但并不是这样。他们指出的每一个优点,都有相应的缺点。最大的优点是,你根本不需要知道输出对象的类型。但仔细想想,你可能使用了错误类型,而编译器不会警报。在使用流时,很容易犯这种错误:

cout<<this;           //打印地址

cout<<*this;          //打印指针内容

由于运算符<<被重载,编译器将不会报错。这也是我们不提倡重载的原因。

一些人说printf格式繁琐不易阅读,但流也好不到哪里。

看看下面的两段代码,处理同一类型的数据。哪一个更简单呢?

cerr<<”Error connecting to ‘”<<foo->bar()->hostname.first

     <<”:”<<foo->bar->hostname.second<<”:”<<strerror(errno);

Fprintf(stderr,”Error connecting to ‘%s:%u:%s”,

          foo->bar()->hostname.first,foo->bar()->hostname.second,

          strerror(errno));

等等你可能发现的其他问题。(你也许会争论,正确地封装更好。但如果它对一个模式正确,对另一个呢?再者,我们的目标是尽量简化一种语言,而不是增加更多需要学习的机制。)

关于两者优缺点的争论还在继续,也找不到一种更合理的解决方案。尽管我们简单地规定了选择它们中的一种,但还是使用printf+read/write的居多。

5.10   前置自增和前置自减(Preincrement and Predecrement)

在操作迭代器和模板对象的时候使用前置自增和前置自减。

定义:当一个变量自增(++i或i--)(自减(--i或i--)且不需要使用其值时,必须考虑是前置还是后置++/--。

利:不考虑返回值时,前置性能总是优于后置。这是因为后置需要复制i的值。如果i是一个迭代器或者非标量类型,复制i将是很大的开销。既然两者的作用相同,为何不选择前置运算呢。

弊:在C传统开发中,常使用后置方式,尤其在for循环中。有些人发现后置运算更易读,与英语语法一样,后置运算的对象(i)在动词(++)之前。

当需要的时候尽量使用const修饰。

定义:使用const来限制变量或者参数不被修改(比如const int foo)。类方法也可用const修饰以说明其不会改变类的状态。(比如class Foo{(int Bar(char c) const;))

利:有利于程序员理解变量的使用方式。编译器能进行更严格的类型检查,而且常常能产生更高效的代码。说服程序员相信其程序的正确性,毕竟他们知道其调用的函数对这些变量的修改是有限制的。这可以帮助程序员知道在多线程编程中哪些函数不需要锁也能正确地运行。

弊:const是传递的,如果你给函数传递const变量,那么这个函数在声明时必须加上const声明(否则变量需要const_cast)。在使用库函数时,这将是一个问题。

结论:

const变量、数据成员、方法和参数使编译器进行编译时类型检查,因此能尽早发现程序错误。以下情况强烈建议使用const修饰:

l  函数不修改通过引用或者指针传递的参数值,应该使用const修饰;

l  类方法应该尽量定义成const。访问器(Accessor)应该永远是const的。其他不修改类数据成员、不调用非const方法不返回非const引用或指针的函数也应该是const的;

l  把初始化后不需要修改的数据成员定义成const。

但不要滥用const。像const int* const * const x就是滥用,即使你能准确地解释清楚const x是什么意思。注意什么是真正有用的,这种情况下,const int ** x就足够了。

当进行多线程编程时,mutable关键字可能不安全,所以在使用时应该首先考虑线程安全。

应该将const放在什么位置

某些人更喜欢将const放在类型后(int const *foo而不是const int* foo)。他们认为这样更易读和一致,const修饰的对象总是紧跟在它后面。然而,这种一致性在这里却不适用,因为“不要太过分”宣言排除大多数你认为的一致性用法。将const放在前面更具可读性,因为它与英语语法一样,把形容词(const)放在名词(int)前面。这并不是说我们鼓励你将const放在最前面你就一定要这么做,注意与你的代码保持一致性。

5.12   整型类型(Integer Types)

所有C++整型中,唯一可能用到就是int。当需要其他长度的整型时,使用stdint.h头文件中的精确整型,比如int16_t。

定义:C++未明确定义整型的长度。人们通常的假设是:short:16位,int:32位,long:32位,long long:64位。

利:一致的声明;

弊:整型的长度因编译器和计算机结构不同而不同。

结论:

在stdint.h头文件中定义了int16_t, uint32_t,int64_t等整型,用到整型时,应该首选这些精确的类型。对于C的整型,只可以使用int。适当的时候,你还应该使用其他标准类型,比如size_t和ptrdiff_t。

对于已知不会太大的整数,可以使用int,比如循环变量。但应该考虑int至少是32位的,但不要认为int会超过32位。当需要64位整型时,使用int64_t或者uint64_t。如果已知整数会比较大,使用int64_t。

不要使用无符号的uint32_t,除非你确切地知道要存储是一个位组而不是一个数字,或者你定义二进制补码溢出。特别地,不要用无符号类型来说明一个数是非负的,用断言来说明。

关于无符号整型

一些人,包括教科书的作者在内,推荐使用无符号类型来代表非负数值。这被认为是一种自文档(即代码本身就能说明其含义而不需要文档(比如注释)来说明:unsigned int book_amount;就是一种自文档)。然而,C中,这种自文档的优点却无法掩盖其带来的程序缺陷:考虑下面的代码:

for(unsigned int i = foo.Length()-1; i >= 0; --i) ...

这是一个死循环!GCC可能会提醒这个缺陷并给出警告,但大多数情况下会被忽略。相似的严重缺陷在进行有符号和无符号变量比较时也会出现。根本原因是C语言的类型提升机制使无符号类型的行为变得无法预期。所以,使用断言来说明一个变量的非负性。不要使用无符号类型。

5.13   64位兼容性(64-bitPortability)

程序代码应该保持32位和64位的兼容性。考虑打印(Printing)、比较(Comparisons)和数据对齐(Structurealignment):

l  对于某些类型,指示符printf()不能很好在32和64位系统间兼容。C99定义了一些格式兼容的指示符。但MCVC7.1对某些不支持,所以某些情况下,只有自己基于inttypes.h的风格来自己定义自己比较丑陋的版本了。

// size_t的输出宏,基于inttypes.h的风格

#ifdef _LP64

#define __PRIS_PREFIX “z”

#else

#define __PRIS_PREFIX

#endif

 

//在printf格式字符串中的%后使用这些宏来确保正确的32/64位行为

//用法如下:

// size_t size = records.size();

// printf(“%”PRIus”\n”,size);

 

#define PRIdS __PRIS_PREFIX “d”

#define PRIoS __PRIS_PREFIX “o”

#define PRIuS __PRIS_PREFIX “u”

#define PRIxS __PRIS_PREFIX “x”

#define PRIXS __PRIS_PREFIX “X”

类型

不应该使用

应该使用

说明

void *(或任何指针)

%lx

%p

 

int64_t

%qd,%lld

%”PRId64”

 

uint64_t

%qu,%llu,%llx

%”PRIu64”,%”PRIx64”

 

size_t

%u

%”PRIuS”,%”PRIxS”

C99指定%zu

ptrdiff_t

%d

%”PRIdS”

C99指定%zd

注意PRI*宏作为独立字符串的扩展由编译器进行连接。因为,在使用非常量字符串时,需要将宏体插入格式说明中而不是命名它。将长度指示符加在%后也是允许的。比如printf(“x = %30”PRIuS”\n”,x),在32位Linux上展开时为printf(“x = %32”u””\n”,x),编译器识别为printf(“x = %30u\n”, x)。

l  注意,sizeof(void *) != sizeof(int)。如果需要指针大小的整数,使用intptr_t;

l  注意数据尤其是保存在磁盘上的数据的对齐问题。任何64位系统的类或结构体,如果有int64_t或uint64_t类型的数据成员,默认情况下,会以8字节结尾来保持数据对齐。在32和64位兼容代码中,如果有这种结构,应该确保它们在两种体系下被一致地封装。大多数编译器提供了数据对齐转换功能。比如gcc,使用命令__attribute__((packed))。MSVC则提供了#pragma pack()和__declspec(align())。

l  声明64位常量时使用LL或ULL后缀:

int64_t    my_value = 0x123456789LL;

uint_64_t  my_mask = 3ULL << 48;

l  如果确实需要在32和64位系统上开发不同的代码,可以#ifdef _LP64命令来在两种环境中切换。但尽量避免这样做并保持修改局部化。

宏的使用应倍加小心。尽量用内联函数、枚举和常量来代替宏。

宏意味着编译器对代码的理解和你很不同。这可能导致不可预期的行为,尤其因为宏是全局作用的。

还好,C++对宏的需求不像C那样必要。可以使用内联函数来代替由宏定义的性能要求高的代码;使用const常量来代替由宏定义的常量;使用引用来代替由宏定义的长变量名的缩写;除包含保护外,忽略所有由宏定义的条件编译指令,这些指令使代码难以测试。

当然,宏具有其他技术做不到的特殊应用。这些在底层的库定义和代码库中较常见。而且,它们具有一些语言本身都无法完全实现的特征(比如分词(stringifying)、连接(Concatenation)等)。但使用宏前,考虑一下有没有不使用宏也能达到目的的方法。

下面的用法可以避免大量使用宏引起的问题,如果使用宏,请严格遵守它们:

l  不要在头文件中定义宏;

l  使用宏前定义(#define)它们,使用后立即解定义(#undef);

l  不要解定义一个宏后又接着使用它定义另一个宏,选择一个不同的唯一的宏名称;

l  不要使用导致不平衡C++构造的宏,如果使用,应该详细这种行为的正确性;

l  不要使用#作为函数、类或变量名称的开始

5.15   0和空(0 and NULL)

整型用0、实数用0.0、指针用NULL、字符用’\0’。对于整型和实数这是毫无争议的。

但是对于指针(地址值),有两个选择:0和NULL。BjarneStroustrup建议使用无修饰的0,但我们建议使用NULL,它看上去更像一个指针。事实上,一些C++编译器,比如gcc4.1.0提供了特殊的NULL定义以使其能够检测出错误并给出有用的警告,此时,sizeof(NULL)和sizeof(0)是不等的。

用’\0’代表空字符则使代码更易读。

5.16   存储容量运算符(sizeof)

尽量使用sizeof(varname)而不是sizeof(Type)来确定一个变量或类型的大小。之所以使用sizeof(varname)是因为一旦变量的类型改变,仍可正确计算其存储容量。sizeof(type)只在某些情况有用,应该避免它的使用,在变量发生变化时,它可能不再与变量同步。

Struct data;

memset(&data,0,sizeof(data));

memset(&data,0,sizeof(Struct));

5.17   增强库(Boost)

从增强库中选择那些被广泛认可的库来使用。

定义:Boost是一个流行的,同行评论的、免费开源的C++库系列。

利:Boost代码非常高效,兼容性强并且填补了C++标准库的许多空白。比如类型萃取、改进的函数绑定和改进的智能指针。同时,它还提供了标准库的扩展库-TR1的实现。

弊:一些Boost库可能鼓励可读性差的编程实践,比如元编程和其他高级模板技术以及过度功能化的编程风格。

结论:

为保持高度的可读性,只有被认可的部分Boost特性允许使用。目前,仅限以下库:

l  boost/call_straits.hpp中的Call Traits;

l  boost/compressed_pair.hpp中的CompressedPair;

l  除C++03标准中未定义的容器持久化和封装工具(ptr_circlar_buffer.hpp和ptr_unordered*)外的boost/prt_container中的PointerContainer;

l  boost/array.hpp中的Array;

l  boost/graph中除持久化(adj_list_serialize.hpp)和并发、分布式算法和数据结构(boost/graph/parallel/*和boost/graph/distributed/*)。

l  boost/property_map中除并发、分布式(boost/property_map/parallel/*)的属性映射;

l  迭代器部分用于处理迭代器定义的部分:boost/iterator/iterator_adaptor.hpp,boost/iterator/iterator_facade.hpp,boost/function_output_iterator.hpp

我们经常考虑增加一些Boost特性,所以这些规则可能会改变。

5.18   C++ 0x

使用已认可C++0x库和语言扩展,目前还没有被认可的。

定义:C++0x是下一个ISO的C++标准,目前正处于最终委员会草案阶段。包括对C++语言和库的重要变化。

利:我们希望C++0x成为下一个C++标准并最终被大多数编译器支持。它将一些目前广泛使用的C++扩展库(第三方)纳入标准,为某些操作提供了简化方法并在安全方面作了改进。

弊:C++0x标准比它之前的标准都要复杂(1,300页,之前的只有800页),并且,很多开发者对它并不熟悉。它的一些特性对代码可读性和可维护性的长期影响还不得而知。我们并不能预测什么时候它的众多特性才能被众多工具统一地实现,但这将很有趣(gcc,icc,clang,Eclipse等)。

与Boost一样,一些C++0x扩展鼓励降低代码可读性的编程实践-比如移除检测到的冗余(比如类型名),而这些冗余对读者很有帮助,或者鼓励模板元编程。另外一些扩展提供与现在机制复杂的功能,这有可能导致混乱并带来转换开销。

结论:与Boost一样,使用那些被认可的语言扩展和库。虽然目前还没有被认可的库,但随着标准的确立,新特征会被逐渐认识的。

6. 命名(Naming)

管理命名是最重要的保持一致性的规则。通过名称的风格,我们不用看其声明就可以立即猜出命名实体是什么:一个类型、一个变量、一个函数、一个常量或者一个宏等。我们大脑的模式匹配机制在处理识别这些命名规则时起了很大的作用。

命名规则非常随意,但我们觉得在编程领域,一致性比个人表现更为重要,所以,无论是否合理,规则就是规则。

1   

2   

3   

4   

5   

6   

6.1  一般命名规则(General NamingRules)

函数名称、变量名称和文件名称应该具有描述性并且避免简写。类型名和变量名应该是名称,而函数名则应该是命令动词。

怎样命名(Howto Name)

在合理的情况下,尽量给出描述性的命名。不要考虑水平空间,毕竟让你的代码能立即被新读者明白要重要得多。比较好的命名:

int num_erros;                         // 很好

int num_completed_connections;      // 很好

而下面的命名则很不好,要么使用不明确的缩写、要么用无法传达意义的任意字符:

int n;                                   // 不好-无法表达变量意义

int nerr;                               // 不好-不明确的缩写

int n_comp_conns;                      // 不好-不明确的缩写

类型名和变量名通常应该是名词,比如FileOpener_num_errors。

函数名通常应该具有祈使性(也就是说它们应该是命令),比如OpenFile(),set_num_errors()。访问器是一个例,它应该基于其访问的成员来命名,这一点在函数命名中有详细描述。

缩写(Abbreviations)

不要使用缩写,除非这些缩写在项目外被广泛接受。比如:

//很好-这些名称没有缩写

int num_dns_connections;      // 大多数人知道DNS代表什么

int price_count_reader;       // 很好-价格计数。名称有意义

//糟糕-缩写在小组外可能造成混乱和歧义

int wgc_connections;           // 只有你小组成员知道这是什么意思

int pc_reader;                  // pc这个缩写有很多含义

不要以省略字符的方式缩写:

int error_count;       // 很好

int error_cnt;          // 糟糕

6.2  文件命名(File Names)

文件名应该全部小写,可以含有下划线(_)和连字符(-)。对于连接符号(_或-)的选择,可以遵照项目的约定,如果没有统一要求,最好选择下划线(_)。

好文件名示例:

my_useful_class.cc

my-useful-class.cc

myusefulclass.cc

myusefulclass_test.cc  //不再使用_unittest和_regtest

C++源文件应该以.cc作为扩展名而头文件则以.h作为扩展名。

不要与/usr/include中的文件重名,比如db.h。通常,应该使你的文件名尽量具体。比如用http_server_logs.h就比logs.h好得多。一种很常见的情况是一对文件foo_bar.h和foo_bar.cc定义一个叫作FooBar的类。

内联函数必须在头文件中定义。如果头文件很短,可以直接放在头文件中。然而,如果内联函数包含很多代码,它们应该在第三个以-inl.h结尾的文件中定义。如果类有很多内联代码,你的类应该有3个文件:

url_table.h             // 类声明

url_table.cc            // 类定义

url_table_inl.h        // 包括大量代码的内联函数

另外,参见内联头文件(-inl.hFiles)。

6.3  类型命名(Type Names)

类型名以大写字母开头,每个单词的首字母大写,中间没有下划线。所有类型——类、结构体、类型定义(typedef)和枚举——都符合统一的命名约定。比如

// 类和结构体

class UrlTable{...

class UrlTableTeser{...

struct UrlTableProperties{...

// 类型定义

Typedef hash_map<UrlTableProperties*, string> PropertiesMap

// 枚举

Enum UrlTableErros{...

6.4  变量命名(Variable Names)

变量名所有字母小写,单词之间用下划线(_)分开,类成员变量以下划线结束。

普通变量命名(CommonVariable Names)

比如:

string table_name;             // 可以-使用下划线

string tablename;              // 可以-全部字母小写

string tableName;              // 糟糕-大小写混合

类数据组成变量命名(Class Data Members)

数据成员(又叫实例变量或者成员变量)的命名与普通变量一样,全部字母小写,可选的下划线分隔符,但应该以下划线结束。

string table_name_;           // 可以-以下划线结束

string tablename_;             // 可以

结构体成员变量命名(Struct Variables)

结构体成员变量和普通变量命名规则一致,且不像类成员变量以下划线结束。

struct UrlTableProperties{

  string name;

  int num_entries;

}

参见结构体与类部分的关于它们使用的讨论。

全局变量命名(GlobalVariables)

全局变量的使用较为罕见,但当用到时,考虑以前缀g_开头或标以其他记号,以便与局部变量区分。

6.5  常量命名(Constant Names)

K后跟混合大小写的名称:kDaysInAWeek。所有编译时常量,不管是被声明为局部、全局还是作为类的成员,都应该遵守与其他变量命名有轻微差别的命名约定:k后跟单词首字母大写的名称:

const int kDaysInAWeek = 7;

6.6  函数命名(Function Names):

正规函数有很多复杂情况:访问器和修改器以变量名命名:MyExcitingFunction(),MyExcitingMethod(),my_exciting_member_variable(),set_my_exciting_member_variable()。

正规函数命名(RegularFunctions)

正规函数名应该以大写字母开头,单词首字母大写,不使用下划线。如果函数可能因错误而崩溃,应该在函数名后加上OrDie。这仅适用于那些被产品代码调用或者正常操作有可能引起错误的函数。

AddTableEntry()

DeleteUrl()

OpenFileOrDie()

访问器和修改器(Accessorsand Mutators)

访问器和修改器(get和set函数)应该与它们关联的变量名匹配。下面显示了一个类的部分摘录,它有一个实例变量num_entries

class MyClass{

  public:

...

int num_entries() const{return num_entries;}

void set_num_entries(int num_entries){num_entries = num_entries;}

private:

  int num_entries_;

};

你也可以使用小写字母和下划线来命名非常短小的内联函数。比如,如果一个函数的调用开销很小,在循环调用时,没必要缓存其值,这时,小写字母命名是允许的。

6.7  名称空间的命名(NamespaceNames)

名称空间以小写字母命名,名称基于项目名称和项目的目录结构:google_awesome_project。

参见名称空间部分关于名称空间及其命名的讨论。

6.8  枚举器的命名(EnumeratorNames)

枚举器要么像常量一样,要么像宏一样命名:或者kEnumName,或者ENUM_NAME。通常,枚举变量按常量命名方式命名更好。然而,按宏命名方式亦未尝不可。下面的例子中,枚举的命名是一个类型,因此是一个混合情况:

enum UrlTableErros{

  kOK = 0;

  kErrorOutOfMemory,

  kErrorMalformedInput,

];

enum AlternateUrlTableErrors{

  OK = 0;

  OUT_OF_MEMORY = 1;

  MALFORMED_INPUT = 2,

};

直到2009年1月,枚举的命名方式都依据宏。这在枚举和宏之间引起了名称冲突。因此才将其改为常量命名方式。新代码应该遵照这一约定。然而,旧代码没必要修改,除非这会引起编译时问题。

6.9  宏命名(Macro Names)

你真的打算定义宏吗?如果的确如此,这样命名:MY_MACRO_THAT_SCARES_SMALL_CHILDREN。参见宏描述(Descriptionof macros)。

#define ROUND(x) ...

#define PI_ROUNDED 3.0

6.10    命名规则的例外情况(Exceptions to Naming Rules)

如果你给与已有C或C++实体相似的实体命名,可以遵照已有命名约定模式。

bigopen()           函数名称,继承自open()

uint                 类型定义

bigpos               结构体或类,根据pos

sparse_hash_map    类标准模板库实体,根据STL命名约定

LONGLONG_MAX        一个常量,类似INT_MAX

7. 注释(Comments)

尽管书写注释很痛苦,但对于代码的可读性,注释至关重要。下面的规则指导在哪里和怎样注释。但记住,尽管注释很重要,最好的代码应该是自文档的。给类型和变量命以有意义的名称,比命以模糊名称而又通过注释进行阐释要好得多。

书写注释时,考虑你的继任者:下一个需要读懂你代码的开发人员(通常还是你自己)。

1      

2      

3      

4      

5      

6      

7      

7.1      注释风格(Comment Style)

使用//或者/* */,直到形成自己的风格。通常,//更常用,但与自己的风格保持一致。

7.2      文件注释(File Comments)

每个文件都应该提供版权信息,然后是文件内容的综合性描述。

合法公告和作者信息行(Legal Notice and Author Line)

每个文件都应依次包括以下条目,

l  版权声明(比如Copyright2008 Google Inc.);

l  一个许可引用。选择适合你项目使用的许可引用(比如Apache 2.0、BSD、LGPL、GPL)

l  作者信息行说明文件原始作者

如果你对原始作者的文件做了实质性修改,可以在作者信息行加上你的名字。当其他开发者有问题时,这样可以方便他们正确地联系到修改者。

文件内容注释(FileContents)

每个文件都应该在其版权信息及作者信息后面和内容前面有一个内容描述性的注释。

通常,头文件描述它所声明的类的目的及用法。而源文件则应该包含更多有关实现和技巧性算法的讨论信息。但如果你觉得这些信息对于头文件的阅读者更有用,可以将其放在头文件中,但在源文件中应该注明其文档在头文件中。不要在头文件和源文件中重复注释,这样容易造成歧义。

7.3      类注释(Class Comments)

每个类定义都应该伴随有说明其目的和用法的注释。

// 遍历GargantuanTable的内容。用法示例:

// GargantuanTableIterator* iter  = table->NewIterator();

// for(iter->seek(“foo”);!iter->done();iter->next()){

//   process(iter->key(),iter->value();

//  }

// delete iter;

Class GargantuanTableIterator{

...

如果你在文件开始就已对类进行了详细描述,可以在类实现部分简单地声明“参见文件开始注释部分的完整描述”,但注意,这里还是要添加少量注释。

7.4      函数注释(Function Comments)

函数声明部分的注释描述函数的用法,实现部分的注释描述函数实现的操作。

函数声明注释(Functiondeclaration):

每个函数声明的前面都应该有一个描述函数功能和用法的注释。这些注释应该是描述性(Opens the file),而不是祈使性的(Open the file),注释仅仅描述函数能够完成什么功能而不是函数是怎么实现的,这些应该在函数实现的注释中。

在函数声明注释中应该提到的信息类型:

l  输入和输出;

l  对于类成员函数,在该方法的调用周期外,对象是否有引用参数,它是否会释放这些引用;

l  如果一个函数申请了内存,它必须释放它们;

l  参数是否可以是空;

l  函数的使用方法是否会影响其性能;

l  如果函数可重入。它是怎么实现同步的?

这里有一个例子:

// 返回这个表的一个迭代器

// 当遍历结束时,由客户程序负责迭代器的释放

// 一旦此迭代器的创建者GargantuanTable对象被释放

// 客户程序不可再使用此迭代器

//

// 迭代器被初始化为指向表的开始

//

// 这个方法等价于:

// Iterator *iter = table->NewIterator();

iter->seek(“”);

return iter;

// 如果你想立即寻找返回的迭代器中的另一个位置,使用NewIterator()更快,而//且避免了额外的查找。

Iterator* getIterator()const

然而,避免不必要的冗长注释且不要添加显而易见的注释。比如下例外中,返回假的情况就没必要,因为这很明显:

// 如果表已被占满,不能再容纳实体,则返回真

bool IsTableFull();

当给构造和析构函数加注释时,注意,读者清楚地知道这些函数的作用,所以诸如“销毁这个对象”的注释毫无意义。注释内容应该说明构造函数怎样处理参数(比如它是否取得指针的控制权)和析构函数怎么完成清理工作。如果这些不很重要,可以省略它们。文件的开头注释中没有关于析构函数的注释是很正常的。

函数定义注释(FunctionDefinition)

每个函数都应该有一个注释来描述函数的功能和其完成这些功能的实现技巧(如果有的话)。比如,你在函数定义注释中,你可以描述编码中用到的技巧,给出大致的执行步骤或者解释一下你选择这种实现而不使用其他替代方法的原因。比如,你可能需要说明为什么函数前半部分需要锁而后半部分不需要。

注意,不能简单地重复头文件或者其他地方函数声明部分的注释。可以再次概括一下函数的功能,但焦点应该是函数是怎么实现功能的。

7.5      变量注释(Variable Comments)

通常,一个变量的实际名称已经提供了足够和描述信息来说明其用途。但某些情况下,可能需要更多注释。

类数据成员(ClassData Members)

每个类数据成员(也称实例变量或者成员变量)应该有一个描述其用途的注释。如果这个变量能取得具有特殊意义的标志值,比如NULL或-1,则需要加以说明。比如:

Private:

  //记录表中实体的总数

  //用于确保不打破数量限制。

  //-1意味着表的实体数未知

  int num_total_entries;

全局变量(GlobalVariables)

和数据成员一样,所有全局变量应该有一个描述其功能及其意义的注释。比如

// 所有本次回归测试使用到的测试用例数

cosnt int kNumTestCases = 6;

实现注释(ImplementationComments)

在实现中,应该对你代码技巧性的、不明显的、有趣的或者重要的部分进行注释。

类数据成员(ClassData Members)

技巧性的和复杂难懂的代码块前应该有注释:

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

}

行注释(LineComments)

同样,当行代码中有不明显的地方时,也需要在其行末添加注释。这种行末注释应该以2个空白与代码分开。

// If we have enough money, 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

DoSomethingElseThatIsLong(); // Comment here so there are two spaces

                                   // between the code and the comment

{// One space before commnet 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

}

7.6      以空、真/假、数字作为参数(NULL、true/false、1,2,3…)

当给函数传入NULL、布尔值和整型值串时,应该增加注释以说明这些值的意义,或者你可以使用常量使代码自文档化。比较以下两段代码:

bool success = CalculateSomething(interesting_value,

                                         10,

                                         false,

                                         NULL);//这些参数都是什么意思?

VS

bool success = CalculateSomething(interesting_value,

                                         10,   //默认基数

                                         flase,//非第一次调用

                                         NULL);//无回调

也可以使用替代方案,常量或自描述的变量:

const int  kDefaultBaseValue = 10;

const bool kFirstTimeCalling = false;

callback *null_callback = NULL;

bool success = CalculateSomething(interesting_value,

                                         kDefaultBaseValue,

                                         kFirstTimeCalling,

                                         null_callback);

不要这么做:

不要试图描述代码本身。假设代码阅读者比你更了解C++,既使这样,他/她也对你到底想做什么毫无头绪。

// 现在遍历b数组并且确保如果i存在,下一个元素是i+1。

...// 天哪,这些都是垃圾注释

7.7      注释的标点、拼写和语法 (Punctuation,Spelling and Gramma)

在书写注释时注意标点、拼写和语法,规范的注释比不规范的注释更具可读性。注释应该是完整的语句,首字母大写且以句号结束。简短的注释(比如行尾注释)可以不这么正式,但应该保持你一致的风格。完整的句子更易读,而且它们在某些方面保证了注释的完整性而不是一个未完成的想法。

尽管有时被代码审核者指责标点错误(比如本该用句号却用了逗号)会令人沮丧,但保持源代码的可维持性和高度的清晰性、可读性更重要。合适的标点、拼写和语法将有助于实现这个目标。

7.8      TODO注释(TODO Comments)

TODO注释用于提供临时、权宜的解决方案或者虽然很好但还有待完善的代码。TODO注释应该以TODO开头,后跟一个括着你的姓名、电子邮箱或者其他个人标识信息的括号。然后是一个可选的冒号。规定这些格式的主要目的是提供一种一致的TODO注释格式,以使修改者(能在要求下提供更多细节的开发者)更容易地找到这些注释。TODO注释并不是你自己提供解决方法的承诺。

// TODO(kl@gmail.com):Use a “*” here for concatenation operator

// TODO(Zeke) change this to use relations.

当以“在未来某个时候做一些事”的形式来进行TODO注释时,确保你的注释包括一个确定的日期(比如“2005年11月”)或者一个特定事件(比如“当所有客户机都可以处理XML响应的时候,移除这段代码”)

7.9      废弃性注释(Deprecation Comments)

使用废弃性注释来标识一个废弃的接口点。你可以使用全大写的DEPERCATED注释来声明一个接口被废弃。注释可位于接口声明前或者与其在同一行。在DEPRECATED后面,用括号注上你的姓名、电子邮箱或者其他个人标识信息。

一个废弃性注释必须简短、清楚的指令来告诉调用者如何修复他们的调用点。在C++中,你可以实现一个废弃的函数为内联函数来调用新的接口点。

把一个接口标记为DEPRECATED并不会引起调用点的改变。如果想让其他调用者实际停止废弃设施的使用,你必须自己修改调用点或者招募其他组员来帮助你。

新写的代码不应该再调用废弃接口点,而应该调用新的功能点。如果你不理解这些指令,找到创建废弃的人并询问新接口点的使用帮助。

8. 编码格式(Formatting)

代码风格和格式通常很随意,但具有统一代码风格的项目将更容易看懂。每个人不必完全遵照每一个格式规则,一些规则可能使一些人习惯于它们,但项目开发者应该遵照统一的风格规则,以便于项目组内其他人能更容易理解别人的代码。查看更多谷歌代码风格,参见谷歌编译器文件列表

8   

每行代码最多80个字符。尽管存在争议,但太多已有代码遵照这一规则,所以保持一致性更重要。

利:喜欢这条规则的程序员可能会觉得让他们改变编程窗口大小是粗鲁的,而且,也鲜没有超过80个字符的代码行。一些人喜欢在编码时一个挨一个地同时打开几个窗口,而80个字符的宽度正好可以摆放这些窗口。80字符宽度似乎已成为大多数开发人员开发环境的最大窗口宽度,那还改变它干吗呢?

弊:求变者认为更长的代码行可以增加代码可读性。80字符宽的限制是60年代的死板规则,现在的设备大多拥有宽屏,可以轻易显示更多字符。

结论:

每行最多80个字符。

但也有例外:其一、当注释行包含命令示例或者URL地址时,为便于粘贴和复制,可以超过80个字段;其二、包含命令(#include)后跟长路径名时,可以超过80个字符。但应尽量避免这种情况的发生;其三、#include保护命令(#include guard)不需要考虑最大字符数。

8.2  非ASCII码字符(Non-ASCII Characters)

注:本条适用开发语言为拉丁语系的开发人员,对于东方语系不适用。

应该尽量不用非ASCII字符,即使使用,请用UTF-8格式。

不应该在源文件中包含面向普通用户的硬编码,即使是英语也不行,所以,尽量避免使用非ASCII字符。然而,一些情况下包含这些文本也无可厚非。比如,如果你的代码从外文源文件中解析数据文件,使用一些硬编码的非ASCII字符串作为定界符是允许的。更常见地,单元测试代码(不应该被本地化)可以包含非ASCII字符串。在这些情况下,应该使用UTF-8编码,因为大多数工具可以处理ASCII和UTF-8字符。也可以使用16进制编码,而且这样可以增大可读性-举个例子,”\xEF\xBB\xBF”在Unicode中表示0宽度不间断空格,源文件以UTF-8编码时不会显示。

8.3  窗格还是制表符(Spaces vs.Tabs)

我们的建议是只使用空格,用2个空格进行缩进。不要使用制表符,应该把你编辑器的Tab调成空格。

8.4  函数声明与定义(FunctionDeclarations and Definitions)

返回类型和函数名在同一行,如果空间足够,参数列表也应在同一行。即:

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::ReallyRealyReallyLongFunctionName(

Type par_name1,      // 4个空格的缩进

Type par_nane2,

Type par_name3){

  DoSomething();

}

需要注意的地方:

l  返回类型应该保持与函数名在同一行;

l  左括号应该与函数名保持在同一行且中间不应该有空格;

l  括号与参数之间不应该有空格;

l  左花括号应该与最后一个参数保持在同一行;

l  右花括号要么独自一行,要么与左花括号保持在同一行(其他风格规则允许时);

l  右圆括号与左花括号之间应该有一个空格隔开;

l  无论是函数声明还是函数实现,都应该给参数命以同样的名称;

l  默认缩进2个空格;

l  分行的参数以4个空格缩进;

定义const函数时,const关键字应该与最后一个参数保持在同一行。

// 这个函数签名中的所有代码不超过80个字符

ReturnType FunctionName(Type par) const{

  ...

}

// 这个函数签名需要分行,但注意,const与最后一个参数保持在同一行

ReturnType ReallyLongFunctionName(Type par1,

                                         Type par2)const{

  ...

}

如果有未使用参数,在函数实现中注释其名称:

// 在接口中,参数一定要命名

class Shape{

  public:

virtual void Rotate(double radians) = 0;

};

// 在声明中,参数一定要命名

class Circle : public Shape{

  virtual void Rotate(double radians);

};

// 在函数实现中,以注释注明未使用的参数命名

void Circle::Rotate(double /*radians*/){}

// 不好-如果以后需要其他人实现,参数名称不详

void Circle::Rotate(double){}

8.5  函数调用(Function Calls)

尽量书写在一行,如果字符太多,将参数分行。比如:

bool retval = DoSomething(argument1,argument2,argumetn3);

如果参数无法在同一行内写完,可以分行,每一行的参数都应该与第一个参数对齐,且不要在左圆括号后或右圆括号前加空格。

bool retval = DoSomething(averyveryveryverylongargument1,

                               argument2,argument3);

如果函数参数太多,可以每一行写一个参数,这样代码更可读:

bool retval = DoSomething(argument1,

                               argument2,

                               argument3,

                               argument4);

如果参数签名太长,超过最大行宽,可以将参数分行写书:

if(...){

  ...

  ...

 if(...)

   DoSomethingTheatRequiresALongFunctionName(

       very_long_argument1,    // 缩进4个空格

       argument2,

       argument3,

       argument4);

}

8.6  条件语句(Conditonals)

括号内最好不要有空格,else应该另起一行。

基本条件语句主要有两种常见格式:一种是在括号和条件中插入空格,另一种是没有。但后者更常用。尽管两种方式都可以,但注意保持一致性。当你修改别人的代码时,保持与已有风格的一致;而当你添加新代码时,与当前目录或项目中已有代码的风格保持一致。当你不确定而且感觉无所谓时,不要插入空格。

if(condition){   // 括号内没有空格

  ...              // 缩进2个空格

}else{             // else与右花括号在同一行

  ...

}

当然,你也可以根据自己的喜好在括号内插入空格

if ( condition ){     // 括号内插入空格-不常用

  ...                    // 缩进2个空格

}else{                  // else与右花括号在同一行

  ...

}

注意:if和左圆括号之间应该有空格。右圆括号和左花括号(如果使用)之间也应该有空格。

if(condition)         // 糟糕-IF后面没有空格

if (condition){       // 糟糕-右圆括号和左花括号之间没有空格

if(condition){        // 非常糟糕

为便于阅读,简短的条件语句可以写在同一行。但仅限于这行代码很明确且没有else分支的情况:

if (x == kFoo) return new Foo();

if (x == kBar) return new Bar();

// if语句有else分支,不允许写在同一行

if (x) DoThis();

else DoThat();

通常,单语句条件语句不需要花括号,但可以根据自己的喜好加上。有复杂条件和语句的条件和循环加上圆括号后更具可读性。一些项目甚至要求if在所有情况都应该加上花括号。

if ( condition )    

  DoSomething();      // 缩进2个空格

if (condition) {

  DoSomethign();

}

为保持一致,if或者else分支加了花括号,另一分支也应该加上。

// 不允许-if分支有花括号,而else分支没有

if (condition) {

  foo;

}else

  Bar;

// 不允许-else分支有花括号,而if分支没有

If (condition)

  foo;

else {

  bar;

}

// if和else分支都应该加上花括号,因为它们之一有

if (condition) {

  foo;

}else{

  Bar;

}

8.7  循环和多分支语句(Loops andSwitch Statements)

在多分支语句中,块可以用花括号限制。而对于无循环体的循环,可以使用{}或continue。case语句块可根据自己的喜好使用花括号,如果使用花括号,请遵照下面的格式:

除非是判断枚举类型,多分支语句应该有一个默认分支(在枚举情况下,编译器将检测到未处理分支并警告)。如果默认分支不可能被执行,使用断言(assert)声明。

swtich (var) {

  case 0: {

...

Break;

}

case 1: {

  ...

  Break;

}

default: {

  assert(false);

}

空循环体应该用{}或者continue,但不要用单独的分号:

while (condition) {

  // 一直测试条件,直到它返回假

}

for(int i = 0; i < kSomeNumber;++i){}   // 很好-空循环体

while (condtiton) continue;               // 很好-continue指示无逻辑

while (condition);   // 糟糕-看起来像是do/while循环

8.8  指针与引用表达式(Pointer andReference Expressions)

点和箭头(.和->)两边不应该有空格,指针运算符(*和&)后不应该有尾随空格。下面都是格式正确的指针和引用表达式:

x = *p;

p = &x;

x = r.y;

x = r->y;

可以看到:

l  用于访问成员时,点运算符和箭头运算符两边没有空格;

l  指针运算符*和&后没有空格。

定义指针变量或参数时,星号(*)既可紧挨类型,也可紧挨变量名:

// 很好-空格在运算符前面

char *c;

const string &str;

// 这样也可以,空格在运算符后面

char* c;       // 但注意,要定义更多指针,每个变量名都应该有* “char* c,*d”

const string& str;

char * c;   // 糟糕-*两边都有空格

const string & str; // 糟糕-&两边都有空格

在每个文件内,保持一致性;但当修改其他文件时,保持已有风格。

8.9  布尔表达式(BooleanExpressions)

当你的布尔表达式超过标准行宽时,保持你分行书写的一致性。

下面的例子中,逻辑运算符AND放在行末:

If (this_one_thing > this_other_thing &&

     a_third_thing == a_fourth_thing &&

     yet_another && last_one) {

  ...

}

注意:这段代码中,两个逻辑运算符AND(&&)都在行末。这是谷歌代码中的常见用法,当然,逻辑运算符也可以位于行的开头。不要担心给逻辑表达式加括号,合适的括号可增加代码的可读性。另外,使用符号运算符(&&、~)而不是明文(and、compl)运算符。

8.10   返回值(Return Values)

不要给return语句加不必要的括号,仅有一种情况需要这样:return的形式是x = expr。

return result;         // 简单情况下不需要括号

return (some_long_codition &&// 使用空格来使复杂表达式更可读

 another_condition);

return (value);       // 你不会这样给变量赋值 var = (value);,

return (result);      // return语句不是函数,所以不需要括号

8.11    变量和数组的初始化(Variable and Array Initialization)

可以使用赋值(=)或者默认构造()来完成初始化:

int x = 3;   // 赋值-C风格初始化

int x(3);    // 默认构造-C++风格初始化

string name(“Some Name);

string name = “Some Name”;

8.12    预处理指令(Preprocessor Directives)

预处理指针不需要缩进,即使在已缩进的代码块内,也应该行内左对齐。

8.13    类格式(Class Format)

成员按public、protected和private的次序定义,每个部分缩进1个空格。下例显示了基本的类格式(例子中缺少必要的注释,参见类注释(Class Comments)部分):

class MyClass : public OtherClass{

 public:        // 注意,缩进1个空格

  MyClass();    // 注意,缩进2个空格

  Explicit MyClass(int var);

  ~MyClass(){}

  void SomeFunction();

  void SomeFunctionThatDoesNothing(){

}

void set_some_var(int var) { some_var = var; }

int some_var() const { reurn some_var; }

 

 private:

  bool SomeInternalFunction();

  int some_var;

  int some_other_var;

  DISALLOW_COPY_AND_ASSIGN(MyClass);

}

注意:

l  基类名称直接置于子类名称后的同一行内而不受最大长度(80字符)的限制;

l  public::protected::和private::只需缩进1个空格,且除非是第1个实例的情况,它们应该位于单独的行,最好在紧接着的下一行进行成员声明;

l  public部分行1位、protected部分第2位,private放在最后;

l  参见声明次序(DeclarationOrder)部分关于各部分声明次序的说明。

8.14    构造函数初始化列表(Constructor Initializer Lists)

构造函数的初始化列表即可以位于一行,也可以分多行书写,但注意缩进4个空格:

// 置于同一行

MyClass::MyClass(int var):some_var(var),some_other_var(var+1){}

// 需要分行时,缩进4个空格,冒号放在第一行初始化参数前

MyClass::MyClass(int var)

: some_var(var),            // 缩进4个空格

   Some_other_var(var+1){  // 注意排列整齐

  ...

  DoSomething();

  ...

}

8.15    名称空间格式(Namespace Formatting)

命令空间不需要额外的缩进级别:

8.16    水平空白(Horizontal Whitespace)

水平空白的使用依情况而定,但不要在行末使用水平空白。

通常:

当进行文件集成时,结尾的空白(完全可以移除)给其他编辑者带来额外的工作。所以,不要在代码行结尾添加空白。如果你已经修改过这些行,移除它们,也可以单独进行清理工作(在没有其他开发者使用文件的时候)。

循环和条件判断(Loopsand Conditions)

if (b) {        // 条件和循环关键字后加空格

} 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){  // For循环在分号前后常常插入空格

  ...

switch (i) {

  case 1:              // 冒号前无空格

...

Case 2: break;      // 如果冒号后有代码,最好插入空格

运算符(Operators)

x = 0;        // 赋值运算符两边应该有空格

++x;      // 单目运算符紧临其操作数

if (x && !y)

  ...

v = w * x + y / z; // 双目运算符两边常常有插入空格

v = w*x + y/z;      // 但不加也无可厚非

v = w * (x + z)     // 括号不应该再加空格

模板与类型转换(Templatesand Casts)

Vector<string> x;    // 尖括号、圆括号内及其两边无空格

y = static_cast<char*>(x);

vector<char*> x;     // 指针与类型之间可以有空格

set<list<string> > x; // 连续两个>>时需要空格

set< list<string> > x;// 前面也可以加空格

8.17    垂直空白(Vertical Whitespace)

尽量少用垂直空白。

这不是规则,而是原则:没必要的话不要使用空行。尤其,函数之间不要插入空行,函数开始处不要有空行,不要以空行结束函数,同时,注意函数内的空行。

一些使用空行的经验法则:

l  函数开始和结束处的空行对增加可读性鲜有作用;

l  If-else链内的空行常常很有用。

9. 本规则的例外情况(Exceptonsto the Rules)

本文上述编码习惯是强制性的。但正如大多数好的规则,总有一些例外:

9   

9.1   现存不一致代码(ExistingNon-conformant Code)

当处理不遵守本风格指南的旧代码时,你可以引入其他规则。如果你发现你修改的代码遵守不同于本指南的其他风格,为保持一致性,你可能不得不遵守他们的规则。如果你不确定,可以询问代码的原始作者或者目前的代码拥有者。记住,本地一致性也是一致性。

9.2  Windows代码(Windows Code)

Windows程序员有他们自已的一套惯例,这些惯例大多来自Windows的头文件和微软的代码。我们的目标是让任何人都可以容易地读懂你的代码,所以我们推出这个能让任何人在任何平台进行C++编程的风格指南。

如果已你习惯于流行的Windows风格,重申一下你可能忘记的规则是必要的:

l  不要使用匈牙利命名法,使用谷歌的命名法,包括源文件以.cc作为扩展名;

l  Windows定义了他们自己的简单类型,比如DWORD、HANDLE等。进行Windows API调用时,建议使用这些类型,但尽量与C++的内建类型保持一致,比如使用const TCHAR*而不是LPCTSTR;

l  使用MicrosoftVisual C++时,将编译器的警告调到3级,并将警告处理成错误;

l  不要使用#pragma once,使用谷歌标准包含保护命令。

l  不要使用任何非标准扩展,比如#pragma和__declspec,除非你必须这样做。__declspec(dllimprot)和__declspec(dllexport)的使用是允许的,但必须通过DLLIMPORT和DLLEXPORT宏来使用,以便利用代码者可以方便地禁用这些扩展。

然而,进行Windows编程时,我们不得不打破一些规则:

l  通常,我们禁止多重实现继承,但使用Windows的COM和ATL/WTL类时,必须多重继承。这时,你可以使用多重实现继承来实现COM或ATL/WTL类和接口;

l  尽管你自己的代码中不应该有异常处理,但在ATL和一些STL中,包括Visaul C++库,却需要异常处理。使用ATL时,定义_ATL_NO_EXCEPTIONS来禁用异常处理。同时,注意研究能否关闭STL的异常处理,如果不同,打开编译器的异常处理。(注意,这里只是编译STL。你自己的代码仍然不要进行异常处理。);

l  使用预编译头文件的常见做法是在头文件头部包含名如StdAfx.h或precompile.h。为便于与其他项目进行代码重用,避免显式地包含这些文件(除了在precomile.cc文件的情况),使用编译器参数/FI来自动包含这些文件。

l  有些常被命名为resource.h的资源头文件,它们只包含一些宏定义,不需要遵守此指南。

10.     结束语(PartingWords)

使用风格常识并保持一致性。

编写代码时,注意查看已有代码的风格。如果他们在if条件句中使用空格,你也应该这样做;如果他们的注释周围无星框,你的注释中也不要有。

本风格指南的主要目标是确立一个编码的常识,以便开发人员专注于你的代码,而不是你是怎么实现这些代码的。我们只是想让更多开发者了解这些常识。但本地风格也很重要。如果你增加的代码与已有代码风格迥异,这会打断读者阅读的连续性。尽量不要这么做。

好了,已经写得够多了,编码本身更有趣。编码快乐。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值