关闭

关闭

关闭

封号提示

内容

首页 Google C++编程风格指南.pdf

Google C++编程风格指南.pdf

Google C++编程风格指南.pdf

上传者: johnnie_chan 2010-12-18 评分 0 0 0 0 0 0 暂无简介 简介 举报

简介:本文档为《Google C++编程风格指南pdf》,可适用于IT/计算机领域,主题内容包含GoogleC编程风格指南(一)背景Google的开源项目大多使用C开发。每一个C程序员也都知道C具有很多强大的语言特性但这种强大不可避免的导致它的符等。

GoogleC编程风格指南(一)背景Google的开源项目大多使用C开发。每一个C程序员也都知道C具有很多强大的语言特性但这种强大不可避免的导致它的复杂这种复杂会使得代码更易于出现bug、难于阅读和维护。本指南的目的是通过详细阐述在C编码时要怎样写、不要怎样写来规避其复杂性。这些规则可在允许代码有效使用C语言特性的同时使其易于管理。风格也被视为可读性主要指称管理C代码的习惯。使用术语风格有点用词不当因为这些习惯远不止源代码文件格式这么简单。使代码易于管理的方法之一是增强代码一致性让别人可以读懂你的代码是很重要的保持统一编程风格意味着可以轻松根据“模式匹配”规则推断各种符号的含义。创建通用的、必需的习惯用语和模式可以使代码更加容易理解在某些情况下改变一些编程风格可能会是好的选择但我们还是应该遵循一致性原则尽量不这样去做。本指南的另一个观点是C特性的臃肿。C是一门包含大量高级特性的巨型语言某些情况下我们会限制甚至禁止使用某些特性使代码简化避免可能导致的各种问题指南中列举了这类特性并解释说为什么这些特性是被限制使用的。由Google开发的开源项目将遵照本指南约定。注意:本指南并非C教程我们假定读者已经对C非常熟悉。头文件通常每一个cc文件(C的源文件)都有一个对应的h文件(头文件)也有一些例外如单元测试代码和只包含main()的cc文件。正确使用头文件可令代码在可读性、文件大小和性能上大为改观。下面的规则将引导你规避使用头文件时的各种麻烦。#define的保护所有头文件都应该使用#define防止头文件被多重包含(multipleinclusion)命名格式当是:<PROJECT><PATH><FILE>H为保证唯一性头文件的命名应基于其所在项目源代码树的全路径。例如项目foo中的头文件foosrcbarbazh按如下方式保护:#ifndefFOOBARBAZH#defineFOOBARBAZH#endifFOOBARBAZH头文件依赖使用前置声明(forwarddeclarations)尽量减少h文件中#include的数量。当一个头文件被包含的同时也引入了一项新的依赖(dependency)只要该头文件被修改代码就要重新编译。如果你的头文件包含了其他头文件这些头文件的任何改变也将导致那些包含了你的头文件的代码重新编译。因此我们宁可尽量少包含头文件尤其是那些包含在其他头文件中的。使用前置声明可以显著减少需要包含的头文件数量。举例说明:头文件中用到类File但不需要访问File的声明则头文件中只需前置声明classFile无需#include"filebasefileh"。在头文件如何做到使用类Foo而无需访问类的定义?)将数据成员类型声明为Foo*或Foo)参数、返回值类型为Foo的函数只是声明(但不定义实现))静态数据成员的类型可以被声明为Foo因为静态数据成员的定义在类定义之外。另一方面如果你的类是Foo的子类或者含有类型为Foo的非静态数据成员则必须为之包含头文件。有时使用指针成员(pointermembers如果是scopedptr更好)替代对象成员(objectmembers)的确更有意义。然而这样的做法会降低代码可读性及执行效率。如果仅仅为了少包含头文件还是不要这样替代的好。当然cc文件无论如何都需要所使用类的定义部分自然也就会包含若干头文件。译者注:能依赖声明的就不要依赖定义。内联函数只有当函数只有行甚至更少时才会将其定义为内联函数(inlinefunction)。定义(Definition):当函数被声明为内联函数之后编译器可能会将其内联展开无需按通常的函数调用机制调用内联函数。优点:当函数体比较小的时候内联该函数可以令目标代码更加高效。对于存取函数(accessor、mutator)以及其他一些比较短的关键执行函数。缺点:滥用内联将导致程序变慢内联有可能是目标代码量或增或减这取决于被内联的函数的大小。内联较短小的存取函数通常会减少代码量但内联一个很大的函数(译者注:如果编译器允许的话)将戏剧性的增加代码量。在现代处理器上由于更好的利用指令缓存(instructioncache)小巧的代码往往执行更快。结论:一个比较得当的处理规则是不要内联超过行的函数。对于析构函数应慎重对待析构函数往往比其表面看起来要长因为有一些隐式成员和基类析构函数(如果有的话)被调用!另一有用的处理规则:内联那些包含循环或switch语句的函数是得不偿失的除非在大多数情况下这些循环或switch语句从不执行。重要的是虚函数和递归函数即使被声明为内联的也不一定就是内联函数。通常递归函数不应该被声明为内联的(译者注:递归调用堆栈的展开并不像循环那么简单比如递归层数在编译时可能是未知的大多数编译器都不支持内联递归函数)。析构函数内联的主要原因是其定义在类的定义中为了方便抑或是对其行为给出文档。inlh文件复杂的内联函数的定义应放在后缀名为inlh的头文件中。在头文件中给出内联函数的定义可令编译器将其在调用处内联展开。然而实现代码应完全放到cc文件中我们不希望h文件中出现太多实现代码除非这样做在可读性和效率上有明显优势。如果内联函数的定义比较短小、逻辑比较简单其实现代码可以放在h文件中。例如存取函数的实现理所当然都放在类定义中。出于实现和调用的方便较复杂的内联函数也可以放到h文件中如果你觉得这样会使头文件显得笨重还可以将其分离到单独的inlh中。这样即把实现和类定义分离开来当需要时包含实现所在的inlh即可。inlh文件还可用于函数模板的定义从而使得模板定义可读性增强。要提醒的一点是inlh和其他头文件一样也需要#define保护。函数参数顺序(FunctionParameterOrdering)定义函数时参数顺序为:输入参数在前输出参数在后。CC函数参数分为输入参数和输出参数两种有时输入参数也会输出(译者注:值被修改时)。输入参数一般传值或常数引用(constreferences)输出参数或输入输出参数为非常数指针(nonconstpointers)。对参数排序时将所有输入参数置于输出参数之前。不要仅仅因为是新添加的参数就将其置于最后而应该依然置于输出参数之前。这一点并不是必须遵循的规则输入输出两用参数(通常是类结构体变量)混在其中会使得规则难以遵循。包含文件的名称及次序将包含次序标准化可增强可读性、避免隐藏依赖(hiddendependencies译者注:隐藏依赖主要是指包含的文件中编译时)次序如下:C库、C库、其他库的h、项目内的h。项目内头文件应按照项目源代码目录树结构排列并且避免使用UNIX文件路径(当前目录)和(父目录)。例如googleawesomeprojectsrcbaseloggingh应像这样被包含:#include"baseloggingh"dirfoocc的主要作用是执行或测试dirfooh的功能foocc中包含头文件的次序如下:dirfooh(优先位置详情如下)C系统文件C系统文件其他库头文件本项目内头文件这种排序方式可有效减少隐藏依赖我们希望每一个头文件独立编译。最简单的实现方式是将其作为第一个h文件包含在对应的cc中。dirfoocc和dirfooh通常位于相同目录下(像basebasictypesunittestcc和basebasictypesh)但也可在不同目录下。相同目录下头文件按字母序是不错的选择。举例来说googleawesomeprojectsrcfoointernalfooservercc的包含次序如下:#include"foopublicfooserverh"优先位置#include<systypesh>#include<unistdh>#include<hashmap>#include<vector>#include"basebasictypesh"#include"basecommandlineflagsh"#include"foopublicbarh"译者:英语不太好翻译的也就不太好。这一篇主要提到的是头文件的一些规则总结一下:避免多重包含是学编程时最基本的要求前置声明是为了降低编译依赖防止修改一个头文件引发多米诺效应内联函数的合理使用可提高代码执行效率inlh可提高代码可读性(一般用不到吧:D)标准化函数参数顺序可以提高可读性和易维护性(对函数参数的堆栈空间有轻微影响我以前大多是相同类型放在一起)包含文件的名称使用和虽然方便却易混乱使用比较完整的项目路径看上去很清晰、很条理包含文件的次序除了美观之外最重要的是可以减少隐藏依赖使每个头文件在“最需要编译”(对应源文件处:D)的地方编译有人提出库文件放在最后这样出错先是项目内的文件头文件都放在对应源文件的最前面这一点足以保证内部错误的及时发现了。GoogleC编程风格指南(二)作用域命名空间(Namespaces)在cc文件中提倡使用不具名的命名空间(unnamednamespaces译者注:不具名的命名空间就像不具名的类一样似乎被介绍的很少:()。使用具名命名空间时其名称可基于项目或路径名称不要使用using指示符。定义:命名空间将全局作用域细分为不同的、具名的作用域可有效防止全局作用域的命名冲突。优点:命名空间提供了(可嵌套)命名轴线(nameaxis译者注:将命名分割在不同命名空间内)当然类也提供了(可嵌套)的命名轴线(译者注:将命名分割在不同类的作用域内)。举例来说两个不同项目的全局作用域都有一个类Foo这样在编译或运行时造成冲突。如果每个项目将代码置于不同命名空间中project::Foo和project::Foo作为不同符号自然不会冲突。缺点:命名空间具有迷惑性因为它们和类一样提供了额外的(可嵌套的)命名轴线。在头文件中使用不具名的空间容易违背C的唯一定义原则(OneDefinitionRule(ODR))。结论:根据下文将要提到的策略合理使用命名空间。)不具名命名空间(UnnamedNamespaces)在cc文件中允许甚至提倡使用不具名命名空间以避免运行时的命名冲突:namespace{cc文件中命名空间的内容无需缩进enum{UNUSED,EOF,ERROR}经常使用的符号boolAtEof(){returnpos==EOF}使用本命名空间内的符号EOF}namespace然而与特定类关联的文件作用域声明在该类中被声明为类型、静态数据成员或静态成员函数而不是不具名命名空间的成员。像上文展示的那样不具名命名空间结束时用注释namespace标识。不能在h文件中使用不具名命名空间。)具名命名空间(NamedNamespaces)具名命名空间使用方式如下:命名空间将除文件包含、全局标识的声明定义以及类的前置声明外的整个源文件封装起来以同其他命名空间相区分。h文件namespacemynamespace{所有声明都置于命名空间中注意不要使用缩进classMyClass{public:voidFoo()}}namespacemynamespacecc文件namespacemynamespace{函数定义都置于命名空间中voidMyClass::Foo(){}}namespacemynamespace通常的cc文件会包含更多、更复杂的细节包括对其他命名空间中类的引用等。#include"ah"DEFINEbool(someflag,false,"dummyflag")classC全局命名空间中类C的前置声明namespacea{classA}命名空间a中的类a::A的前置声明namespaceb{codeforbb中的代码}namespaceb不要声明命名空间std下的任何内容包括标准库类的前置声明。声明std下的实体会导致不明确的行为如不可移植。声明标准库下的实体需要包含对应的头文件。最好不要使用using指示符以保证命名空间下的所有名称都可以正常使用。禁止污染命名空间usingnamespacefoo在cc文件、h文件的函数、方法或类中可以使用using。允许:cc文件中h文件中必须在函数、方法或类的内部使用using::foo::bar在cc文件、h文件的函数、方法或类中还可以使用命名空间别名。允许:cc文件中h文件中必须在函数、方法或类的内部使用namespacefbz=::foo::bar::baz嵌套类(NestedClass)当公开嵌套类作为接口的一部分时虽然可以直接将他们保持在全局作用域中但将嵌套类的声明置于命名空间中是更好的选择。定义:可以在一个类中定义另一个类嵌套类也称成员类(memberclass)。classFoo{private:Bar是嵌套在Foo中的成员类classBar{}}优点:当嵌套(成员)类只在被嵌套类(enclosingclass)中使用时很有用将其置于被嵌套类作用域作为被嵌套类的成员不会污染其他作用域同名类。可在被嵌套类中前置声明嵌套类在cc文件中定义嵌套类避免在被嵌套类中包含嵌套类的定义因为嵌套类的定义通常只与实现相关。缺点:只能在被嵌套类的定义中才能前置声明嵌套类。因此任何使用Foo::Bar*指针的头文件必须包含整个Foo的声明。结论:不要将嵌套类定义为public除非它们是接口的一部分比如某个方法使用了这个类的一系列选项。非成员函数(Nonmember)、静态成员函数(StaticMember)和全局函数(GlobalFunctions)使用命名空间中的非成员函数或静态成员函数尽量不要使用全局函数。优点:某些情况下非成员函数和静态成员函数是非常有用的将非成员函数置于命名空间中可避免对全局作用域的污染。缺点:将非成员函数和静态成员函数作为新类的成员或许更有意义当它们需要访问外部资源或具有重要依赖时更是如此。结论:有时不把函数限定在类的实体中是有益的甚至需要这么做要么作为静态成员要么作为非成员函数。非成员函数不应依赖于外部变量并尽量置于某个命名空间中。相比单纯为了封装若干不共享任何静态数据的静态成员函数而创建类不如使用命名空间。定义于同一编译单元的函数被其他编译单元直接调用可能会引入不必要的耦合和连接依赖静态成员函数对此尤其敏感。可以考虑提取到新类中或者将函数置于独立库的命名空间中。如果你确实需要定义非成员函数又只是在cc文件中使用它可使用不具名命名空间或static关联(如staticintFoo(){})限定其作用域。局部变量(LocalVariables)将函数变量尽可能置于最小作用域内在声明变量时将其初始化。C允许在函数的任何位置声明变量。我们提倡在尽可能小的作用域中声明变量离第一次使用越近越好。这使得代码易于阅读易于定位变量的声明位置、变量类型和初始值。特别是应使用初始化代替声明赋值的方式。intii=f()坏初始化和声明分离ntj=g()好初始化时声明注意:gcc可正确执行for(inti=i<i)(i的作用域仅限for循环)因此其他for循环中可重用i。if和while等语句中作用域声明(scopedeclaration)同样是正确的。while(constchar*p=strchr(str,''))str=p注意:如果变量是一个对象每次进入作用域都要调用其构造函数每次退出作用域都要调用其析构函数。低效的实现for(inti=i<i){Foof构造函数和析构函数分别调用次!fDoSomething(i)}类似变量放到循环作用域外面声明要高效的多:Foof构造函数和析构函数只调用次for(inti=i<i){fDoSomething(i)}全局变量(GlobalVariables)class类型的全局变量是被禁止的内建类型的全局变量是允许的当然多线程代码中非常数全局变量也是被禁止的。永远不要使用函数返回值初始化全局变量。不幸的是全局变量的构造函数、析构函数以及初始化操作的调用顺序只是被部分规定每次生成有可能会有变化从而导致难以发现的bugs。因此禁止使用class类型的全局变量(包括STL的string,vector等等)因为它们的初始化顺序有可能导致构造出现问题。内建类型和由内建类型构成的没有构造函数的结构体可以使用如果你一定要使用class类型的全局变量请使用单件模式(singletonpattern)。对于全局的字符串常量使用C风格的字符串而不要使用STL的字符串:constcharkFrogSays="ribbet"虽然允许在全局作用域中使用全局变量使用时务必三思。大多数全局变量应该是类的静态数据成员或者当其只在cc文件中使用时将其定义到不具名命名空间中或者使用静态关联以限制变量的作用域。记住静态成员变量视作全局变量所以也不能是class类型!译者:这一篇主要提到的是作用域的一些规则总结一下:cc中的不具名命名空间可避免命名冲突、限定作用域避免直接使用using提示符污染命名空间嵌套类符合局部使用原则只是不能在其他头文件中前置声明尽量不要public尽量不用全局函数和全局变量考虑作用域和命名空间限制尽量单独形成编译单元多线程中的全局变量(含静态成员变量)不要使用class类型(含STL容器)避免不明确行为导致的bugs。作用域的使用除了考虑名称污染、可读性之外主要是为降低耦合度提高编译、执行效率。GoogleC编程风格指南(三)这一篇主要提到的是类Lippman在《InsideTheCObjectModel》第二章中对构造函数作了详尽说明本文中提到的几个单词基本仿该书中译本侯捷先生的翻译:explicit:明确的implicit:隐含的trivial:没有意义的nontrivial:有意义的原文地址:GoogleCStyleGuide类类是C中基本的代码单元自然被广泛使用。本节列举了在写一个类时要做什么、不要做什么。构造函数(Constructor)的职责构造函数中只进行那些没有实际意义的(trivial译者注:简单初始化对于程序执行没有实际的逻辑意义因为成员变量的“有意义”的值大多不在构造函数中确定)初始化可能的话使用Init()方法集中初始化为有意义的(nontrivial)数据。定义:在构造函数中执行初始化操作。优点:排版方便无需担心类是否初始化。缺点:在构造函数中执行操作引起的问题有:)构造函数中不易报告错误不能使用异常。)操作失败会造成对象初始化失败引起不确定状态。)构造函数内调用虚函数调用不会派发到子类实现中即使当前没有子类化实现将来仍是隐患。)如果有人创建该类型的全局变量(虽然违背了上节提到的规则)构造函数将在main()之前被调用有可能破坏构造函数中暗含的假设条件。例如gflags尚未初始化。结论:如果对象需要有意义的(nontrivial)初始化考虑使用另外的Init()方法并(或)增加一个成员标记用于指示对象是否已经初始化成功。默认构造函数(DefaultConstructors)如果一个类定义了若干成员变量又没有其他构造函数需要定义一个默认构造函数否则编译器将自动生产默认构造函数。定义:新建一个没有参数的对象时默认构造函数被调用当调用new(为数组)时默认构造函数总是被调用。优点:默认将结构体初始化为“不可能的”值使调试更加容易。缺点:对代码编写者来说这是多余的工作。结论:如果类中定义了成员变量没有提供其他构造函数你需要定义一个默认构造函数(没有参数)。默认构造函数更适合于初始化对象使对象内部状态(internalstate)一致、有效。提供默认构造函数的原因是:如果你没有提供其他构造函数又没有定义默认构造函数编译器将为你自动生成一个编译器生成的构造函数并不会对对象进行初始化。如果你定义的类继承现有类而你又没有增加新的成员变量则不需要为新类定义默认构造函数。明确的构造函数(ExplicitConstructors)对单参数构造函数使用C关键字explicit。定义:通常只有一个参数的构造函数可被用于转换(conversion译者注:主要指隐式转换下文可见)例如定义了Foo::Foo(stringname)当向需要传入一个Foo对象的函数传入一个字符串时构造函数Foo::Foo(stringname)被调用并将该字符串转换为一个Foo临时对象传给调用函数。看上去很方便但如果你并不希望如此通过转换生成一个新对象的话麻烦也随之而来。为避免构造函数被调用造成隐式转换可以将其声明为explicit。优点:避免不合时宜的变换。缺点:无。结论:所有单参数构造函数必须是明确的。在类定义中将关键字explicit加到单参数构造函数前:explicitFoo(stringname)例外:在少数情况下拷贝构造函数可以不声明为explicit特意作为其他类的透明包装器的类。类似例外情况应在注释中明确说明。拷贝构造函数(CopyConstructors)仅在代码中需要拷贝一个类对象的时候使用拷贝构造函数不需要拷贝时应使用DISALLOWCOPYANDASSIGN。定义:通过拷贝新建对象时可使用拷贝构造函数(特别是对象的传值时)。优点:拷贝构造函数使得拷贝对象更加容易STL容器要求所有内容可拷贝、可赋值。缺点:C中对象的隐式拷贝是导致很多性能问题和bugs的根源。拷贝构造函数降低了代码可读性相比按引用传递跟踪按值传递的对象更加困难对象修改的地方变得难以捉摸。结论:大量的类并不需要可拷贝也不需要一个拷贝构造函数或赋值操作(assignmentoperator)。不幸的是如果你不主动声明它们编译器会为你自动生成而且是public的。可以考虑在类的private中添加空的(dummy)拷贝构造函数和赋值操作只有声明没有定义。由于这些空程序声明为private当其他代码试图使用它们的时候编译器将报错。为了方便可以使用宏DISALLOWCOPYANDASSIGN:禁止使用拷贝构造函数和赋值操作的宏应在类的private:中使用#defineDISALLOWCOPYANDASSIGN(TypeName)TypeName(constTypeName)voidoperator=(constTypeName)classFoo{public:Foo(intf)~Foo()private:DISALLOWCOPYANDASSIGN(Foo)}如上所述绝大多数情况下都应使用DISALLOWCOPYANDASSIGN如果类确实需要可拷贝应在该类的头文件中说明原由并适当定义拷贝构造函数和赋值操作注意在operator=中检测自赋值(selfassignment)情况。在将类作为STL容器值得时候你可能有使类可拷贝的冲动。类似情况下真正该做的是使用指针指向STL容器中的对象可以考虑使用std::tr::sharedptr。结构体和类(StructsvsClasses)仅当只有数据时使用struct其它一概使用class。在C中关键字struct和class几乎含义等同我们为其人为添加语义以便为定义的数据类型合理选择使用哪个关键字。struct被用在仅包含数据的消极对象(passiveobjects)上可能包括有关联的常量但没有存取数据成员之外的函数功能而存取功能通过直接访问实现而无需方法调用这儿提到的方法是指只用于处理数据成员的如构造函数、析构函数、Initialize()、Reset()、Validate()。如果需要更多的函数功能class更适合如果不确定的话直接使用class。如果与STL结合对于仿函数(functors)和特性(traits)可以不用class而是使用struct。注意:类和结构体的成员变量使用不同的命名规则。继承(Inheritance)使用组合(composition译者注这一点也是GoF在《DesignPatterns》里反复强调的)通常比使用继承更适宜如果使用继承的话只使用公共继承。定义:当子类继承基类时子类包含了父基类所有数据及操作的定义。C实践中继承主要用于两种场合:实现继承(implementationinheritance)子类继承父类的实现代码接口继承(interfaceinheritance)子类仅继承父类的方法名称。优点:实现继承通过原封不动的重用基类代码减少了代码量。由于继承是编译时声明(compiletimedeclaration)编码者和编译器都可以理解相应操作并发现错误。接口继承可用于程序上增强类的特定API的功能在类没有定义API的必要实现时编译器同样可以侦错。缺点:对于实现继承由于实现子类的代码在父类和子类间延展要理解其实现变得更加困难。子类不能重写父类的非虚函数当然也就不能修改其实现。基类也可能定义了一些数据成员还要区分基类的物理轮廓(physicallayout)。结论:所有继承必须是public的如果想私有继承的话应该采取包含基类实例作为成员的方式作为替代。不要过多使用实现继承组合通常更合适一些。努力做到只在“是一个”("isa"译者注其他"hasa"情况下请使用组合)的情况下使用继承:如果Bar的确“是一种”Foo才令Bar是Foo的子类。必要的话令析构函数为virtual必要是指如果该类具有虚函数其析构函数应该为虚函数。译者注:至于子类没有额外数据成员甚至父类也没有任何数据成员的特殊情况下析构函数的调用是否必要是语义争论从编程设计规范的角度看在含有虚函数的父类中定义虚析构函数绝对必要。限定仅在子类访问的成员函数为protected需要注意的是数据成员应始终为私有。当重定义派生的虚函数时在派生类中明确声明其为virtual。根本原因:如果遗漏virtual阅读者需要检索类的所有祖先以确定该函数是否为虚函数(译者注虽然不影响其为虚函数的本质)。多重继承(MultipleInheritance)真正需要用到多重实现继承(multipleimplementationinheritance)的时候非常少只有当最多一个基类中含有实现其他基类都是以Interface为后缀的纯接口类时才会使用多重继承。定义:多重继承允许子类拥有多个基类要将作为纯接口的基类和具有实现的基类区别开来。优点:相比单继承多重实现继承可令你重用更多代码。缺点:真正需要用到多重实现继承的时候非常少多重实现继承看上去是不错的解决方案通常可以找到更加明确、清晰的、不同的解决方案。结论:只有当所有超类(superclass)除第一个外都是纯接口时才能使用多重继承。为确保它们是纯接口这些类必须以Interface为后缀。注意:关于此规则Windows下有种例外情况(译者注将在本译文最后一篇的规则例外中阐述)。接口(Interface)接口是指满足特定条件的类这些类以Interface为后缀(非必需)。定义:当一个类满足以下要求时称之为纯接口:)只有纯虚函数("=")和静态函数(下文提到的析构函数除外))没有非静态数据成员)没有定义任何构造函数。如果有也不含参数并且为protected)如果是子类也只能继承满足上述条件并以Interface为后缀的类。接口类不能被直接实例化因为它声明了纯虚函数。为确保接口类的所有实现可被正确销毁必须为之声明虚析构函数(作为第条规则的例外析构函数不能是纯虚函数)。具体细节可参考Stroustrup的《TheCProgrammingLanguage,rdedition》第节。优点:以Interface为后缀可令他人知道不能为该接口类增加实现函数或非静态数据成员这一点对于多重继承尤其重要。另外对于Java程序员来说接口的概念已经深入人心。缺点:Interface后缀增加了类名长度为阅读和理解带来不便同时接口特性作为实现细节不应暴露给客户。结论:。只有在满足上述需要时类才以Interface结尾但反过来满足上述需要的类未必一定以Interface结尾。操作符重载(OperatorOverloading)除少数特定环境外不要重载操作符。定义:一个类可以定义诸如、等操作符使其可以像内建类型一样直接使用。优点:使代码看上去更加直观就像内建类型(如int)那样重载操作符使那些Equals()、Add()等黯淡无光的函数名好玩多了。为了使一些模板函数正确工作你可能需要定义操作符。缺点:虽然操作符重载令代码更加直观但也有一些不足)混淆直觉让你误以为一些耗时的操作像内建操作那样轻巧)查找重载操作符的调用处更加困难查找Equals()显然比同等调用==容易的多)有的操作符可以对指针进行操作容易导致bugsFoo做的是一件事而Foo可能做的是完全不同的另一件事对于二者编译器都不会报错使其很难调试)重载还有令你吃惊的副作用比如重载操作符的类不能被前置声明。结论:一般不要重载操作符尤其是赋值操作(operator=)比较阴险应避免重载。如果需要的话可以定义类似Equals()、CopyFrom()等函数。然而极少数情况下需要重载操作符以便与模板或“标准”C类衔接(如operator<<(ostream,constT))如果被证明是正当的尚可接受但你要尽可能避免这样做。尤其是不要仅仅为了在STL容器中作为key使用就重载operator==或operator<取而代之你应该在声明容器的时候创建相等判断和大小比较的仿函数类型。有些STL算法确实需要重载operator==时可以这么做不要忘了提供文档说明原因。参考拷贝构造函数和函数重载。存取控制(AccessControl)将数据成员私有化并提供相关存取函数如定义变量foo及取值函数foo()、赋值函数setfoo()。存取函数的定义一般内联在头文件中。参考继承和函数命名。声明次序(DeclarationOrder)在类中使用特定的声明次序:public:在private:之前成员函数在数据成员(变量)前。定义次序如下:public:、protected:、private:如果那一块没有直接忽略即可。每一块中声明次序一般如下:)typedefs和enums)常量)构造函数)析构函数)成员函数含静态成员函数)数据成员含静态数据成员。宏DISALLOWCOPYANDASSIGN置于private:块之后作为类的最后部分。参考拷贝构造函数。cc文件中函数的定义应尽可能和声明次序一致。不要将大型函数内联到类的定义中通常只有那些没有特别意义的或者性能要求高的并且是比较短小的函数才被定义为内联函数。更多细节参考译文第一篇的内联函数。编写短小函数(WriteShortFunctions)倾向于选择短小、凝练的函数。长函数有时是恰当的因此对于函数长度并没有严格限制。如果函数超过行可以考虑在不影响程序结构的情况下将其分割一下。即使一个长函数现在工作的非常好一旦有人对其修改有可能出现新的问题甚至导致难以发现的bugs。使函数尽量短小、简单便于他人阅读和修改代码。在处理代码时你可能会发现复杂的长函数不要害怕修改现有代码:如果证实这些代码使用、调试困难或者你需要使用其中的一小块考虑将其分割为更加短小、易于管理的若干函数。译者:关于类的注意事项总结一下:不在构造函数中做太多逻辑相关的初始化编译器提供的默认构造函数不会对变量进行初始化如果定义了其他构造函数编译器不再提供需要编码者自行提供默认构造函数为避免隐式转换需将单参数构造函数声明为explicit为避免拷贝构造函数、赋值操作的滥用和编译器自动生成可目前声明其为private且无需实现仅在作为数据集合时使用struct组合>实现继承>接口继承>私有继承子类重载的虚函数也要声明virtual关键字虽然编译器允许不这样做避免使用多重继承使用时除一个基类含有实现外其他基类均为纯接口接口类类名以Interface为后缀除提供带实现的虚析构函数、静态成员函数外其他均为纯虚函数不定义非静态数据成员不提供构造函数提供的话声明为protected为降低复杂性尽量不重载操作符模板、标准类中使用时提供文档说明存取函数一般内联在头文件中声明次序:public>protected>private函数体尽量短小、紧凑功能单一。GoogleC编程风格指南(四)Google特有的风情Google有很多自己实现的使C代码更加健壮的技巧、功能以及有异于别处的C的使用方式。智能指针(SmartPointers)如果确实需要使用智能指针的话scopedptr完全可以胜任。在非常特殊的情况下例如对STL容器中对象你应该只使用std::tr::sharedptr任何情况下都不要使用autoptr。“智能”指针看上去是指针其实是附加了语义的对象。以scopedptr为例scopedptr被销毁时删除了它所指向的对象。sharedptr也是如此而且sharedptr实现了引用计数(referencecounting)从而只有当它所指向的最后一个对象被销毁时指针才会被删除。一般来说我们倾向于设计对象隶属明确的代码最明确的对象隶属是根本不使用指针直接将对象作为一个域(field)或局部变量使用。另一种极端是引用计数指针不属于任何对象这样设计的问题是容易导致循环引用或其他导致对象无法删除的诡异条件而且在每一次拷贝或赋值时连原子操作都会很慢。虽然不推荐这么做但有些时候引用计数指针是最简单有效的解决方案。译者注:看来Google所谓的不同之处在于尽量避免使用智能指针:D使用时也尽量局部化并且安全第一。其他C特性引用参数(ReferenceArguments)所以按引用传递的参数必须加上const。定义:在C语言中如果函数需要修改变量的值形参(parameter)必须为指针如intfoo(int*pval)。在C中函数还可以声明引用形参:intfoo(intval)。优点:定义形参为引用避免了像(*pval)这样丑陋的代码像拷贝构造函数这样的应用也是必需的而且不像指针那样不接受空指针。缺点:容易引起误解因为引用在语法上是值却拥有指针的语义。结论:函数形参表中所有引用必须是const:voidFoo(conststringin,string*out)事实上这是一个硬性约定:输入参数为值或常数引用输出参数为指针输入参数可以是常数指针但不能使用非常数引用形参。在强调参数不是拷贝而来在对象生命期内必须一直存在时可以使用常数指针最好将这些在注释中详细说明。bindnd和memfun等STL适配器不接受引用形参这种情况下也必须以指针形参声明函数。函数重载(FunctionOverloading)仅在输入参数类型不同、功能相同时使用重载函数(含构造函数)不要使用函数重载模仿缺省函数参数。定义:可以定义一个函数参数类型为conststring并定义其重载函数类型为constchar*。classMyClass{public:voidAnalyze(conststringtext)voidAnalyze(constchar*text,sizettextlen)}优点:通过重载不同参数的同名函数令代码更加直观模板化代码需要重载同时为访问者带来便利。缺点:限制使用重载的一个原因是在特定调用处很难确定到底调用的是哪个函数另一个原因是当派生类只重载函数的部分变量会令很多人对继承语义产生困惑。此外在阅读库的客户端代码时因缺省函数参数造成不必要的费解。结论:如果你想重载一个函数考虑让函数名包含参数信息例如使用AppendString()、AppendInt()而不是Append()。缺省参数(DefaultArguments)禁止使用缺省函数参数。优点:经常用到一个函数带有大量缺省值偶尔会重写一下这些值缺省参数为很少涉及的例外情况提供了少定义一些函数的方便。缺点:大家经常会通过查看现有代码确定如何使用API缺省参数使得复制粘贴以前的代码难以呈现所有参数当缺省参数不适用于新代码时可能导致重大问题。结论:所有参数必须明确指定强制程序员考虑API和传入的各参数值避免使用可能不为程序员所知的缺省参数。变长数组和alloca(VariableLengthArraysandalloca())禁止使用变长数组和alloca()。优点:变长数组具有浑然天成的语法变长数组和alloca()也都很高效。缺点:变长数组和alloca()不是标准C的组成部分更重要的是它们在堆栈(stack)上根据数据分配大小可能导致难以发现的内存泄漏:“在我的机器上运行的好好的到了产品中却莫名其妙的挂掉了”。结论:使用安全的分配器(allocator)如scopedptrscopedarray。友元(Friends)允许合理使用友元类及友元函数。通常将友元定义在同一文件下避免读者跑到其他文件中查找其对某个类私有成员的使用。经常用到友元的一个地方是将FooBuilder声明为Foo的友元FooBuilder以便可以正确构造Foo的内部状态而无需将该状态暴露出来。某些情况下将一个单元测试用类声明为待测类的友元会很方便。友元延伸了(但没有打破)类的封装界线当你希望只允许另一个类访问某个成员时使用友元通常比将其声明为public要好得多。当然大多数类应该只提供公共成员与其交互。异常(Exceptions)不要使用C异常。优点:)异常允许上层应用决定如何处理在底层嵌套函数中发生的“不可能发生”的失败不像出错代码的记录那么模糊费解)应用于其他很多现代语言中引入异常使得C与Python、Java及其他与C相近的语言更加兼容)许多C第三方库使用异常关闭异常将导致难以与之结合)异常是解决构造函数失败的唯一方案虽然可以通过工厂函数(factoryfunction)或Init()方法模拟异常但他们分别需要堆分配或新的“非法”状态)在测试框架(testingframework)中异常确实很好用。缺点:)在现有函数中添加throw语句时必须检查所有调用处即使它们至少具有基本的异常安全保护或者程序正常结束永远不可能捕获该异常。例如:iff()callsg()callsh()h抛出被f捕获的异常g就要当心了避免没有完全清理)通俗一点说异常会导致程序控制流(controlflow)通过查看代码无法确定:函数有可能在不确定的地方返回从而导致代码管理和调试困难当然你可以通过规定何时何地如何使用异常来最小化的降低开销却给开发人员带来掌握这些规定的负担)异常安全需要RAII和不同编码实践。轻松、正确编写异常安全代码需要大量支撑。允许使用异常)加入异常使二进制执行代码体积变大增加了编译时长(或许影响不大)还可能增加地址空间压力)异常的实用性可能会刺激开发人员在不恰当的时候抛出异常或者在不安全的地方从异常中恢复例如非法用户输入可能导致抛出异常。如果允许使用异常会使得这样一篇编程风格指南长出很多(译者注这个理由有点牵强:()!结论:从表面上看使用异常利大于弊尤其是在新项目中然而对于现有代码引入异常会牵连到所有依赖代码。如果允许异常在新项目中使用在跟以前没有使用异常的代码整合时也是一个麻烦。因为Google现有的大多数C代码都没有异常处理引入带有异常处理的新代码相当困难。鉴于Google现有代码不接受异常在现有代码中使用异常比在新项目中使用的代价多少要大一点迁移过程会比较慢也容易出错。我们也不相信异常的有效替代方案如错误代码、断言等都是严重负担。我们并不是基于哲学或道德层面反对使用异常而是在实践的基础上。因为我们希望使用Google上的开源项目但项目中使用异常会为此带来不便因为我们也建议不要在Google上的开源项目中使用异常如果我们需要把这些项目推倒重来显然不太现实。对于Windows代码来说这一点有个例外(等到最后一篇吧:D)。译者注:对于异常处理显然不是短短几句话能够说清楚的以构造函数为例很多C书籍上都提到当构造失败时只有异常可以处理Google禁止使用异常这一点仅仅是为了自身的方便说大了无非是基于软件管理成本上实际使用中还是自己决定。运行时类型识别(RunTimeTypeInformation,RTTI)我们禁止使用RTTI。定义:RTTI允许程序员在运行时识别C类对象的类型。优点:RTTI在某些单元测试中非常有用如在进行工厂类测试时用于检验一个新建对象是否为期望的动态类型。除测试外极少用到。缺点:运行时识别类型意味著设计本身有问题如果你需要在运行期间确定一个对象的类型这通常说明你需要重新考虑你的类的设计。结论:除单元测试外不要使用RTTI如果你发现需要所写代码因对象类型不同而动作各异的话考虑换一种方式识别对象类型。虚函数可以实现随子类类型不同而执行不同代码工作都是交给对象本身去完成。如果工作在对象之外的代码中完成考虑双重分发方案如Visitor模式可以方便的在对象本身之外确定类的类型。如果你认为上面的方法你掌握不了可以使用RTTI但务必请三思不要去手工实现

用户评论(0)

0/200

精彩专题

上传我的资料

每篇奖励 +2积分

资料评价:

/10
仅支持在线阅读

意见
反馈

立即扫码关注

爱问共享资料微信公众号

返回
顶部