标签:
1. 头文件:
通常每一个.cc文件都有一个对应的.h文件。也有一些常见例外,如单元测试代码和只包含main()函数的.cc文件。
#define保护:所有头文件都应该使用#define 防止头文件被多重包含, 命名格式当是:<PROJECT>_<PATH>_<FILE>_H_
为保证唯一性,头文件的命名应该依据所在项目源代码树的全路径。例如,项目foo中的头文件foo/src/bar/baz.h可按如下方式保护:
当一个头文件被包含的同时也引入了新的依赖,一旦该头文件被修改,代码就会被重新编译。如果这个头文件又包含了其他头文件,这些头文件的任何改变都将导致所有包含了该头文件的代码被重新编译。因此,我们倾向于减少包含头文件,尤其是在头文件中包含头文件。
使用前置声明可以显著减少需要包含的头文件数量。举例说明:如果头文件中用到类 File,但不需要访问File类的声明,头文件中只需前置声明class File;而无须 #include "file/base/file.h"
不允许访问类的定义的前提下,我们在一个头文件中能对类Foo做哪些操作?
(1)、我们可以将数据成员类型声明为Foo *或Foo &.
(2)、我们可以将函数参数/返回值的类型声明为Foo(但不能定义实现).
(3)、我们可以将静态数据成员的类型声明为Foo,因为静态数据成员的定义在类定义之外.
反之,如果你的类是Foo的子类,或者含有类型为Foo的非静态数据成员,则必须包含Foo所在的头文件.
有时,使用指针成员(如果是scoped_ptr更好)替代对象成员的确是明智之选.然而,这会降低代码可读性及执行效率,因此如果仅仅为了少包含头文件还是不要这么做的好.
当然.cc文件无论如何都需要所使用类的定义部分,自然也就会包含若干头文件.
内联函数:只有当函数只有10行甚至更少时才将其定义为内联函数。
定义:当函数被声明为内联函数之后,编译器会将其内联展开,而不是按通常的函数调用机制进行调用。
优点:当函数体比较小的时候,内联该函数可以令目标代码更加高效。对于存取函数以及其它函数体比较短,性能关键的函数,鼓励使用内联。
缺点:滥用内联将导致程序变慢。内联可能使目标代码量或增或减,这取决于内联函数的大小。内联非常短小的存取函数通常会减少代码大小,但内联一个相当大的函数将戏剧性的增加代码大小。现代处理器由于更好的利用了指令缓存,小巧的代码往往执行更快。
结论:一个较为合理的经验准则是,不要内联超过10行的函数。谨慎对待析构函数,析构函数往往比其表面看起来要更长,因为有隐含的成员和基类析构函数被调用!
另一个实用的经验准则:内联那些包含循环或switch语句的函数常常是得不偿失(除非在大多数情况下,这些循环或switch语句从不被执行)。
有些函数即使声明为内联的也不一定会被编译器内联,这点很重要:比如虚函数和递归函数就不会被正常内联。通常,递归函数不应该声明成内联函数。(YuleFox注:递归调用堆栈的展开并不像循环那么简单,比如递归层数在编译时可能是未知的,大多数编译器都不支持内联递归函数)。虚函数内联的主要原因则是想把它的函数体放在类定义内,为了图个方便,抑或是当作文档描述其行为,比如精短的存取函数。
-inl.h文件:复杂的内联函数的定义,应放在后缀名为-inl.h的头文件中。
内联函数的定义必须放在头文件中,编译器才能在调用点内联展开定义。然而,实现代码理论上应该放在.cc 文件中,我们不希望.h文件中有太多实现代码,除非在可读性和性能上有明显优势。
如果内联函数的定义比较短小,逻辑比较简单,实现代码放在.h文件里没有任何问题。比如,存取函数的实现理所当然都应该放在类定义内。出于编写者和调用者的方便,较复杂的内联函数也可以放到.h文件中,如果你觉得这样会使头文件显得笨重,也可以把它萃取到单独的-inl.h中。这样把实现和类定义分离开来,当需要时包含对应的-inl.h即可。
-inl.h文件还可用于函数模板的定义。从而增强模板定义的可读性。
别忘了-inl.h和其他头文件一样,也需要#define保护。
函数参数的顺序:定义函数时,参数顺序依次为:输入参数,然后是输出参数。
C/C++函数参数分为输入参数,输出参数,和输入/输出参数三种。输入参数一般传值或传const引用,输出参数或输入/输出参数则是非const指针。对参数排序时, 将只输入的参数放在所有输出参数之前。尤其是不要仅仅因为是新加的参数,就把它放在最后;即使是新加的只输入参数也要放在输出参数之前。
这条规则并不需要严格遵守。输入/输出两用参数(通常是类/结构体变量)把事情变得复杂,为保持和相关函数的一致性,你有时不得不有所变通。
#include的路径及顺序:使用标准的头文件包含顺序可增强可读性, 避免隐藏依赖:C库,C++库,其他库的.h,本项目内的.h.
项目内头文件应按照项目源代码目录树结构排列,避免使用UNIX特殊的快捷目录.(当前目录)或 ..(上级目录)。例如,google-awesome-project/src/base/logging.h 应该按如下方式包含:
(1)、dir2/foo2.h(优先位置)
(2)、C系统文件
(3)、C++系统文件
(4)、其他库的.h文件
(5)、本项目内.h文件
这种排序方式可有效减少隐藏依赖。我们希望每一个头文件都是可被独立编译的(yospaly译注:即该头文件本身已包含所有必要的显式依赖),最简单的方法是将其作为第一个.h文件#include进对应的.cc.
dir/foo.cc和 dir2/foo2.h通常位于同一目录下(如 base/basictypes_unittest.cc 和 base/basictypes.h),但也可以放在不同目录下。
按字母顺序对头文件包含进行二次排序是不错的主意(yospaly译注:之前已经按头文件类别排过序了)。
举例来说,google-awesome-project/src/foo/internal/fooserver.cc的包含次序如下:
(1)、避免多重包含是学编程时最基本的要求;
(2)、前置声明是为了降低编译依赖,防止修改一个头文件引发多米诺效应;
(3)、内联函数的合理使用可提高代码执行效率;
(4)、-inl.h可提高代码可读性(一般用不到吧:D);
(5)、标准化函数参数顺序可以提高可读性和易维护性(对函数参数的堆栈空间有轻微影响,我以前大多是相同类型放在一起);
(6)、包含文件的名称使用.和..虽然方便却易混乱,使用比较完整的项目路径看上去很清晰,很条理,包含文件的次序除了美观之外,最重要的是可以减少隐藏依赖, 使每个头文件在“最需要编译”(对应源文件处 :D)的地方编译,有人提出库文件放在最后,这样出错先是项目内的文件,头文件都放在对应源文件的最前面,这一点足以保证内部错误的及时发现了。
2. 作用域:
名字空间:鼓励在.cc文件内使用匿名名字空间.使用具名的名字空间时,其名称可基于项目名或相对路径.不要使用using 关键字.
定义:名字空间将全局作用域细分为独立的,具名的作用域,可有效防止全局作用域的命名冲突.
优点:(1)、虽然类已经提供了(可嵌套的)命名轴线(YuleFox注:将命名分割在不同类的作用域内),名字空间在这基础上又封装了一层.(2)、举例来说,两个不同项目的全局作用域都有一个类Foo,这样在编译或运行时造成冲突.如果每个项目将代码置于不同名字空间中,project1::Foo和project2::Foo作为不同符号自然不会冲突.
缺点:(1)、名字空间具有迷惑性,因为它们和类一样提供了额外的(可嵌套的)命名轴线.(2)、在头文件中使用匿名空间导致违背C++的唯一定义原则(One Definition Rule (ODR)).
结论:根据下文将要提到的策略合理使用命名空间.
在.cc文件中,允许甚至鼓励使用匿名名字空间,以避免运行时的命名冲突:
不要在.h文件中使用匿名名字空间.
具名的名字空间使用方式如下:
(1)、用名字空间把文件包含,gflags的声明/定义,以及类的前置声明以外的整个源文件封装起来,以区别于其它名字空间:
(3)、最好不要使用"using"关键字, 以保证名字空间下的所有名称都可以正常使用.
在一个类内部定义另一个类;嵌套类也被称为成员类(memberclass).
缺点:嵌套类只能在外围类的内部做前置声明.因此,任何使用了Foo::Bar*指针的头文件不得不包含类Foo的整个声明.
结论:不要将嵌套类定义成公有,除非它们是接口的一部分,比如,嵌套类含有某些方法的一组选项.
非成员函数、静态成员函数和全局函数:使用静态成员函数或名字空间内的非成员函数,尽量不要用裸的全局函数.
优点:某些情况下,非成员函数和静态成员函数是非常有用的,将非成员函数放在名字空间内可避免污染全局作用域.
缺点:将非成员函数和静态成员函数作为新类的成员或许更有意义,当它们需要访问外部资源或具有重要的依赖关系时更是如此.
结论:(1)、有时,把函数的定义同类的实例脱钩是有益的,甚至是必要的.这样的函数可以被定义成静态成员,或是非成员函数.非成员函数不应依赖于外部变量,应尽量置于某个名字空间内.相比单纯为了封装若干不共享任何静态数据的静态成员函数而创建类,不如使用命名空间.(2)、定义在同一编译单元的函数,被其他编译单元直接调用可能会引入不必要的耦合和链接时依赖;静态成员函数对此尤其敏感.可以考虑提取到新类中,或者将函数置于独立库的名字空间内.(3)、如果你必须定义非成员函数,又只是在.cc文件中使用它,可使用匿名名字空间或static链接关键字(如static int Foo() {...})限定其作用域.
局部变量:将函数变量尽可能置于最小作用域内,并在变量声明时进行初始化.
C++允许在函数的任何位置声明变量.我们提倡在尽可能小的作用域中声明变量,离第一次使用越近越好.这使得代码浏览者更容易定位变量声明的位置,了解变量的类型和初始值.特别是,使用初始化的方式替代声明再赋值, 比如:
静态生存周期的对象,包括全局变量,静态变量,静态类成员变量,以及函数静态变量,都必须是原生数据类型(POD:Plain Old Data):只能是int,char,float,和void,以及POD类型的数组/结构体/指针.永远不要使用函数返回值初始化静态变量;不要在多线程代码中使用非const的静态变量.
不幸的是,静态变量的构造函数,析构函数以及初始化操作的调用顺序在C++标准中未明确定义,甚至每次编译构建都有可能会发生变化,从而导致难以发现的bug.比如,结束程序时,某个静态变量已经被析构了,但代码还在跑--其它线程很可能--试图访问该变量,直接导致崩溃.
所以,我们只允许POD类型的静态变量.本条规则完全禁止vector(使用C数组替代),string(使用constchar*), 及其它以任意方式包含或指向类实例的东东,成为静态变量.出于同样的理由,我们不允许用函数返回值来初始化静态变量.
如果你确实需要一个class类型的静态或全局变量,可以考虑在main()函数或pthread_once()内初始化一个你永远不会回收的指针.
yospaly译注:上文提及的静态变量泛指静态生存周期的对象,包括:全局变量,静态变量,静态类成员变量,以及函数静态变量.
译者(YuleFox)笔记:
(1)、cc中的匿名名字空间可避免命名冲突,限定作用域,避免直接使用using关键字污染命名空间;
(2)、嵌套类符合局部使用原则,只是不能在其他头文件中前置声明,尽量不要public;
(3)、尽量不用全局函数和全局变量,考虑作用域和命名空间限制,尽量单独形成编译单元;
(4)、多线程中的全局变量(含静态成员变量)不要使用class类型(含STL容器),避免不明确行为导致的bug.
(5)、作用域的使用, 除了考虑名称污染, 可读性之外, 主要是为降低耦合, 提高编译/执行效率.
3. 类:
类是C++中代码的基本单元.显然,它们被广泛使用.本节列举了在写一个类时的主要注意事项.
构造函数的职责:构造函数中只进行那些没什么意义的(trivial,YuleFox注:简单初始化对于程序执行没有实际的逻辑意义, 因为成员变量“有意义”的值大多不在构造函数中确定)初始化,可能的话,使用Init()方法集中初始化有意义的 (non-trivial)数据.
定义:在构造函数体中进行初始化操作.
优点:排版方便,无需担心类是否已经初始化.
缺点:在构造函数中执行操作引起的问题有:
(1)、构造函数中很难上报错误, 不能使用异常.
(2)、操作失败会造成对象初始化失败,进入不确定状态.
(3)、如果在构造函数内调用了自身的虚函数,这类调用是不会重定向到子类的虚函数实现.即使当前没有子类化实现,将来仍是隐患.
(4)、如果有人创建该类型的全局变量(虽然违背了上节提到的规则),构造函数将先main()一步被调用,有可能破坏构造函数中暗含的假设条件.例如, gflag 尚未初始化.
结论:如果对象需要进行有意义的(non-trivial)初始化,考虑使用明确的Init()方法并(或)增加一个成员标记用于指示对象是否已经初始化成功.
默认构造函数:如果一个类定义了若干成员变量又没有其它构造函数,必须定义一个默认构造函数.否则编译器将自动生产一个很糟糕的默认构造函数.
定义:new一个不带参数的类对象时,会调用这个类的默认构造函数.用new[]创建数组时,默认构造函数则总是被调用.
优点:默认将结构体初始化为“无效”值,使调试更方便.
缺点:对代码编写者来说,这是多余的工作.
结论:(1)、如果类中定义了成员变量,而且没有提供其它构造函数,你必须定义一个(不带参数的)默认构造函数.把对象的内部状态初始化成一致/有效的值无疑是更合理的方式.(2)、这么做的原因是:如果你没有提供其它构造函数,又没有定义默认构造函数,编译器将为你自动生成一个.编译器生成的构造函数并不会对对象进行合理的初始化.(3)、如果你定义的类继承现有类, 而你又没有增加新的成员变量, 则不需要为新类定义默认构造函数.
显式构造函数:对单个参数的构造函数使用C++关键字explicit.
定义:通常,如果构造函数只有一个参数,可看成是一种隐式转换.打个比方,如果你定义了Foo::Foo(stringname),接着把一个字符串传给一个以Foo对象为参数的函数,构造函数Foo::Foo(string name)将被调用,并将该字符串转换为一个Foo的临时对象传给调用函数.看上去很方便,但如果你并不希望如此通过转换生成一个新对象的话,麻烦也随之而来.为避免构造函数被调用造成隐式转换,可以将其声明为explicit.
优点:避免不合时宜的变换.
缺点:无
结论:(1)、所有单参数构造函数都必须是显式的.在类定义中,将关键字explicit加到单参数构造函数前: explicit Foo(string name);(2)、例外:在极少数情况下,拷贝构造函数可以不声明成explicit.作为其它类的透明包装器的类也是特例之一。类似的例外情况应在注释中明确说明.
拷贝构造函数:仅在代码中需要拷贝一个类对象的时候使用拷贝构造函数;大部分情况下都不需要,此时应使用 DISALLOW_COPY_AND_ASSIGN.
定义:拷贝构造函数在复制一个对象到新建对象时被调用(特别是对象传值时).
优点:拷贝构造函数使得拷贝对象更加容易.STL容器要求所有内容可拷贝,可赋值.
缺点:C++中的隐式对象拷贝是很多性能问题和bug的根源.拷贝构造函数降低了代码可读性,相比传引用,跟踪传值的对象更加困难,对象修改的地方变得难以捉摸.
结论:大部分类并不需要可拷贝,也不需要一个拷贝构造函数或重载赋值运算符.不幸的是,如果你不主动声明它们,编译器会为你自动生成,而且是public的.
可以考虑在类的private:中添加拷贝构造函数和赋值操作的空实现,只有声明,没有定义.由于这些空函数声明为private,当其他代码试图使用它们的时候,编译器将报错.方便起见,我们可以使用 DISALLOW_COPY_AND_ASSIGN 宏:
注意在operator=中检测自我赋值的情况(yospaly注:即operator=接收的参数是该对象本身).
为了能作为STL容器的值,你可能有使类可拷贝的冲动.在大多数类似的情况下,真正该做的是把对象的指针放到STL容器中.可以考虑使用std::tr1::shared_ptr.
结构体VS.类:仅当只有数据时使用struct,其它一概使用class.
在C++中struct和class关键字几乎含义一样.我们为这两个关键字添加我们自己的语义理解,以便为定义的数据类型选择合适的关键字.
struct用来定义包含数据的被动式对象,也可以包含相关的常量,但除了存取数据成员之外,没有别的函数功能.并且存取功能是通过直接访问位域(field),而非函数调用.除了构造函数,析构函数,Initialize(),Reset(),Validate()外,不能提供其它功能的函数.
如果需要更多的函数功能,class更适合.如果拿不准,就用class.
为了和STL保持一致,对于仿函数(functors)和特性(traits)可以不用class而是使用struct.
注意:类和结构体的成员变量使用不同的命名规则.
继承:使用组合(composition,YuleFox注:这一点也是GoF在<<DesignPatterns>>里反复强调的)常常比使用继承更合理.如果使用继承的话,定义为public继承.
定义:当子类继承基类时,子类包含了父基类所有数据及操作的定义.C++实践中,继承主要用于两种场合:实现继承(implementation inheritance),子类继承父类的实现代码;接口继承(interface inheritance),子类仅继承父类的方法名称.
优点:实现继承通过原封不动的复用基类代码减少了代码量.由于继承是在编译时声明,程序员和编译器都可以理解相应操作并发现错误.从编程角度而言,接口继承是用来强制类输出特定的API.在类没有实现API中某个必须的方法时,编译器同样会发现并报告错误.
缺点:对于实现继承,由于子类的实现代码散布在父类和子类间之间,要理解其实现变得更加困难.子类不能重写父类的非虚函数,当然也就不能修改其实现.基类也可能定义了一些数据成员,还要区分基类的实际布局.
结论:(1)、所有继承必须是public的.如果你想使用私有继承,你应该替换成把基类的实例作为成员对象的方式.(2)、不要过度使用实现继承.组合常常更合适一些.尽量做到只在“是一个”(“is-a”,YuleFox注:其他“has-a”情况下请使用组合)的情况下使用继承:如果Bar的确“是一种”Foo,Bar才能继承Foo.(3)、必要的话,析构函数声明为virtual.如果你的类有虚函数,则析构函数也应该为虚函数.注意数据成员在任何情况下都必须是私有的.(4)、当重载一个虚函数,在衍生类中把它明确的声明为virtual.理论依据:如果省略virtual关键字,代码阅读者不得不检查所有父类,以判断该函数是否是虚函数.
多重继承:真正需要用到多重实现继承的情况少之又少.只在以下情况我们才允许多重继承:最多只有一个基类是非抽象类;其它基类都是以Interface为后缀的纯接口类.
定义:多重继承允许子类拥有多个基类.要将作为纯接口的基类和具有实现的基类区别开来.
优点:相比单继承(见继承),多重实现继承可以复用更多的代码.
缺点:真正需要用到多重实现继承的情况少之又少.多重实现继承看上去是不错的解决方案,但你通常也可以找到一个更明确,更清晰的不同解决方案.
结论:只有当所有父类除第一个外都是纯接口类时,才允许使用多重继承.为确保它们是纯接口,这些类必须以Interface为后缀.
接口:接口是指满足特定条件的类,这些类以Interface为后缀(不强制).
定义:当一个类满足以下要求时,称之为纯接口:(1)、只有纯虚函数(“=0”)和静态函数(除了下文提到的析构函数).(2)、没有非静态数据成员.(3)、没有定义任何构造函数.如果有,也不能带有参数,并且必须为protected.(4)、如果它是一个子类,也只能从满足上述条件并以Interface为后缀的类继承.
接口类不能被直接实例化,因为它声明了纯虚函数.为确保接口类的所有实现可被正确销毁,必须为之声明虚析构函数(作为上述第1条规则的特例,析构函数不能是纯虚函数).具体细节可参考Stroustrup的TheC++ProgrammingLanguage,3rdedition第12.4节.
优点:以Interface为后缀可以提醒其他人不要为该接口类增加函数实现或非静态数据成员.这一点对于多重继承尤其重要.另外,对于Java程序员来说,接口的概念已是深入人心.
缺点:Interface后缀增加了类名长度,为阅读和理解带来不便.同时,接口特性作为实现细节不应暴露给用户.
结论:只有在满足上述需要时,类才以Interface结尾,但反过来,满足上述需要的类未必一定以Interface结尾.
运算符重载:除少数特定环境外,不要重载运算符.
定义:一个类可以定义诸如+和/等运算符,使其可以像内建类型一样直接操作.
优点:使代码看上去更加直观,类表现的和内建类型(如int)行为一致.重载运算符使Equals(),Add()等函数名黯然失色.为了使一些模板函数正确工作,你可能必须定义操作符.
缺点:虽然操作符重载令代码更加直观,但也有一些不足:(1)、混淆视听,让你误以为一些耗时的操作和操作内建类型一样轻巧.(2)、更难定位重载运算符的调用点,查找Equals()显然比对应的==调用点要容易的多.(3)、有的运算符可以对指针进行操作,容易导致bug.Foo+4做的是一件事,而&Foo+4可能做的是完全不同的另一件事.对于二者,编译器都不会报错,使其很难调试;(4)、重载还有令你吃惊的副作用.比如,重载了operator&的类不能被前置声明.
结论:
(1)、一般不要重载运算符.尤其是赋值操作(operator=)比较诡异,应避免重载.如果需要的话,可以定义类似Equals(),CopyFrom()等函数.
(2)、然而,极少数情况下可能需要重载运算符以便与模板或“标准”C++类互操作(如operator<<(ostream&,constT&)).只有被证明是完全合理的才能重载,但你还是要尽可能避免这样做.尤其是不要仅仅为了在STL容器中用作键值就重载operator==或operator<;相反,你应该在声明容器的时候,创建相等判断和大小比较的仿函数类型.
有些STL算法确实需要重载operator==时,你可以这么做,记得别忘了在文档中说明原因.
存取控制:将所有数据成员声明为private,并根据需要提供相应的存取函数.例如,某个名为foo_的变量,其取值函数是foo().还可能需要一个赋值函数set_foo().
一般在头文件中把存取函数定义成内联函数.
声明顺序:在类中使用特定的声明顺序:public:在private:之前,成员函数在数据成员(变量)前。
类的访问控制区段的声明顺序依次为:public:,protected:,private:.如果某区段没内容,可以不声明.
每个区段内的声明通常按以下顺序:
(1)、typedefs和枚举;
(2)、常量;
(3)、构造函数;
(4)、析构函数;
(5)、成员函数,含静态成员函数;
(6)、数据成员,含静态数据成员
宏DISALLOW_COPY_AND_ASSIGN的调用放在private:区段的末尾.它通常是类的最后部分.
.cc文件中函数的定义应尽可能和声明顺序一致.
不要在类定义中内联大型函数.通常,只有那些没有特别意义或性能要求高,并且是比较短小的函数才能被定义为内联函数.
编写简短函数:倾向编写简短,凝练的函数.
我们承认长函数有时是合理的,因此并不硬性限制函数的长度.如果函数超过40行,可以思索一下能不能在不影响程序结构的前提下对其进行分割.
即使一个长函数现在工作的非常好,一旦有人对其修改,有可能出现新的问题.甚至导致难以发现的bug.使函数尽量简短,便于他人阅读和修改代码.
在处理代码时,你可能会发现复杂的长函数.不要害怕修改现有代码:如果证实这些代码使用/调试困难,或者你需要使用其中的一小段代码,考虑将其分割为更加简短并易于管理的若干函数.
译者(YuleFox)笔记:
(1)、不在构造函数中做太多逻辑相关的初始化;
(2)、编译器提供的默认构造函数不会对变量进行初始化,如果定义了其他构造函数,编译器不再提供,需要编码者自行提供默认构造函数;
(3)、为避免隐式转换,需将单参数构造函数声明为explicit;
(4)、为避免拷贝构造函数,赋值操作的滥用和编译器自动生成,可将其声明为private且无需实现;
(5)、仅在作为数据集合时使用struct;
(6)、组合>实现继承>接口继承>私有继承,子类重载的虚函数也要声明virtual关键字,虽然编译器允许不这样做;
(7)、避免使用多重继承,使用时,除一个基类含有实现外,其他基类均为纯接口;
(8)、接口类类名以Interface为后缀,除提供带实现的虚析构函数,静态成员函数外,其他均为纯虚函数,不定义非静态数据成员,不提供构造函数,提供的话,声明为protected;
(9)、为降低复杂性,尽量不重载操作符,模板,标准类中使用时提供文档说明;
(10)、存取函数一般内联在头文件中;
(11)、声明次序:public->protected->private;
(12)、函数体尽量短小,紧凑,功能单一.
4. 来自Google的齐技:
Google用了很多自己实现的技巧/工具使C++代码更加健壮,我们使用C++的方式可能和你在其它地方见到的有所不同.
智能指针:如果确实需要使用智能指针的话,scoped_ptr完全可以胜任.你应该只在非常特定的情况下使用std::tr1::shared_ptr,例如STL容器中的对象.任何情况下都不要使用auto_ptr.
“智能”指针看上去是指针,其实是附加了语义的对象.以scoped_ptr为例,scoped_ptr被销毁时,它会删除所指向的对象.shared_ptr也是如此,并且shared_ptr实现了引用计数,所以最后一个shared_ptr对象析构时,如果检测到引用次数为0,就会销毁所指向的对象.
一般来说,我们倾向于设计对象隶属明确的代码,最明确的对象隶属是根本不使用指针,直接将对象作为一个作用域或局部变量使用.另一种极端做法是,引用计数指针不属于任何对象.这种方法的问题是容易导致循环引用,或者导致某个对象无法删除的诡异状态,而且在每一次拷贝或赋值时连原子操作都会很慢.
虽然不推荐使用引用计数指针,但有些时候它们的确是最简单有效的解决方案.
(YuleFox注:看来,Google所谓的不同之处,在于尽量避免使用智能指针:D,使用时也尽量局部化,并且,安全第一)
cpplint:使用cpplint.py检查风格错误.
cpplint.py是一个用来分析源文件,能检查出多种风格错误的工具.它不并完美,甚至还会漏报和误报,但它仍然是一个非常有用的工具.用行注释//NOLINT可以忽略误报.
某些项目会指导你如何使用他们的项目工具运行cpplint.py.如果你参与的项目没有提供,你可以单独下载cpplint.py.
5. 其它C++特性:
引用参数:所有按引用传递的参数必须加上const.
定义:在C语言中,如果函数需要修改变量的值,参数必须为指针,如intfoo(int*pval).在C++中,函数还可以声明引用参数:intfoo(int &val).
优点:定义引用参数防止出现(*pval)++这样丑陋的代码.像拷贝构造函数这样的应用也是必需的.而且更明确,不接受NULL指针.
缺点:容易引起误解,因为引用在语法上是值变量却拥有指针的语义.
结论:函数参数列表中,所有引用参数都必须是const:
在以下情况你可以把输入参数定义为const指针:你想强调参数不是拷贝而来的,在对象生存周期内必须一直存在;最好同时在注释中详细说明一下.bind2nd和mem_fun等STL适配器不接受引用参数,这种情况下你也必须把函数参数声明成指针类型.
函数重载:仅在输入参数类型不同,功能相同时使用重载函数(含构造函数).不要用函数重载模拟缺省函数参数.
定义:你可以编写一个参数类型为conststring&的函数,然后用另一个参数类型为const char*的函数重载它:
缺点:限制使用重载的一个原因是在某个特定调用点很难确定到底调用的是哪个函数.另一个原因是当派生类只重载了某个函数的部分变体,会令很多人对继承的语义产生困惑.此外在阅读库的用户代码时,可能会因反对使用缺省函数参数造成不必要的费解.
结论:如果你想重载一个函数,考虑让函数名包含参数信息,例如,使用AppendString(),AppendInt()而不是Append().
缺省参数:我们不允许使用缺省函数参数.
优点:多数情况下,你写的函数可能会用到很多的缺省值,但偶尔你也会修改这些缺省值.无须为了这些偶尔情况定义很多的函数,用缺省参数就能很轻松的做到这点.
缺点:大家通常都是通过查看别人的代码来推断如何使用API.用了缺省参数的代码更难维护,从老代码复制粘贴而来的新代码可能只包含部分参数.当缺省参数不适用于新代码时可能会导致重大问题.
结论:我们规定所有参数必须明确指定,迫使程序员理解API和各参数值的意义,避免默默使用他们可能都还没意识到的缺省参数.
变长数组和alloca():我们不允许使用变长数组和alloca().
优点:变长数组具有浑然天成的语法.变长数组和alloca()也都很高效.
缺点:变长数组和alloca()不是标准C++的组成部分.更重要的是,它们根据数据大小动态分配堆栈内存,会引起难以发现的内存越界bugs:“在我的机器上运行的好好的,发布后却莫名其妙的挂掉了”.
结论:使用安全的内存分配器,如scoped_ptr/scoped_array.
友元:我们允许合理的使用友元类及友元函数.
通常友元应该定义在同一文件内,避免代码读者跑到其它文件查找使用该私有成员的类.经常用到友元的一个地方是将FooBuilder声明为Foo的友元,以便FooBuilder正确构造Foo的内部状态,而无需将该状态暴露出来.某些情况下,将一个单元测试类声明成待测类的友元会很方便.
友元扩大了(但没有打破)类的封装边界.某些情况下,相对于将类成员声明为public,使用友元是更好的选择,尤其是如果你只允许另一个类访问该类的私有成员时.当然,大多数类都只应该通过其提供的公有成员进行互操作.
异常:我们不使用C++异常.
优点:(1)、异常允许上层应用决定如何处理在底层嵌套函数中“不可能出现的”失败,不像错误码记录那么含糊又易出错;(2)、很多现代语言都使用异常.引入异常使得C++与Python,Java以及其它C++相近的语言更加兼容.(3)、许多第三方C++库使用异常,禁用异常将导致很难集成这些库.(4)、异常是处理构造函数失败的唯一方法.虽然可以通过工厂函数或Init()方法替代异常,但他们分别需要堆分配或新的“无效”状态;(5)、在测试框架中使用异常确实很方便.
缺点:(1)、在现有函数中添加throw语句时,你必须检查所有调用点.所有调用点得至少有基本的异常安全保护,否则永远捕获不到异常,只好“开心的”接受程序终止的结果.例如,如果f()调用了g(),g()又调用了h(),h抛出的异常被f捕获,g要当心了,很可能会因疏忽而未被妥善清理.(2)、更普遍的情况是,如果使用异常,光凭查看代码是很难评估程序的控制流:函数返回点可能在你意料之外.这会导致代码管理和调试困难.你可以通过规定何时何地如何使用异常来降低开销,但是让开发人员必须掌握并理解这些规定带来的代价更大.(3)、异常安全要求同时采用RAII和不同编程实践.要想轻松编写正确的异常安全代码,需要大量的支撑机制配合.另外,要避免代码读者去理解整个调用结构图,异常安全代码必须把写持久化状态的逻辑部分隔离到“提交”阶段.它在带来好处的同时,还有成本(也许你不得不为了隔离“提交”而整出令人费解的代码).允许使用异常会驱使我们不断为此付出代价,即使我们觉得这很不划算.(4)、启用异常使生成的二进制文件体积变大,延长了编译时间(或许影响不大),还可能增加地址空间压力;(5)、异常的实用性可能会怂恿开发人员在不恰当的时候抛出异常,或者在不安全的地方从异常中恢复.例如,处理非法用户输入时就不应该抛出异常.如果我们要完全列出这些约束,这份风格指南会长出很多!
结论:(1)、从表面上看,使用异常利大于弊,尤其是在新项目中.但是对于现有代码,引入异常会牵连到所有相关代码.如果新项目允许异常向外扩散,在跟以前未使用异常的代码整合时也将是个麻烦.因为Google现有的大多数C++代码都没有异常处理,引入带有异常处理的新代码相当困难.(2)、鉴于Google现有代码不接受异常,在现有代码中使用异常比在新项目中使用的代价多少要大一些.迁移过程比较慢,也容易出错.我们不相信异常的使用有效替代方案,如错误代码,断言等会造成严重负担.(3)、我们并不是基于哲学或道德层面反对使用异常,而是在实践的基础上.我们希望在Google使用我们自己的开源项目,但项目中使用异常会为此带来不便,因此我们也建议不要在Google的开源项目中使用异常.如果我们需要把这些项目推倒重来显然不太现实.
(YuleFox注:对于异常处理,显然不是短短几句话能够说清楚的,以构造函数为例,很多C++书籍上都提到当构造失败时只有异常可以处理,Google禁止使用异常这一点,仅仅是为了自身的方便,说大了,无非是基于软件管理成本上,实际使用中还是自己决定)
运行时类型识别:我们禁止使用RTTI.
定义:RTTI允许程序员在运行时识别C++类对象的类型.
优点:RTTI在某些单元测试中非常有用.比如进行工厂类测试时,用来验证一个新建对象是否为期望的动态类型.除测试外,极少用到.
缺点:在运行时判断类型通常意味着设计问题.如果你需要在运行期间确定一个对象的类型,这通常说明你需要考虑重新设计你的类.
结论:(1)、除单元测试外,不要使用RTTI.如果你发现自己不得不写一些行为逻辑取决于对象类型的代码,考虑换一种方式判断对象类型.(2)、如果要实现根据子类类型来确定执行不同逻辑代码,虚函数无疑更合适.在对象内部就可以处理类型识别问题.(3)、如果要在对象外部的代码中判断类型,考虑使用双重分派方案,如访问者模式.可以方便的在对象本身之外确定类的类型.(4)、如果你认为上面的方法你真的掌握不了,你可以使用RTTI,但务必请三思:-).不要试图手工实现一个貌似RTTI的替代方案,我们反对使用RTTI的理由,同样适用于那些在类型继承体系上使用类型标签的替代方案.
类型转换:使用C++的类型转换,如static_cast<>().不要使用int y = (int)x或int y =int(x)等转换方式;
定义:C++采用了有别于C的类型转换机制,对转换操作进行归类.
优点:C语言的类型转换问题在于模棱两可的操作;有时是在做强制转换(如(int)3.5),有时是在做类型转换(如(int)"hello").另外,C++的类型转换在查找时更醒目.
缺点:恶心的语法.
结论:不要使用C风格类型转换.而应该使用C++风格.(1)、用static_cast替代C风格的值转换,或某个类指针需要明确的向上转换为父类指针时.(2)、用const_cast去掉const限定符.(3)、用reinterpret_cast指针类型和整型或其它指针之间进行不安全的相互转换.仅在你对所做一切了然于心时使用.(4)、dynamic_cast测试代码以外不要使用.除非是单元测试,如果你需要在运行时确定类型信息,说明有设计缺陷.
流:只在记录日志时使用流.
定义:流用来替代printf()和scanf().
优点:有了流,在打印时不需要关心对象的类型.不用担心格式化字符串与参数列表不匹配(虽然在gcc中使用printf也不存在这个问题).流的构造和析构函数会自动打开和关闭对应的文件.
缺点:流使得pread()等功能函数很难执行.如果不使用printf风格的格式化字符串,某些格式化操作(尤其是常用的格式字符串%.*s)用流处理性能是很低的.流不支持字符串操作符重新排序(%1s),而这一点对于软件国际化很有用.
结论:不要使用流,除非是日志接口需要.使用printf之类的代替.
使用流还有很多利弊,但代码一致性胜过一切.不要在代码中使用流.
拓展讨论:对这一条规则存在一些争论,这儿给出点深层次原因.回想一下唯一性原则(OnlyOne Way):我们希望在任何时候都只使用一种确定的I/O类型,使代码在所有I/O处都保持一致.因此,我们不希望用户来决定是使用流还是printf + read/write.相反,我们应该决定到底用哪一种方式.把日志作为特例是因为日志是一个非常独特的应用,还有一些是历史原因.
流的支持者们主张流是不二之选,但观点并不是那么清晰有力.他们指出的流的每个优势也都是其劣势.流最大的优势是在输出时不需要关心打印对象的类型.这是一个亮点.同时,也是一个不足:你很容易用错类型,而编译器不会报警.使用流时容易造成的这类错误:
有人说printf的格式化丑陋不堪,易读性差,但流也好不到哪儿去.看看下面两段代码吧,实现相同的功能,哪个更清晰?
每一种方式都是各有利弊,“没有最好,只有更适合”.简单性原则告诫我们必须从中选择其一,最后大多数决定采用printf + read/write.
前置自增和自减:对于迭代器和其他模板对象使用前缀形式(++i)的自增,自减运算符.
定义:对于变量在自增(++i或i++)或自减(--i或i--)后表达式的值又没有没用到的情况下,需要确定到底是使用前置还是后置的自增(自减).
优点:不考虑返回值的话,前置自增(++i)通常要比后置自增(i++)效率更高.因为后置自增(或自减)需要对表达式的值i进行一次拷贝.如果i是迭代器或其他非数值类型,拷贝的代价是比较大的.既然两种自增方式实现的功能一样,为什么不总是使用前置自增呢?
缺点:在C开发中,当表达式的值未被使用时,传统的做法是使用后置自增,特别是在for循环中.有些人觉得后置自增更加易懂,因为这很像自然语言,主语(i)在谓语动词(++)前.
结论:对简单数值(非对象),两种都无所谓.对迭代器和模板类型,使用前置自增(自减).
const的使用:我们强烈建议你在任何可能的情况下都要使用const.
定义:在声明的变量或参数前加上关键字const用于指明变量值不可被篡改(如const int foo).为类中的函数加上const限定符表明该函数不会修改类成员变量的状态(如class Foo{int Bar(char c) const;};).
优点:大家更容易理解如何使用变量.编译器可以更好地进行类型检测,相应地,也能生成更好的代码.人们对编写正确的代码更加自信,因为他们知道所调用的函数被限定了能或不能修改变量值.即使是在无锁的多线程编程中,人们也知道什么样的函数是安全的.
缺点:const是入侵性的:如果你向一个函数传入const变量,函数原型声明中也必须对应const参数(否则变量需要const_cast类型转换),在调用库函数时显得尤其麻烦.
结论:const变量,数据成员,函数和参数为编译时类型检测增加了一层保障;便于尽早发现错误.因此,我们强烈建议在任何可能的情况下使用const:(1)、如果函数不会修改传入的引用或指针类型参数,该参数应声明为const.(2)、尽可能将函数声明为const.访问函数应该总是const.其他不会修改任何数据成员,未调用非const函数,不会返回数据成员非const指针或引用的函数也应该声明成const.(3)、如果数据成员在对象构造之后不再发生变化,可将其定义为const.(4)、然而,也不要发了疯似的使用const.像const int * const * const x;就有些过了,虽然它非常精确的描述了常量x.关注真正有帮助意义的信息:前面的例子写成constint **x就够了.
关键字mutable可以使用,但是在多线程中是不安全的,使用时首先要考虑线程安全.
const的位置:有人喜欢int const*foo形式,不喜欢const int *foo,他们认为前者更一致因此可读性也更好:遵循了const总位于其描述的对象之后的原则.但是一致性原则不适用于此,“不要过度使用”的声明可以取消大部分你原本想保持的一致性.将const放在前面才更易读,因为在自然语言中形容词(const)是在名词(int)之前.这是说,我们提倡但不强制const在前.但要保持代码的一致性!(yospaly注:也就是不要在一些地方把const写在类型前面,在其他地方又写在后面,确定一种写法,然后保持一致.)
整型:C++内建整型中,仅使用int.如果程序中需要不同大小的变量,可以使用<stdint.h>中长度精确的整型,如int16_t.
定义:C++没有指定整型的大小.通常人们假定short是16位,int是32位,long是32位,long long是64位.
优点:保持声明统一.
缺点:C++中整型大小因编译器和体系结构的不同而不同.
结论:(1)、<stdint.h>定义了int16_t,uint32_t,int64_t等整型,在需要确保整型大小时可以使用它们代替short,unsigned long long等.在C整型中,只使用int.在合适的情况下,推荐使用标准类型如size_t和ptrdiff_t.(2)、如果已知整数不会太大,我们常常会使用int,如循环计数.在类似的情况下使用原生类型int.你可以认为int至少为32位,但不要认为它会多于32位.如果需要64位整型,用int64_t或uint64_t.(3)、对于大整数,使用int64_t.(4)、不要使用uint32_t等无符号整型,除非你是在表示一个位组而不是一个数值,或是你需要定义二进制补码溢出.尤其是不要为了指出数值永不会为负,而使用无符号类型.相反,你应该使用断言来保护数据.
关于无符号整数:有些人,包括一些教科书作者,推荐使用无符号类型表示非负数.这种做法试图达到自我文档化.但是,在C语言中,这一优点被由其导致的bug所淹没.看看下面的例子:
因此,使用断言来指出变量为非负数,而不是使用无符号型!
64位下的可移植性:代码应该对64位和32位系统友好.处理打印,比较,结构体对齐时应切记:
对于某些类型,printf()的指示符在32位和64位系统上可移植性不是很好.C99标准定义了一些可移植的格式化指示符.不幸的是,MSVC7.1并非全部支持,而且标准中也有所遗漏,所以有时我们不得不自己定义一个丑陋的版本(头文件inttypes.h仿标准风格):
类型 不要使用 使用 备注
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*宏会被编译器扩展为独立字符串.因此如果使用非常量的格式化字符串,需要将宏的值而不是宏名插入格式中.使用PRI*宏同样可以在%后包含长度指示符.例如,printf("x = %30"PRIuS"\n", x)在32位Linux上将被展开为printf("x = %30" "u""\n", x),编译器当成printf("x = %30u\n", x)处理(yospaly注:这在MSVC6.0上行不通,VC6编译器不会自动把引号间隔的多个字符串连接一个长字符串).
记住sizeof(void*) != sizeof(int).如果需要一个指针大小的整数要用intptr_t.
你要非常小心的对待结构体对齐,尤其是要持久化到磁盘上的结构体(yospaly注:持久化--将数据按字节流顺序保存在磁盘文件或数据库中).在64位系统中,任何含有int64_t/uint64_t成员的类/结构体,缺省都以8字节在结尾对齐.如果32位和64位代码要共用持久化的结构体,需要确保两种体系结构下的结构体对齐一致.大多数编译器都允许调整结构体对齐.gcc中可使用__attribute__((packed)).MSVC则提供了#pragmapack()和__declspec(align())(YuleFox注,解决方案的项目属性里也可以直接设置).
创建64位常量时使用LL或ULL作为后缀,如:
预处理宏:使用宏时要非常谨慎,尽量以内联函数,枚举和常量代替之.
宏意味着你和编译器看到的代码是不同的.这可能会导致异常行为,尤其因为宏具有全局作用域.
值得庆幸的是,C++中,宏不像在C中那么必不可少.以往用宏展开性能关键的代码,现在可以用内联函数替代.用宏表示常量可被const变量代替.用宏“缩写”长变量名可被引用代替.用宏进行条件编译...这个,千万别这么做,会令测试更加痛苦(#define防止头文件重包含当然是个特例).
宏可以做一些其他技术无法实现的事情,在一些代码库(尤其是底层库中)可以看到宏的某些特性(如用#字符串化,用##连接等等).但在使用前,仔细考虑一下能不能不使用宏达到同样的目的.
下面给出的用法模式可以避免使用宏带来的问题;如果你要宏,尽可能遵守:(1)、不要在.h文件中定义宏.(2)、在马上要使用时才进行#define,使用后要立即#undef.(3)、不要只是对已经存在的宏使用#undef,选择一个不会冲突的名称;(4)、不要试图使用展开后会导致C++构造不稳定的宏,不然也至少要附上文档说明其行为.
0和NULL:整数用0,实数用0.0,指针用NULL,字符(串)用‘\0‘.
整数用0,实数用0.0,这一点是毫无争议的.
对于指针(地址值),到底是用0还是NULL,Bjarne Stroustrup建议使用最原始的0.我们建议使用看上去像是指针的NULL,事实上一些C++编译器(如gcc4.1.0)对NULL进行了特殊的定义,可以给出有用的警告信息,尤其是sizeof(NULL)和sizeof(0)不相等的情况.
字符(串)用‘\0‘,不仅类型正确而且可读性好.
sizeof:尽可能用sizeof(varname)代替sizeof(type).
使用sizeof(varname)是因为当代码中变量类型改变时会自动更新.某些情况下sizeof(type)或许有意义,但还是要尽量避免,因为它会导致变量类型改变后不能同步.
定义:Boost库集是一个广受欢迎,经过同行鉴定,免费开源的C++库集.
优点:Boost代码质量普遍较高,可移植性好,填补了C++标准库很多空白,如型别的特性,更完善的绑定器,更好的智能指针,同时还提供了TR1(标准库扩展)的实现.
缺点:某些Boost库提倡的编程实践可读性差,比如元编程和其他高级模板技术,以及过度“函数化”的编程风格.
结论:为了向阅读和维护代码的人员提供更好的可读性,我们只允许使用Boost一部分经认可的特性子集.目前允许使用以下库:
(1)、CompressedPair:boost/compressed_pair.hpp
(2)、PointerContainer:boost/ptr_container(序列化除外)
(3)、Array:boost/array.hpp
(4)、TheBoostGraphLibrary(BGL):boost/graph(序列化除外)
(5)、PropertyMap:boost/property_map.hpp
(6)、Iterator中处理迭代器定义的部分:boost/iterator/iterator_adaptor.hpp,boost/iterator/iterator_facade.hpp,以及boost/function_output_iterator.hpp
我们正在积极考虑增加其它Boost特性,所以列表中的规则将不断变化.
6. 命名约定:
最重要的一致性规则是命名管理.命名风格快速获知名字代表是什么东东:类型?变量?函数?常量?宏...?甚至不需要去查找类型声明.我们大脑中的模式匹配引擎可以非常可靠的处理这些命名规则.
命名规则具有一定随意性,但相比按个人喜好命名,一致性更重,所以不管你怎么想,规则总归是规则.
通用命名规则:函数命名,变量命名,文件命名应具备描述性;不要过度缩写.类型和变量应该是名词,函数名可以用“命令性”动词.
如何命名:尽可能给出描述性的名称.不要节约行空间,让别人很快理解你的代码更重要.好的命名风格:
函数名通常是指令性的(确切的说它们应该是命令),如OpenFile(),set_num_errors().取值函数是个特例(在函数命名处详细阐述),函数名和它要取值的变量同名.
缩写:除非该缩写在其它地方都非常普遍,否则不要使用.例如:
可接受的文件命名:
不要使用已经存在于/usr/include下的文件名(yospaly注:即编译器搜索系统头文件的路径),如db.h.
通常应尽量让文件名更加明确.http_server_logs.h就比logs.h要好.定义类时文件名一般成对出现,如foo_bar.h和foo_bar.cc,对应于类FooBar.
内联函数必须放在.h文件中.如果内联函数比较短,就直接放在.h中.如果代码比较长,可以放到以-inl.h结尾的文件中.对于包含大量内联代码的类,可以使用三个文件:
所有类型命名——类,结构体,类型定义(typedef),枚举——均使用相同约定.例如:
全局变量:对全局变量没有特别要求,少用就好,但如果你要用,可以用g_或其它标志作为前缀,以便更好的区分局部变量.
常量命名:在名称前加k:kDaysInAWeek.
所有编译时常量,无论是局部的,全局的还是类中的,和其他变量稍微区别一下.k后接大写字母开头的单词::
const int kDaysInAWeek = 7;
函数命名:常规函数使用大小写混合,取值和设值函数则要求与变量名匹配:MyExcitingFunction(),MyExcitingMethod(),my_exciting_member_variable(),set_my_exciting_member_variable().
常规函数:函数名的每个单词首字母大写,没有下划线:
名字空间命名:名字空间用小写字母命名,并基于项目名称和目录结构:google_awesome_project.
关于名字空间的讨论和如何命名,参考名字空间一节.
枚举命名:枚举的命名应当和常量或宏一致:kEnumName或是ENUM_NAME.
单独的枚举值应该优先采用常量的命名方式.但宏方式的命名也可以接受.枚举名UrlTableErrors(以及AlternateUrlTableErrors)是类型,所以要用大小写混合的方式.
宏命名:你并不打算使用宏,对吧?如果你一定要用,像这样命名:MY_MACRO_THAT_SCARES_SMALL_CHILDREN.
参考预处理宏<preprocessor-macros>;通常不应该使用宏.如果不得不用,其命名像枚举命名一样全部大写,使用下划线:
bigopen():函数名,参照open()的形式
uint:typedef
bigpos:struct或class,参照pos的形式
sparse_hash_map:STL相似实体;参照STL命名约定
LONGLONG_MAX:常量,如同INT_MAX
7. 注释:
注释虽然写起来很痛苦,但对保证代码可读性至关重要.下面的规则描述了如何注释以及在哪儿注释.当然也要记住:注释固然很重要,但最好的代码本身应该是自文档化.有意义的类型名和变量名,要远胜过要用注释解释的含糊不清的名字.
你写的注释是给代码读者看的:下一个需要理解你的代码的人.慷慨些吧,下一个人可能就是你!
注释风格:使用//或/* */,统一就好.
//或/**/都可以;但//更常用.要在如何注释及注释风格上确保统一.
文件注释:在每一个文件开头加入版权公告,然后是文件内容描述.
法律公告和作者信息:每个文件都应该包含以下项,依次是:
(1)、版权声明(比如,Copyright2008GoogleInc.)
(2)、许可证.为项目选择合适的许可证版本(比如,Apache2.0,BSD,LGPL,GPL)
(3)、作者:标识文件的原始作者.
如果你对原始作者的文件做了重大修改,将你的信息添加到作者信息里.这样当其他人对该文件有疑问时可以知道该联系谁.
文件内容:紧接着版权许可和作者信息之后,每个文件都要用注释描述文件内容.
通常,.h文件要对所声明的类的功能和用法作简单说明..cc文件通常包含了更多的实现细节或算法技巧讨论,如果你感觉这些实现细节或算法技巧讨论对于理解.h文件有帮助,可以该注释挪到.h,并在.cc中指出文档在.h.
不要简单的在.h和.cc间复制注释.这种偏离了注释的实际意义.
类注释:每个类的定义都要附带一份注释,描述类的功能和用法.
如果类有任何同步前提,文档说明之.如果该类的实例可被多线程访问,要特别注意文档说明多线程环境下相关的规则和常量使用.
函数注释:函数声明处注释描述函数功能;定义处描述函数实现.
函数声明:注释位于声明之前,对函数功能及用法进行描述.注释使用叙述式(“Opensthefile”)而非指令式(“Openthefile”);注释只是为了描述函数,而不是命令函数做什么.通常,注释不会描述函数如何工作.那是函数定义部分的事情.
函数声明处注释的内容:
(1)、函数的输入输出.
(2)、对类成员函数而言:函数调用期间对象是否需要保持引用参数,是否会释放这些参数.
(3)、如果函数分配了空间,需要由调用者释放.
(4)、参数是否可以为NULL.
(5)、是否存在函数使用上的性能隐患.
(6)、如果函数是可重入的,其同步前提是什么?
举例如下:
函数定义:每个函数定义时要用注释说明函数功能和实现要点.比如说说你用的编程技巧,实现的大致步骤,或解释如此实现的理由,为什么前半部分要加锁而后半部分不需要.
不要从.h文件或其他地方的函数声明处直接复制注释.简要重述函数功能是可以的,但注释重点要放在如何实现上.
变量注释:通常变量名本身足以很好说明变量用途.某些情况下,也需要额外的注释说明.
类数据成员:每个类数据成员(也叫实例变量或成员变量)都应该用注释说明用途.如果变量可以接受NULL或-1等警戒值,须加以说明.比如:
代码前注释:巧妙或复杂的代码段前要加注释.比如:
如果你需要连续进行多行注释,可以使之对齐获得更好的可读性:
向函数传入NULL,布尔值或整数时,要注释说明含义,或使用常量让代码望文知意.例如,对比:
Warning:
warning:
注释的通常写法是包含正确大小写和结尾句号的完整语句.短一点的注释(如代码行尾注释)可以随意点,依然要注意风格的一致性.完整的语句可读性更好,也可以说明该注释是完整的,而不是一些不成熟的想法.
虽然被别人指出该用分号时却用了逗号多少有些尴尬,但清晰易读的代码还是很重要的.正确的标点,拼写和语法对此会有所帮助.
TODO注释:对那些临时的,短期的解决方案,或已经够好但仍不完美的代码使用TODO注释.
TODO注释要使用全大写的字符串TODO,在随后的圆括号里写上你的大名,邮件地址,或其它身份标识.冒号是可选的.主要目的是让添加注释的人(也是可以请求提供更多细节的人)可根据规范的TODO格式进行查找.添加TODO注释并不意味着你要自己来修正.
译者(YuleFox)笔记:
(1)、关于注释风格,很多C++的coders更喜欢行注释,Ccoders或许对块注释依然情有独钟,或者在文件头大段大段的注释时使用块注释;
(2)、文件注释可以炫耀你的成就,也是为了捅了篓子别人可以找你;
(3)、注释要言简意赅,不要拖沓冗余,复杂的东西简单化和简单的东西复杂化都是要被鄙视的;
(4)、对于Chinesecoders来说,用英文注释还是用中文注释,itisaproblem,但不管怎样,注释是为了让别人看懂,难道是为了炫耀编程语言之外的你的母语或外语水平吗;
(5)、注释不要太乱,适当的缩进才会让人乐意看.但也没有必要规定注释从第几列开始(我自己写代码的时候总喜欢这样),UNIX/LINUX下还可以约定是使用tab还是space,个人倾向于space;
(6)、TODO很不错,有时候,注释确实是为了标记一些未完成的或完成的不尽如人意的地方,这样一搜索,就知道还有哪些活要干,日志都省了.
8. 格式:
代码风格和格式确实比较随意,但一个项目中所有人遵循同一风格是非常容易的.个体未必同意下述每一处格式规则,但整个项目服从统一的编程风格是很重要的,只有这样才能让所有人能很轻松的阅读和理解代码.
另外,我们写了一个emacs配置文件来帮助你正确的格式化代码.
行长度:每一行代码字符数不超过80.
我们也认识到这条规则是有争议的,但很多已有代码都已经遵照这一规则,我们感觉一致性更重要.
优点:提倡该原则的人主张强迫他们调整编辑器窗口大小很野蛮.很多人同时并排开几个代码窗口,根本没有多余空间拉伸窗口.大家都把窗口最大尺寸加以限定,并且80列宽是传统标准.为什么要改变呢?
缺点:反对该原则的人则认为更宽的代码行更易阅读.80列的限制是上个世纪60年代的大型机的古板缺陷;现代设备具有更宽的显示屏,很轻松的可以显示更多代码.
结论:80个字符是最大值.
特例:
(1)、如果一行注释包含了超过80字符的命令或URL,出于复制粘贴的方便允许该行超过80字符.
(2)、包含长路径的#include语句可以超出80列.但应该尽量避免.
(3)、头文件保护可以无视该原则.
非ASCII字符:尽量不使用非ASCII字符,使用时必须使用UTF-8编码.
即使是英文,也不应将用户界面的文本硬编码到源代码中,因此非ASCII字符要少用.特殊情况下可以适当包含此类字符.如,代码分析外部数据文件时,可以适当硬编码数据文件中作为分隔符的非ASCII字符串;更常见的是(不需要本地化的)单元测试代码可能包含非ASCII字符串.此类情况下,应使用UTF-8编码,因为很多工具都可以理解和处理UTF-8编码.十六进制编码也可以,能增强可读性的情况下尤其鼓励——比如"\xEF\xBB\xBF"在Unicode中是零宽度无间断的间隔符号,如果不用十六进制直接放在UTF-8格式的源文件中,是看不到的.(yospaly注:"\xEF\xBB\xBF"通常用作UTF-8 withBOM编码标记).
空格还是制表位:只使用空格,每次缩进2个空格.
我们使用空格缩进.不要在代码中使用制符表.你应该设置编辑器将制符表转为空格.
函数声明与定义:返回类型和函数名在同一行,参数也尽量放在同一行.
函数看上去像这样:
(1)、返回值总是和函数名在同一行;
(2)、左圆括号总是和函数名在同一行;
(3)、函数名和左圆括号间没有空格;
(4)、圆括号与参数间没有空格;
(5)、左大括号总在最后一个参数同一行的末尾处;
(6)、右大括号总是单独位于函数最后一行;
(7)、右圆括号和左大括号间总是有一个空格;
(8)、函数声明和实现处的所有形参名称必须保持一致;
(9)、所有形参应尽可能对齐;
(10)、缺省缩进为2个空格;
(11)、换行后的参数保持4个空格的缩进;
如果函数声明成const,关键字const应与最后一个参数位于同一行:=
函数调用遵循如下形式:
对基本条件语句有两种可以接受的格式.一种在圆括号和条件之间有空格,另一种没有.
最常见的是没有空格的格式.哪种都可以,但保持一致性.如果你是在修改一个文件,参考当前已有格式.如果是写新的代码,参考目录下或项目中其它文件.还在徘徊的话,就不要加空格了.
switch语句中的case块可以使用大括号也可以不用,取决于你的个人喜好.如果用的话,要按照下文所述的方法.
如果有不满足case条件的枚举值,switch应该总是包含一个default匹配(如果有输入值没有case去处理,编译器将报警).如果default应该永远执行不到,简单的加条assert:
下面是指针和引用表达式的正确使用范例:
(1)、在访问成员时,句点或箭头前后没有空格.
(2)、指针操作符*或&后没有空格.
在声明指针变量或参数时,星号与类型或变量名紧挨都可以:
布尔表达式:如果一个布尔表达式超过标准行宽,断行方式要统一一下.
下例中,逻辑与(&&)操作符总位于行尾:
函数返回值:return表达式中不要用圆括号包围.
函数返回时不要使用圆括号:
在二者中做出选择;下面的方式都是正确的:
即使预处理指令位于缩进代码块中,指令也应从行首开始.
类声明(对类注释不了解的话,参考类注释)的基本格式如下:
(1)、所有基类名应在80列限制下尽量与子类名放在同一行.
(2)、关键词public:,protected:,private:要缩进1个空格.
(3)、除第一个关键词(一般是public)外,其他关键词前要空一行.如果类比较小的话也可以不空.
(4)、这些关键词后不要保留空行.
(5)、public放在最前面,然后是protected,最后是private.
(6)、关于声明顺序的规则请参考声明顺序一节.
初始化列表:构造函数初始化列表放在同一行或按四格缩进并排几行.
下面两种初始化列表方式都可以接受:
名字空间不要增加额外的缩进层次,例如:
常规:
循环和条件语句:
这不仅仅是规则而是原则问题了:不在万不得已,不要使用空行.尤其是:两个函数定义之间的空行不要超过2行,函数体首尾不要留空行,函数体中也不要随意添加空行.
基本原则是:同一屏可以显示的代码越多,越容易理解程序的控制流.当然,过于密集的代码块和过于疏松的代码块同样难看,取决于你的判断.但通常是垂直留白越少越好.
Warning:函数首尾不要有空行
(1)、对于代码格式,因人,系统而异各有优缺点,但同一个项目中遵循同一标准还是有必要的;
(2)、行宽原则上不超过80列,把22寸的显示屏都占完,怎么也说不过去;
(3)、尽量不使用非ASCII字符,如果使用的话,参考UTF-8格式(尤其是UNIX/Linux下,Windows下可以考虑宽字符),尽量不将字符串常量耦合到代码中,比如独立出资源文件,这不仅仅是风格问题了;
(4)、UNIX/Linux下无条件使用空格,MSVC的话使用Tab也无可厚非;
(5)、函数参数,逻辑条件,初始化列表:要么所有参数和函数名放在同一行,要么所有参数并排分行;
(6)、除函数定义的左大括号可以置于行首外,包括函数/类/结构体/枚举声明,各种语句的左大括号置于行尾,所有右大括号独立成行;
(7)、./->操作符前后不留空格,*/&不要前后都留,一个就可,靠左靠右依各人喜好;
(8)、预处理指令/命名空间不使用额外缩进,类/结构体/枚举/函数/语句使用缩进;
(9)、初始化用=还是()依个人喜好,统一就好;
(10)、return不要加();
(11)、水平/垂直留白不要滥用,怎么易读怎么来.
(12)、关于UNIX/Linux风格为什么要把左大括号置于行尾(.cc文件的函数实现处,左大括号位于行首),我的理解是代码看上去比较简约,想想行首除了函数体被一对大括号封在一起之外,只有右大括号的代码看上去确实也舒服;Windows风格将左大括号置于行首的优点是匹配情况一目了然.
9. 规则特例:
前面说明的编程习惯基本都是强制性的. 但所有优秀的规则都允许例外, 这里就是探讨这些特例.
现有不合规范的代码:对于现有不符合既定编程风格的代码可以网开一面.
当你修改使用其他风格的代码时,为了与代码原有风格保持一致可以不使用本指南约定.如果不放心可以与代码原作者或现在的负责人员商讨,记住,一致性包括原有的一致性.
Windows代码:Windows程序员有自己的编程习惯,主要源于Windows头文件和其它Microsoft代码.我们希望任何人都可以顺利读懂你的代码,所以针对所有平台的C++编程只给出一个单独的指南.
如果你习惯使用Windows编码风格,这儿有必要重申一下某些你可能会忘记的指南:
(1)、不要使用匈牙利命名法(比如把整型变量命名成iNum).使用Google命名约定,包括对源文件使用.cc扩展名.
(2)、Windows定义了很多原生类型的同义词(YuleFox注:这一点,我也很反感),如DWORD,HANDLE等等.在调用Windows API时这是完全可以接受甚至鼓励的.但还是尽量使用原有的C++类型,例如,使用const TCHAR*而不是LPCTSTR.
(3)、使用Microsoft Visual C++进行编译时,将警告级别设置为3或更高,并将所有warnings当作errors处理.
(4)、不要使用#pragma once;而应该使用Google的头文件保护规则.头文件保护的路径应该相对于项目根目录(yospaly注:如#ifndef SRC_DIR_BAR_H_,参考#define保护一节).
(5)、除非万不得已,不要使用任何非标准的扩展,如#pragma和__declspec.允许使用__declspec(dllimport)和__declspec(dllexport);但你必须通过宏来使用,比如DLLIMPORT和DLLEXPORT,这样其他人在分享使用这些代码时很容易就去掉这些扩展.
在Windows上,只有很少的一些情况下,我们可以偶尔违反规则:
(1)、通常我们禁止使用多重继承,但在使用COM和ATL/WTL类时可以使用多重继承.为了实现COM或ATL/WTL类/接口,你可能不得不使用多重实现继承.
(2)、虽然代码中不应该使用异常,但是在ATL和部分STL(包括VisualC++的STL)中异常被广泛使用.使用ATL时,应定义_ATL_NO_EXCEPTIONS以禁用异常.你要研究一下是否能够禁用STL的异常,如果无法禁用,启用编译器异常也可以.(注意这只是为了编译STL,自己代码里仍然不要含异常处理.)
(3)、通常为了利用头文件预编译,每个每个源文件的开头都会包含一个名为StdAfx.h或precompile.h的文件.为了使代码方便与其他项目共享,避免显式包含此文件(precompile.cc),使用/FI编译器选项以自动包含.
(4)、资源头文件通常命名为resource.h,且只包含宏的,不需要遵守本风格指南.
10. 结束语
运用常识和判断力,并保持一致.
编辑代码时,花点时间看看项目中的其它代码,并熟悉其风格.如果其它代码中if语句使用空格,那么你也要使用.如果其中的注释用星号(*)围成一个盒子状,你同样要这么做.
风格指南的重点在于提供一个通用的编程规范,这样大家可以把精力集中在实现内容而不是表现形式上.我们展示了全局的风格规范,但局部风格也很重要,如果你在一个文件中新加的代码和原有代码风格相去甚远,这就破坏了文件本身的整体美观,也影响阅读,所以要尽量避免.
标签:
原文地址:http://www.cnblogs.com/yyxt/p/5223080.html