GoogleC++编程风格指南,Google

code 10
C++编程风格指南 Release Apr07,2017
0. 4.45 BenjyWeinberger,CraigSilverstein,GregoryEitzmann,MarkMentovai,TashanaLandray YuleFox,Yang.Y,acgtyrant,lilinsanity •GoogleStyleGuide•Google开源项目风格指南-中文版 0.1 Google经常会发布一些开源项目,意味着会接受来自其他代码贡献者的代码.但是如果代码贡献者的编程风格与Google的不一致,会给代码阅读者和其他代码提交者造成不小的困扰.Google因此发布了这份自己的编程风格指南,使所有提交代码的人都能获知Google的编程风格. 翻译初衷:规则的作用就是避免混乱.但规则本身一定要权威,有说服力,并且是理性的.我们所见过的大部分编程规范,其内容或不够严谨,或阐述过于简单,或带有一定的武断性.Google保持其一贯的严谨精神,5万汉字的指南涉及广泛,论证严密.我们翻译该系列指南的主因也正是其严谨性.严谨意味着指南的价值不仅仅局限于它罗列出的规范,更具参考意义的是它为了列出规范而做的谨慎权衡过程.指南不仅列出你要怎么做,还告诉你为什么要这么做,哪些情况下可以不这么做,以及如何权衡其利弊.其他团队未必要完全遵照指南亦步亦趋,如前面所说,这份指南是Google根据自身实际情况打造的,适用于其主导的开源项目.其他团队可以参照该指南,或从中汲取灵感,建立适合自身实际情况的规范.我们在翻译的过程中,收获颇多.希望本系列指南中文版对你同样能有所帮助.我们翻译时也是尽力保持严谨,但水平所限,bug在所难免.有任何意见或建议,可与我们取得联系.中文版和英文版一样,使用ArtisticLicense/GPL开源许可.中文版修订历史:•2015-08:热心的清华大学同学@lilinsanity完善了「类」章节以及其它一些小章节。
至此,对GoogleCPPStyleGuide4.45的翻译正式竣工。
•2015-074.45:acgtyrant为了学习C++的规范,顺便重新翻译了本C++风格指南,特别是C++11的全新内容。
排版大幅度优化,翻译措辞更地道,添加了新译者笔记。
Google总部C++工程师innocentim,清华大学不愿意透露姓名的唐马儒先生,大阪大学大学院情报科学研究科计算机科学专攻博士farseerfc和其它ArchLinux中文社区众帮了译者不少忙,谢谢他们。
因为C++Primer尚未完全入门,暂时没有翻译「类」章节和其它一些小章节。
•2009-063.133:YuleFox的1.0版已经相当完善,但原版在近一年的时间里,其规范也发生了一些变化. Yang.Y与YuleFox一拍即合,以项目的形式来延续中文版:Google开源项目风格指南-中文版项目.主要变化是同步到3.133最新英文版本,做部分勘误和改善可读性方面的修改,并改进排版效果.Yang.Y重新翻修,YuleFox做后续评审.•2008-071.0:出自YuleFox的Blog,很多地方摘录的也是该版本. 0.2 C++是Google大部分开源项目的主要编程语言.正如每个C++程序员都知道的,C++有很多强大的特性,但这种强大不可避免的导致它走向复杂,使代码更容易产生bug,难以阅读和维护. 本指南的目的是通过详细阐述C++注意事项来驾驭其复杂性.这些规则在保证代码易于管理的同时,也能高效使用C++的语言特性. 风格,亦被称作可读性,也就是指导C++编程的约定.使用术语“风格”有些用词不当,因为这些习惯远不止源代码文件格式化这么简单. 使代码易于管理的方法之一是加强代码一致性.让任何程序员都可以快速读懂你的代码这点非常重要.保持统一编程风格并遵守约定意味着可以很容易根据“模式匹配”规则来推断各种标识符的含义.创建通用,必需的习惯用语和模式可以使代码更容易理解.在一些情况下可能有充分的理由改变某些编程风格,但我们还是应该遵循一致性原则,尽量不这么做. 本指南的另一个观点是C++特性的臃肿.C++是一门包含大量高级特性的庞大语言.某些情况下,我们会限制甚至禁止使用某些特性.这么做是为了保持代码清爽,避免这些特性可能导致的各种问题.指南中列举了这类特性,并解释为什么这些特性被限制使用. Google主导的开源项目均符合本指南的规定.注意:本指南并非C++教程,我们假定读者已经对C++非常熟悉.
1. 通常每一个文件都有一个对应的.h文件.也有一些常见例外,如单元测试代码和只包含main()函数的文件. 正确使用头文件可令代码在可读性、文件大小和性能上大为改观.下面的规则将引导你规避使用头文件时的各种陷阱. 1.1.Self-contained Tip:头文件应该能够自给自足(self-contained,也就是可以作为第一个头文件被引入),以.h结尾。
至于用来插入文本的文件,说到底它们并不是头文件,所以应以.inc结尾。
不允许分离出-inl.h头文件的做法. 所有头文件要能够自给自足。
换言之,用户和重构工具不需要为特别场合而包含额外的头文件。
详言之,一个头文件要有1.2.#define保护,统统包含它所需要的其它头文件,也不要求定义任何特别symbols. 不过有一个例外,即一个文件并不是self-contained的,而是作为文本插入到代码某处。
或者,文件内容实际上是其它头文件的特定平台(platform-specific)扩展部分。
这些文件就要用.inc文件扩展名。
如果.h文件声明了一个模板或内联函数,同时也在该文件加以定义。
凡是有用到这些的文件,就得统统包含该头文件,否则程序可能会在构建中链接失败。
不要把这些定义放到分离的-inl.h文件里(译者注:过去该规范曾提倡把定义放到-inl.h里过)。
有个例外:如果某函数模板为所有相关模板参数显式实例化,或本身就是某类的一个私有成员,那么它就只能定义在实例化该模板的文件里。
1.2.#define Tip:所有头文件都应该使用#define来防止头文件被多重包含,命名格式当是:___H_. 为保证唯一性,头文件的命名应该基于所在项目源代码树的全路径.例如,项目foo中的头文件foo/src/bar/baz.h可按如下方式保护: #ifndefFOO_BAR_BAZ_H_#defineFOO_BAR_BAZ_H_…#endif//FOO_BAR_BAZ_H_ 1.3. Tip:尽可能地避免使用前置声明。
使用#include包含需要的头文件即可。
定义:所谓「前置声明」(forwarddeclaration)是类、函数和模板的纯粹声明,没伴随着其定义.优点:•前置声明能够节省编译时间,多余的#include会迫使编译器展开更多的文件,处理更多的输入。
•前置声明能够节省不必要的重新编译的时间。
#include使代码因为头文件中无关的改动而被重新编译多次。
缺点: •前置声明隐藏了依赖关系,头文件改动时,用户的代码会跳过必要的重新编译过程。
•前置声明可能会被库的后续更改所破坏。
前置声明函数或模板有时会妨碍头文件开发者变动其API. 例如扩大形参类型,加个自带默认参数的模板形参等等。
•前置声明来自命名空间std::的symbol时,其行为未定义。
•很难判断什么时候该用前置声明,什么时候该用#include。
极端情况下,用前置声明代替 includes甚至都会暗暗地改变代码的含义:如果#include被B和D的前置声明替代,test()就会调用f(void*).*前置声明了不少来自头文件的symbol时,就会比单单一行的include冗长。
*仅仅为了能前置声明而重构代码(比如用指针成员代替对象成员)会使代码变得更慢更复杂.结论:•尽量避免前置声明那些定义在其他项目中的实体.•函数:总是使用#include.•类模板:优先使用#include.至于什么时候包含头文件,参见name-and-order-of-includes。
1.4. Tip:只有当函数只有10行甚至更少时才将其定义为内联函数. 定义:当函数被声明为内联函数之后,编译器会将其内联展开,而不是按通常的函数调用机制进行调用.优点:只要内联的函数体较小,内联该函数可以令目标代码更加高效.对于存取函数以及其它函数体比较短,性能关键的函数,鼓励使用内联.缺点:滥用内联将导致程序变得更慢.内联可能使目标代码量或增或减,这取决于内联函数的大小.内联非常短小的存取函数通常会减少代码大小,但内联一个相当大的函数将戏剧性的增加代码大小.现代处理器由于更好的利用了指令缓存,小巧的代码往往执行更快。
结论:一个较为合理的经验准则是,不要内联超过10行的函数.谨慎对待析构函数,析构函数往往比其表面看起来要更长,因为有隐含的成员和基类析构函数被调用!
另一个实用的经验准则:内联那些包含循环或switch语句的函数常常是得不偿失(除非在大多数情况下,这些循环或switch语句从不被执行).有些函数即使声明为内联的也不一定会被编译器内联,这点很重要;比如虚函数和递归函数就不会被正常内联.通常,递归函数不应该声明成内联函数.(YuleFox注:递归调用堆栈的展开并不像循环那么简单,比如递归层数在编译时可能是未知的,大多数编译器都不支持内联递归函数).虚函数内联的主要原因则是想把它的函数体放在类定义内,为了图个方便,抑或是当作文档描述其行为,比如精短的存取函数. 1.5.#include Tip:使用标准的头文件包含顺序可增强可读性,避免隐藏依赖:相关头文件,C库,C++库,其他库的.h,本项目内的.h. 项目内头文件应按照项目源代码目录树结构排列,避免使用UNIX特殊的快捷目录.(当前目录)或..(上级目录).例如,google-awesome-project/src/base/logging.h应该按如下方式包含: #include"base/logging.h" 又如,dir/的主要作用是实现或测试dir2/foo2.h的功能,中包含头文件的次序如下:1.dir2/foo2.h(优先位置,详情如下)
2.C系统文件
3.C++系统文件
4.其他库的.h文件
5.本项目内.h文件 这种优先的顺序排序保证当dir2/foo2.h遗漏某些必要的库时,dir/或dir/foo_的构建会立刻中止。
因此这一条规则保证维护这些文件的人们首先看到构建中止的消息而不是维护其他包的人们。
dir/和dir2/foo2.h通常位于同一目录下(如base/basictypes_和base/basictypes.h),但也可以放在不同目录下. 按字母顺序对头文件包含进行二次排序是不错的主意。
注意较老的代码可不符合这条规则,要在方便的时候改正它们。
您所依赖的symbols被哪些头文件所定义,您就应该包含(include)哪些头文件,forward-declaration情况除外。
比如您要用到bar.h中的某个symbol,哪怕您所包含的foo.h已经包含了bar.h,也照样得包含bar.h,除非foo.h有明确说明它会自动向您提供bar.h中的symbol.不过,凡是文件所对应的「相关头文件」已经包含的,就不用再重复包含进其文件里面了,就像只包含foo.h就够了,不用再管后者所包含的其它内容。
举例来说,google-awesome-project/src/foo/internal/的包含次序如下: #include"foo/public/fooserver.h"//优先位置 #include#include #include#include #include"base/basictypes.h"#include"mandlineflags.h"#include"foo/public/bar.h" 例外:有时,平台特定(system-specific)代码需要条件编译(conditionalincludes),这些代码可以放到其它includes之后。
当然,您的平台特定代码也要够简练且独立,比如: #include"foo/public/fooserver.h" #include"base/port.h"//ForLANG_CXX11. #ifdefLANG_CXX11#include#endif//LANG_CXX11 (YuleFox)
1.避免多重包含是学编程时最基本的要求;
2.前置声明是为了降低编译依赖,防止修改一个头文件引发多米诺效应;
3.内联函数的合理使用可提高代码执行效率;
4.-inl.h可提高代码可读性(一般用不到吧:D);
5.标准化函数参数顺序可以提高可读性和易维护性(对函数参数的堆栈空间有轻微影响,我以前大多是相同类型放 在一起);
6.包含文件的名称使用.和..虽然方便却易混乱,使用比较完整的项目路径看上去很清晰,很条理,包含文件的 次序除了美观之外,最重要的是可以减少隐藏依赖,使每个头文件在“最需要编译”(对应源文件处:D)的地方编译,有人提出库文件放在最后,这样出错先是项目内的文件,头文件都放在对应源文件的最前面,这一点足以保证内部错误的及时发现了. acgtyrant
1.原来还真有项目用#includes来插入文本,且其文件扩展名.inc看上去也很科学。

2.Google已经不再提倡-inl.h用法。

3.注意,前置声明的类是不完全类型(pletetype),我们只能定义指向该类型的指针或引用,或者声明(但 不能定义)以不完全类型作为参数或者返回类型的函数。
毕竟编译器不知道不完全类型的定义,我们不能创建其类的任何对象,也不能声明成类内部的数据成员。

4.类内部的函数一般会自动内联。
所以某函数一旦不需要内联,其定义就不要再放在头文件里,而是放到对应的文件里。
这样可以保持头文件的类相当精炼,也很好地贯彻了声明与定义分离的原则。

5.在#include中插入空行以分割相关头文件,C库,C++库,其他库的.h和本项目内的.h是个好习惯。

2. 2.1. Tip:鼓励在文件内使用匿名名字空间.使用具名的名字空间时,其名称可基于项目名或相对路径.禁止使用using指示(using-directive)。
禁止使用内联命名空间(inlinenamespace)。
定义:名字空间将全局作用域细分为独立的,具名的作用域,可有效防止全局作用域的命名冲突.优点:虽然类已经提供了(可嵌套的)命名轴线(YuleFox注:将命名分割在不同类的作用域内),名字空间在这基础上又封装了一层.举例来说,两个不同项目的全局作用域都有一个类Foo,这样在编译或运行时造成冲突.如果每个项目将代码置于不同名字空间中,project1::Foo和project2::Foo作为不同符号自然不会冲突.内联命名空间会自动把内部的标识符放到外层作用域,比如: namespaceX{inlinenamespaceY{voidfoo();}} X::Y::foo()与X::foo()彼此可代替。
内联命名空间主要用来保持跨版本的ABI兼容性。
缺点:名字空间具有迷惑性,因为它们和类一样提供了额外的(可嵌套的)命名轴线.命名空间很容易令人迷惑,毕竟它们不再受其声明所在命名空间的限制。
内联命名空间只在大型版本控制里有用。
在头文件中使用匿名空间导致违背C++的唯一定义原则(OneDefinitionRule(ODR)).结论:根据下文将要提到的策略合理使用命名空间. 2.1.1. •在文件中,允许甚至鼓励使用匿名名字空间,以避免运行时的命名冲突: namespace{ //文件中 //名字空间的内容无需缩进enum{kUNUSED,kEOF,kERROR};boolAtEof(){returnpos_==kEOF;} //经常使用的符号//使用本名字空间内的符号EOF }//namespace 然而,与特定类关联的文件作用域声明在该类中被声明为类型,静态数据成员或静态成员函数,而不是匿名名字空间的成员.如上例所示,匿名空间结束时用注释//namespace标识. •不要在.h文件中使用匿名名字空间. 2.1.2. 具名的名字空间使用方式如下:•用名字空间把文件包含,gflags的声明/定义,以及类的前置声明以外的整个源文件封装起来,以区别于其它名字 空间: //.h文件namespacemynamespace{ //所有声明都置于命名空间中//注意不要使用缩进classMyClass{ public:…voidFoo();}; }//namespacemynamespace //文件namespacemynamespace{ //函数定义都置于命名空间中voidMyClass::Foo(){ …} }//namespacemynamespace 通常的文件包含更多,更复杂的细节,比如引用其他名字空间的类等. #include“a.h” DEFINE_bool(someflag,false,“dummyflag”); classC;namespacea{classA;} //全局名字空间中类C的前置声明//a::A的前置声明 namespaceb{ …codeforb… //b中的代码 }//namespaceb •不要在名字空间std内声明任何东西,包括标准库的类前置声明.在std名字空间声明实体会导致不确定的问题,比如不可移植.声明标准库下的实体,需要包含对应的头文件. •最好不要使用using指示,以保证名字空间下的所有名称都可以正常使用. //禁止——污染名字空间usingnamespacefoo; •在文件,.h文件的函数,方法或类中,可以使用using声明。
//允许:文件中//.h文件的话,必须在函数,方法或类的内部使用using::foo::bar; •在文件,.h文件的函数,方法或类中,允许使用名字空间别名. //允许:文件中//.h文件的话,必须在函数,方法或类的内部使用 namespacefbz=::foo::bar::baz; //在.h文件里namespacelibrarian{//以下别名在所有包含了该头文件的文件中生效。
namespacepd_s=::pipeline_diagnostics::sidetable; inlinevoidmy_inline_function(){//namespacealiaslocaltoafunction(ormethod).namespacefbz=::foo::bar::baz;... }}//namespacelibrarian 注意在.h文件的别名对包含了该头文件的所有人可见,所以在公共头文件(在项目外可用)以及它们递归包含的其它头文件里,不要用别名。
毕竟原则上公共API要尽可能地精简。
•禁止用内联命名空间 2.2. Tip:当公有嵌套类作为接口的一部分时,虽然可以直接将他们保持在全局作用域中,但将嵌套类的声明置于2.1.名字空间内是更好的选择. 定义:在一个类内部定义另一个类;嵌套类也被称为成员类(memberclass). classFoo{ private://Bar是嵌套在Foo中的成员类classBar{…}; }; 优点:当嵌套(或成员)类只被外围类使用时非常有用;把它作为外围类作用域内的成员,而不是去污染外部作用域的同名类.嵌套类可以在外围类中做前置声明,然后在文件中定义,这样避免在外围类的声明中定义嵌套类,因为嵌套类的定义通常只与实现相关.缺点:嵌套类只能在外围类的内部做前置声明.因此,任何使用了Foo::Bar*指针的头文件不得不包含类Foo的整个声明.结论:不要将嵌套类定义成公有,除非它们是接口的一部分,比如,嵌套类含有某些方法的一组选项. 2.3. Tip:使用静态成员函数或名字空间内的非成员函数,尽量不要用裸的全局函数. 优点:某些情况下,非成员函数和静态成员函数是非常有用的,将非成员函数放在名字空间内可避免污染全局作用域.缺点:将非成员函数和静态成员函数作为新类的成员或许更有意义,当它们需要访问外部资源或具有重要的依赖关系时更是如此.结论:有时,把函数的定义同类的实例脱钩是有益的,甚至是必要的.这样的函数可以被定义成静态成员,或是非成员函数.非成员函数不应依赖于外部变量,应尽量置于某个名字空间内.相比单纯为了封装若干不共享任何静态数据的静态成员函数而创建类,不如使用2.1.名字空间。
定义在同一编译单元的函数,被其他编译单元直接调用可能会引入不必要的耦合和链接时依赖;静态成员函数对此尤其敏感.可以考虑提取到新类中,或者将函数置于独立库的名字空间内. 如果你必须定义非成员函数,又只是在文件中使用它,可使用匿名namespaces‘或‘‘static‘链接关键字(如staticintFoo(){...})限定其作用域. 2.4. Tip:将函数变量尽可能置于最小作用域内,并在变量声明时进行初始化. C++允许在函数的任何位置声明变量.我们提倡在尽可能小的作用域中声明变量,离第一次使用越近越好.这使得代码浏览者更容易定位变量声明的位置,了解变量的类型和初始值.特别是,应使用初始化的方式替代声明再赋值,比如: inti;i=f();//坏——初始化和声明分离intj=g();//好——初始化时声明 vectorv;v.push_back
(1);//用花括号初始化更好v.push_back
(2); vectorv={1,2};//好——v一开始就初始化注意,GCC可正确实现了for(inti=0;i<10;++i)(i的作用域仅限for循环内),所以其他for循环中可以重新使用i.在if和while等语句中的作用域声明也是正确的,如: while(constchar*p=strchr(str,‘/’))str=p+1; Warning:如果变量是一个对象,每次进入作用域都要调用其构造函数,每次退出作用域都要调用其析构函数. //低效的实现 for(inti=0;i<1000000;++i){ Foof; //构造函数和析构函数分别调用1000000次!
f.DoSomething(i); } 在循环作用域外面声明这类变量要高效的多: Foof; //构造函数和析构函数只调用1次 for(inti=0;i<1000000;++i){ f.DoSomething(i); } 2.5. Tip:禁止使用class类型的静态或全局变量:它们会导致难以发现的bug和不确定的构造和析构函数调用顺序。
不过constexpr变量除外,毕竟它们又不涉及动态初始化或析构。
静态生存周期的对象,即包括了全局变量,静态变量,静态类成员变量和函数静态变量,都必须是原生数据类型(POD:PlainOldData):即int,char和float,以及POD类型的指针、数组和结构体。
静态变量的构造函数、析构函数和初始化的顺序在C++中是不确定的,甚至随着构建变化而变化,导致难以发现的bug.所以除了禁用类类型的全局变量,我们也不允许用函数返回值来初始化POD变量,除非该函数不涉及(比如getenv()或getpid())不涉及任何全局变量。
(函数作用域里的静态变量除外,毕竟它的初始化顺序是有明确定义的,而且只会在指令执行到它的声明那里才会发生。
) 同理,全局和静态变量在程序中断时会被析构,无论所谓中断是从main()返回还是对exit()的调用。
析构顺序正好与构造函数调用的顺序相反。
但既然构造顺序未定义,那么析构顺序当然也就不定了。
比如,在程序结束时某静态变量已经被析构了,但代码还在跑——比如其它线程——并试图访问它且失败;再比如,一个静态string变量也许会在一个引用了前者的其它变量析构之前被析构掉。
改善以上析构问题的办法之一是用quick_exit()来代替exit()并中断程序。
它们的不同之处是前者不会执行任何析构,也不会执行atexit()所绑定的任何handlers.如果您想在执行quick_exit()来中断时执行某handler(比如刷新log),您可以把它绑定到_at_quick_exit().如果您想在exit()和quick_exit()都用上该handler,都绑定上去。
综上所述,我们只允许POD类型的静态变量,即完全禁用vector(使用C数组替代)和string(使用constchar[])。
如果您确实需要一个class类型的静态或全局变量,可以考虑在main()函数或pthread_once()内初始化一个指针且永不回收。
注意只能用raw指针,别用智能指针,毕竟后者的析构函数涉及到上文指出的不定顺序问题。
Note:Yang.Y译注:上文提及的静态变量泛指静态生存周期的对象,包括:全局变量,静态变量,静态类成员变量,以及函数静态变量. (YuleFox) 中的匿名名字空间可避免命名冲突,限定作用域,避免直接使用using关键字污染命名空间;
2.嵌套类符合局部使用原则,只是不能在其他头文件中前置声明,尽量不要public;
3.尽量不用全局函数和全局变量,考虑作用域和命名空间限制,尽量单独形成编译单元;
4.多线程中的全局变量(含静态成员变量)不要使用class类型(含STL容器),避免不明确行为导致的bug.5.作用域的使用,除了考虑名称污染,可读性之外,主要是为降低耦合,提高编译/执行效率. acgtyrant
1.注意「using指示(using-directive)」和「using声明(using-declaration)」的区别。

2.匿名名字空间说白了就是文件作用域,就像Cstatic声明的作用域一样,后者已经被C++标准提倡弃用。

3.局部变量在声明的同时进行显式值初始化,比起隐式初始化再赋值的两步过程要高效,同时也贯彻了计算机体 系结构重要的概念「局部性(locality)」。

4.注意别在循环犯大量构造和析构的低级错误。

3. 类是C++中代码的基本单元.显然,它们被广泛使用.本节列举了在写一个类时的主要注意事项. 3.1. Tip:不要在构造函数中进行复杂的初始化(尤其是那些有可能失败或者需要调用虚函数的初始化). 定义:在构造函数体中进行初始化操作.优点:排版方便,无需担心类是否已经初始化.缺点:在构造函数中执行操作引起的问题有: •构造函数中很难上报错误,不能使用异常.•操作失败会造成对象初始化失败,进入不确定状态.•如果在构造函数内调用了自身的虚函数,这类调用是不会重定向到子类的虚函数实现.即使当前没有 子类化实现,将来仍是隐患.•如果有人创建该类型的全局变量(虽然违背了上节提到的规则),构造函数将先main()一步被调用, 有可能破坏构造函数中暗含的假设条件.例如,gflags尚未初始化.结论:构造函数不得调用虚函数,或尝试报告一个非致命错误.如果对象需要进行有意义的(non-trivial)初始化,考虑使用明确的Init()方法或使用工厂模式. 3.2. Tip:如果类中定义了成员变量,则必须在类中为每个类提供初始化函数或定义一个构造函数.若未声明构造函数,则编译器会生成一个默认的构造函数,这有可能导致某些成员未被初始化或被初始化为不恰当的值. 定义:new一个不带参数的类对象时,会调用这个类的默认构造函数.用new[]创建数组时,默认构造函数则总是被调用.在类成员里面进行初始化是指声明一个成员变量的时候使用一个结构例如int_count=17或者string_name{"abc"}来替代int_count或者string_name这样的形式.优点:用户定义的默认构造函数将在没有提供初始化操作时将对象初始化.这样就保证了对象在被构造之时就处于一个有效且可用的状态,同时保证了对象在被创建时就处于一个显然”不可能”的状态,以此帮助调试.缺点:对代码编写者来说,这是多余的工作.如果一个成员变量在声明时初始化又在构造函数中初始化,有可能造成混乱,因为构造函数中的值会覆盖掉声明中的值. 结论:简单的初始化用类成员初始化完成,尤其是当一个成员变量要在多个构造函数里用相同的方式初始化的时候.如果你的类中有成员变量没有在类里面进行初始化,而且没有提供其它构造函数,你必须定义一个(不带参数的)默认构造函数.把对象的内部状态初始化成一致/有效的值无疑是更合理的方式. 这么做的原因是:如果你没有提供其它构造函数,又没有定义默认构造函数,编译器将为你自动生成一个.编译器生成的构造函数并不会对对象进行合理的初始化.如果你定义的类继承现有类,而你又没有增加新的成员变量,则不需要为新类定义默认构造函数. 3.3. Tip:对单个参数的构造函数使用C++关键字explicit. 定义:通常,如果构造函数只有一个参数,可看成是一种隐式转换.打个比方,如果你定义了Foo::Foo(stringname),接着把一个字符串传给一个以Foo对象为参数的函数,构造函数Foo::Foo(stringname)将被调用,并将该字符串转换为一个Foo的临时对象传给调用函数.看上去很方便,但如果你并不希望如此通过转换生成一个新对象的话,麻烦也随之而来.为避免构造函数被调用造成隐式转换,可以将其声明为explicit.除单参数构造函数外,这一规则也适用于除第一个参数以外的其他参数都具有默认参数的构造函数,例如Foo::Foo(stringname,intid=42).优点:避免不合时宜的变换.缺点:无结论:所有单参数构造函数都必须是显式的.在类定义中,将关键字explicit加到单参数构造函数前:explicitFoo(stringname);例外:在极少数情况下,拷贝构造函数可以不声明成explicit.作为其它类的透明包装器的类也是特例之
一.类似的例外情况应在注释中明确说明.最后,只有std::initializer_list的构造函数可以是非explicit,以允许你的类型结构可以使用列表初始化的方式进行赋值.例如: MyTypem={1,2};MyTypeMakeMyType(){return{1,2};}TakeMyType({1,2}); 3.4. Tip:如果你的类型需要,就让它们支持拷贝/移动.否则,就把隐式产生的拷贝和移动函数禁用. 定义: 可拷贝类型允许对象在初始化时得到来自相同类型的另一对象的值,或在赋值时被赋予相同类型的另一对象的值,同时不改变源对象的值.对于用户定义的类型,拷贝操作一般通过拷贝构造函数与拷贝赋值操作符定义.string类型就是一个可拷贝类型的例子. 可移动类型允许对象在初始化时得到来自相同类型的临时对象的值,或在赋值时被赋予相同类型的临时对象的值(因此所有可拷贝对象也是可移动的).std::unique_ptr就是一个可移动但不可复制的对象的例子.对于用户定义的类型,移动操作一般是通过移动构造函数和移动赋值操作符实现的.拷贝/移动构造函数在某些情况下会被编译器隐式调用.例如,通过传值的方式传递对象.优点:可移动及可拷贝类型的对象可以通过传值的方式进行传递或者返回,这使得API更简单,更安全也更通用.与传指针和引用不同,这样的传递不会造成所有权,生命周期,可变性等方面的混乱,也就没必要在协议中予以明确.这同时也防止了客户端与实现在非作用域内的交互,使得它们更容易被理解与维护.这样的对象可以和需要传值操作的通用API一起使用,例如大多数容器.拷贝/移动构造函数与赋值操作一般来说要比它们的各种替代方案,比如Clone(),CopyFrom()orSwap(),更容易定义,因为它们能通过编译器产生,无论是隐式的还是通过=默认.这种方式很简洁,也保证所有数据成员都会被复制.拷贝与移动构造函数一般也更高效,因为它们不需要堆的分配或者是单独的初始化和赋值步骤,同时,对于类似省略不必要的拷贝这样的优化它们也更加合适.移动操作允许隐式且高效地将源数据转移出右值对象.这有时能让代码风格更加清晰.缺点:许多类型都不需要拷贝,为它们提供拷贝操作会让人迷惑,也显得荒谬而不合理.为基类提供拷贝/赋值操作是有害的,因为在使用它们时会造成对象切割.默认的或者随意的拷贝操作实现可能是不正确的,这往往导致令人困惑并且难以诊断出的错误.拷贝构造函数是隐式调用的,也就是说,这些调用很容易被忽略.这会让人迷惑,尤其是对那些所用的语言约定或强制要求传引用的程序员来说更是如此.同时,这从一定程度上说会鼓励过度拷贝,从而导致性能上的问题.结论:如果需要就让你的类型可拷贝/可移动.作为一个经验法则,如果对于你的用户来说这个拷贝操作不是一眼就能看出来的,那就不要把类型设置为可拷贝.如果让类型可拷贝,一定要同时给出拷贝构造函数和赋值操作的定义.如果让类型可拷贝,同时移动操作的效率高于拷贝操作,那么就把移动的两个操作(移动构造函数和赋值操作)也给出定义.如果类型不可拷贝,但是移动操作的正确性对用户显然可见,那么把这个类型设置为只可移动并定义移动的两个操作.建议通过=default定义拷贝和移动操作.定义非默认的移动操作目前需要异常.时刻记得检测默认操作的正确性.由于存在对象切割的风险,不要为任何有可能有派生类的对象提供赋值操作或者拷贝/移动构造函数(当然也不要继承有这样的成员函数的类).如果你的基类需要可复制属性,请提供一个publicvirtualClone()和一个protected的拷贝构造函数以供派生类实现.如果你的类不需要拷贝/移动操作,请显式地通过=delete或其他手段禁用之. 3.5. Tip:在能够减少重复代码的情况下使用委派和继承构造函数. 定义:委派和继承构造函数是由C++11引进为了减少构造函数重复代码而开发的两种不同的特性.通过特殊的初始化列表语法,委派构造函数允许类的一个构造函数调用其他的构造函数.例如: X::X(conststring&name):name_(name){... } X::X():X(""){} 继承构造函数允许派生类直接调用基类的构造函数,一如继承基类的其他成员函数,而无需重新声明.当基类拥有多个构造函数时这一功能尤其有用.例如: classBase{public:Base();Base(intn);Base(conststring&s);... }; classDerived:publicBase{public:usingBase::Base;//Base'sconstructorsareredeclaredhere. }; 如果派生类的构造函数只是调用基类的构造函数而没有其他行为时,这一功能特别有用.优点:委派和继承构造函数可以减少冗余代码,提高可读性.委派构造函数对Java程序员来说并不陌生.缺点:使用辅助函数可以预估出委派构造函数的行为.如果派生类和基类相比引入了新的成员变量,继承构造函数就会让人迷惑,因为基类并不知道这些新的成员变量的存在.结论:只在能够减少冗余代码,提高可读性的前提下使用委派和继承构造函数.如果派生类有新的成员变量,那么使用继承构造函数时要小心.如果在派生类中对成员变量使用了类内部初始化的话,继承构造函数还是适用的. 3.6. VS. Tip:仅当只有数据时使用struct,其它一概使用class. 说明:在C++中struct和class关键字几乎含义一样.我们为这两个关键字添加我们自己的语义理解,以便未定义的数据类型选择合适的关键字. struct用来定义包含数据的被动式对象,也可以包含相关的常量,但除了存取数据成员之外,没有别的函数功能.并且存取功能是通过直接访问位域,而非函数调用.除了构造函数,析构函数,Initialize(),Reset(),Validate()等类似的函数外,不能提供其它功能的函数.如果需要更多的函数功能,class更适合.如果拿不准,就用class.为了和STL保持一致,对于仿函数和trait特性可以不用class而是使用struct.注意:类和结构体的成员变量使用不同的命名规则. 3.7. Tip:使用组合position,YuleFox注:这一点也是GoF在<>里反复强调的)常常比使用继承更合理.如果使用继承的话,定义为public继承. 定义:当子类继承基类时,子类包含了父基类所有数据及操作的定义.C++实践中,继承主要用于两种场合:实现继承(implementationinheritance),子类继承父类的实现代码;接口继承(interfaceinheritance),子类仅继承父类的方法名称.优点:实现继承通过原封不动的复用基类代码减少了代码量.由于继承是在编译时声明,程序员和编译器都可以理解相应操作并发现错误.从编程角度而言,接口继承是用来强制类输出特定的API.在类没有实现API中某个必须的方法时,编译器同样会发现并报告错误.缺点:对于实现继承,由于子类的实现代码散布在父类和子类间之间,要理解其实现变得更加困难.子类不能重写父类的非虚函数,当然也就不能修改其实现.基类也可能定义了一些数据成员,还要区分基类的实际布局.结论:所有继承必须是public的.如果你想使用私有继承,你应该替换成把基类的实例作为成员对象的方式.不要过度使用实现继承.组合常常更合适一些.尽量做到只在“是一个”(“is-a”,YuleFox注:其他“has-a”情况下请使用组合)的情况下使用继承:如果Bar的确“是一种”Foo,Bar才能继承Foo.必要的话,析构函数声明为virtual.如果你的类有虚函数,则析构函数也应该为虚函数.注意数据成员在任何情况下都必须是私有的.当重载一个虚函数,在衍生类中把它明确的声明为virtual.理论依据:如果省略virtual关键字,代码阅读者不得不检查所有父类,以判断该函数是否是虚函数. 3.8. Tip:真正需要用到多重实现继承的情况少之又少.只在以下情况我们才允许多重继承:最多只有一个基类是非抽象类;其它基类都是以Interface为后缀的纯接口类. 定义:多重继承允许子类拥有多个基类.要将作为纯接口的基类和具有实现的基类区别开来.优点: 相比单继承(见继承),多重实现继承可以复用更多的代码.缺点:真正需要用到多重实现继承的情况少之又少.多重实现继承看上去是不错的解决方案,但你通常也可以找到一个更明确,更清晰的不同解决方案.结论:只有当所有父类除第一个外都是纯接口类时,才允许使用多重继承.为确保它们是纯接口,这些类必须以Interface为后缀. Note:关于该规则,Windows下有个特例. 3.9. Tip:接口是指满足特定条件的类,这些类以Interface为后缀(不强制). 定义:当一个类满足以下要求时,称之为纯接口: •只有纯虚函数(“=0”)和静态函数(除了下文提到的析构函数).•没有非静态数据成员.•没有定义任何构造函数.如果有,也不能带有参数,并且必须为protected.•如果它是一个子类,也只能从满足上述条件并以Interface为后缀的类继承.接口类不能被直接实例化,因为它声明了纯虚函数.为确保接口类的所有实现可被正确销毁,必须为之声明虚析构函数(作为上述第1条规则的特例,析构函数不能是纯虚函数).具体细节可参考Stroustrup的TheC++ProgrammingLanguage,3rdedition第12.4节.优点:以Interface为后缀可以提醒其他人不要为该接口类增加函数实现或非静态数据成员.这一点对于多重继承尤其重要.另外,对于Java程序员来说,接口的概念已是深入人心.缺点:Interface后缀增加了类名长度,为阅读和理解带来不便.同时,接口特性作为实现细节不应暴露给用户.结论:只有在满足上述需要时,类才以Interface结尾,但反过来,满足上述需要的类未必一定以Interface结尾. 3.10. Tip:除少数特定环境外,不要重载运算符. 定义:一个类可以定义诸如+和/等运算符,使其可以像内建类型一样直接操作. 优点:使代码看上去更加直观,类表现的和内建类型(如int)行为一致.重载运算符使Equals(),Add()等函数名黯然失色.为了使一些模板函数正确工作,你可能必须定义操作符.缺点:虽然操作符重载令代码更加直观,但也有一些不足: •混淆视听,让你误以为一些耗时的操作和操作内建类型一样轻巧.•更难定位重载运算符的调用点,查找Equals()显然比对应的==调用点要容易的多.•有的运算符可以对指针进行操作,容易导致bug.Foo+4做的是一件事,而&Foo+4可能做的是完 全不同的另一件事.对于二者,编译器都不会报错,使其很难调试;重载还有令你吃惊的副作用.比如,重载了operator&的类不能被前置声明.结论:一般不要重载运算符.尤其是赋值操作(operator=)比较诡异,应避免重载.如果需要的话,可以定义类似Equals(),CopyFrom()等函数.然而,极少数情况下可能需要重载运算符以便与模板或“标准”C++类互操作(如operator<<(ostream&,constT&)).只有被证明是完全合理的才能重载,但你还是要尽可能避免这样做.尤其是不要仅仅为了在STL容器中用作键值就重载operator==或operator<;相反,你应该在声明容器的时候,创建相等判断和大小比较的仿函数类型.有些STL算法确实需要重载operator==时,你可以这么做,记得别忘了在文档中说明原因.参考拷贝构造函数和函数重载. 3.11. Tip:将所有数据成员声明为private,并根据需要提供相应的存取函数.例如,某个名为foo_的变量,其取值函数是foo().还可能需要一个赋值函数set_foo(). 特例是,静态常量数据成员(一般写做kFoo)不需要是私有成员.一般在头文件中把存取函数定义成内联函数.参考继承和函数命名 3.11. Tip:在类中使用特定的声明顺序:public:在private:之前,成员函数在数据成员(变量)前; 类的访问控制区段的声明顺序依次为:public:,protected:,private:.如果某区段没内容,可以不声明.每个区段内的声明通常按以下顺序:•typedefs和枚举•常量•构造函数•析构函数 •成员函数,含静态成员函数•数据成员,含静态数据成员 友元声明应该放在private区段.如果用宏DISALLOW_COPY_AND_ASSIGN禁用拷贝和赋值,应当将其置于private区段的末尾,也即整个类声明的末尾.参见可拷贝类型和可移动类型. 文件中函数的定义应尽可能和声明顺序一致.不要在类定义中内联大型函数.通常,只有那些没有特别意义或性能要求高,并且是比较短小的函数才能被定义为内联函数.更多细节参考内联函数. 3.12.编 Tip:倾向编写简短,凝练的函数. 我们承认长函数有时是合理的,因此并不硬性限制函数的长度.如果函数超过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++的方式可能和你在其它地方见到的有所不同. 4.1. 指 Tip:动态分配出的对象最好有单一且固定的所有主(onwer),且通过智能指针传递所有权(ownership). 定义:所有权是一种登记/管理动态内存和其它资源的技术。
动态分配出的对象的所有主是一个对象或函数,后者负责确保当前者无用时就自动销毁前者。
所有权有时可以共享,那么就由最后一个所有主来负责销毁它。
甚至也可以不用共享,在代码中直接把所有权传递给其它对象。
其实您可以把智能指针当成一个重载了*和->的「对象」来看。
智能指针类型被用来自动化所有权的登记工作,来确保执行销毁义务到位。
std::unique_ptr是C++11新推出的一种智能指针类型,用来表示动态分配出的对象的「独一无二」所有权;当std::unique_ptr离开作用域,对象就会被销毁。
不能复制std::unique_ptr,但可以把它移动(move)给新所有主。
std::shared_ptr同样表示动态分配对象的所有权,但可以被共享,也可以被复制;对象的所有权由所有复制者共同拥有,最后一个复制者被销毁时,对象也会随着被销毁。
优点:•如果没有清晰、逻辑条理的所有权安排,不可能管理好动态分配的内存。
•传递对象的所有权,开销比复制来得小,如果可以复制的话。
•传递所有权也比「借用」指针或引用来得简单,毕竟它大大省去了两个用户一起协调对象生命周期的工作。
•如果所有权逻辑条理,有文档且不乱来的话,可读性很棒。
•可以不用手动完成所有权的登记工作,大大简化了代码,也免去了一大波错误之恼。
•对于const对象来说,智能指针简单易用,也比深度复制高效。
缺点:•不得不用指针(不管是智能的还是原生的)来表示和传递所有权。
指针语义可要比值语义复杂得许多了,特别是在API里:您不光要操心所有权,还要顾及别名,生命周期,可变性(mutability)以及其它大大小小问题。
•其实值语义的开销经常被高估,所以就所有权的性能来说,可不能光只考虑可读性以及复杂性。
•如果API依赖所有权的传递,就会害得客户端不得不用单一的内存管理模型。
•销毁资源并回收的相关代码不是很明朗。
•std::unique_ptr的所有权传递原理是C++11的move语法,后者毕竟是刚刚推出的,容易迷惑程序员。
•如果原本的所有权设计已经够完善了,那么若要引入所有权共享机制,可能不得不重构整个系统。
•所有权共享机制的登记工作在运行时进行,开销可能相当不小。
•某些极端情况下,所有权被共享的对象永远不会被销毁,比如引用死循环(cyclicreferences)。
•智能指针并不能够完全代替原生指针。
决定:如果必须使用动态分配,倾向于保持分配者的所有权。
如果其他地方要使用这个对象,最好传递它的拷贝,或者传递一个不用改变所有权的指针或引用。
倾向于使用std::unique_ptr来明确所有权传递,例如: std::unique_ptrFooFactory();voidFooConsumer(std::unique_ptrptr); 避免使用共享所有权。
如果对性能要求很高,并且操作的对象是不可变的(比如说std::shared_ptr),这时可以用共享所有权来避免昂贵的拷贝操作。
如果确实要使用共享所有权,倾向于使用std::shared_ptr。
不要在新代码中使用scoped_ptr``,除非你必须兼容老版本的C++。
总是用``std::unique_ptr代替std::auto_ptr。
4.2.cpplint Tip:使用cpplint.py检查风格错误. cpplint.py是一个用来分析源文件,能检查出多种风格错误的工具.它不并完美,甚至还会漏报和误报,但它仍然是一个非常有用的工具.在行尾加//NOLINT,或在上一行加//NOLINTNEXTLINE,可以忽略报错。
某些项目会指导你如何使用他们的项目工具运行cpplint.py.如果你参与的项目没有提供,你可以单独下载cpplint.py. acgtyrant
1.把智能指针当成对象来看待的话,就很好领会它与所指对象之间的关系了。

2.原来Rust的Ownership思想是受到了C++智能指针的很大启发啊。
3.scoped_ptr和auto_ptr已过时。
现在是shared_ptr和uniqued_ptr的天下了。

4.按本文来说,似乎除了智能指针,还有其它所有权机制,值得留意。

5.ArchLinux用户注意了,AUR有对cpplint打包。

5. C++ 5.1. Tip:所有按引用传递的参数必须加上const. 定义:在C语言中,如果函数需要修改变量的值,参数必须为指针,如intfoo(int*pval).在C++中,函数还可以声明引用参数:intfoo(int&val).优点:定义引用参数防止出现(*pval)++这样丑陋的代码.像拷贝构造函数这样的应用也是必需的.而且更明确,不接受NULL指针.缺点:容易引起误解,因为引用在语法上是值变量却拥有指针的语义.结论:函数参数列表中,所有引用参数都必须是const: voidFoo(conststring&in,string*out); 事实上这在GoogleCode是一个硬性约定:输入参数是值参或const引用,输出参数为指针.输入参数可以是const指针,但决不能是非const的引用参数,除非用于交换,比如swap().有时候,在输入形参中用constT*指针比constT&更明智。
比如: •您会传null指针。
•函数要把指针或对地址的引用赋值给输入形参。
总之大多时候输入形参往往是constT&.若用constT*说明输入另有处理。
所以若您要用constT*,则应有理有据,否则会害得读者误解。
5.2. Tip:只在定义移动构造函数与移动赋值操作时使用右值引用.不要使用std::forward. 定义:右值引用是一种只能绑定到临时对象的引用的一种,其语法与传统的引用语法相似.例如,voidf(string&&s);声明了一个其参数是一个字符串的右值引用的函数.优点:用于定义移动构造函数(使用类的右值引用进行构造的函数)使得移动一个值而非拷贝之成为可能.例如,如果v1是一个vector,则autov2(std::move(v1))将很可能不再进行大量的数据复制而只是简单地进行指针操作,在某些情况下这将带来大幅度的性能提升.右值引用使得编写通用的函数封装来转发其参数到另外一个函数成为可能,无论其参数是否是临时对象都能正常工作.右值引用能实现可移动但不可拷贝的类型,这一特性对那些在拷贝方面没有实际需求,但有时又需要将它们作为函数参数传递或塞入容器的类型很有用.要高效率地使用某些标准库类型,例如std::unique_ptr,std::move是必需的.缺点:右值引用是一个相对比较新的特性(由C++11引入),它尚未被广泛理解.类似引用崩溃,移动构造函数的自动推导这样的规则都是很复杂的.结论:只在定义移动构造函数与移动赋值操作时使用右值引用,不要使用std::forward功能函数.你可能会使用std::move来表示将值从一个对象移动而不是复制到另一个对象. 5.3. Tip:若要用好函数重载,最好能让读者一看调用点(callsite)就胸有成竹,不用花心思猜测调用的重载函数到底是哪一种。
该规则适用于构造函数。
定义: 你可以编写一个参数类型为conststring&的函数,然后用另一个参数类型为constchar*的函数重载它: classMyClass{public:voidAnalyze(conststring&text);voidAnalyze(constchar*text,size_ttextlen); }; 优点:通过重载参数不同的同名函数,令代码更加直观.模板化代码需要重载,同时为使用者带来便利.缺点:如果函数单单靠不同的参数类型而重载(acgtyrant注:这意味着参数数量不变),读者就得十分熟悉C++五花八门的匹配规则,以了解匹配过程具体到底如何。
另外,当派生类只重载了某个函数的部分变体,继承语义容易令人困惑。
结论:如果您打算重载一个函数,可以试试改在函数名里加上参数信息。
例如,用AppendString()和AppendInt()等,而不是一口气重载多个Append(). 5.4. Tip:我们不允许使用缺省函数参数,少数极端情况除外。
尽可能改用函数重载。
优点:当您有依赖缺省参数的函数时,您也许偶尔会修改修改这些缺省参数。
通过缺省参数,不用再为个别情况而特意定义一大堆函数了。
与函数重载相比,缺省参数语法更为清晰,代码少,也很好地区分了「必选参数」和「可选参数」。
缺点:缺省参数会干扰函数指针,害得后者的函数签名(functionsignature)往往对不上所实际要调用的函数签名。
即在一个现有函数添加缺省参数,就会改变它的类型,那么调用其地址的代码可能会出错,不过函数重载就没这问题了。
此外,缺省参数会造成臃肿的代码,毕竟它们在每一个调用点(callsite)都有重复(acgtyrant注:我猜可能是因为调用函数的代码表面上看来省去了不少参数,但编译器在编译时还是会在每一个调用代码里统统补上所有默认实参信息,造成大量的重复)。
函数重载正好相反,毕竟它们所谓的「缺省参数」只会出现在函数定义里。
结论:由于缺点并不是很严重,有些人依旧偏爱缺省参数胜于函数重载。
所以除了以下情况,我们要求必须显式提供所有参数(acgtyrant注:即不能再通过缺省参数来省略参数了)。

一,位于文件里的静态函数或匿名空间函数,毕竟都只能在局部文件里调用该函数了。

二,可以在构造函数里用缺省参数,毕竟不可能取得它们的地址。

三,可以用来模拟变长数组。
//通过空AlphaNum以支持四个形参stringStrCat(constAlphaNum&a, constAlphaNum&b=gEmptyAlphaNum,constAlphaNum&c=gEmptyAlphaNum,constAlphaNum&d=gEmptyAlphaNum); 5.5. alloca() Tip:我们不允许使用变长数组和alloca(). 优点:变长数组具有浑然天成的语法.变长数组和alloca()也都很高效.缺点:变长数组和alloca()不是标准C++的组成部分.更重要的是,它们根据数据大小动态分配堆栈内存,会引起难以发现的内存越界bugs:“在我的机器上运行的好好的,发布后却莫名其妙的挂掉了”.结论:改用更安全的分配器(allocator),就像std::vector或std::unique_ptr. 5.6. Tip:我们允许合理的使用友元类及友元函数. 通常友元应该定义在同一文件内,避免代码读者跑到其它文件查找使用该私有成员的类.经常用到友元的一个地方是将FooBuilder声明为Foo的友元,以便FooBuilder正确构造Foo的内部状态,而无需将该状态暴露出来.某些情况下,将一个单元测试类声明成待测类的友元会很方便. 友元扩大了(但没有打破)类的封装边界.某些情况下,相对于将类成员声明为public,使用友元是更好的选择,尤其是如果你只允许另一个类访问该类的私有成员时.当然,大多数类都只应该通过其提供的公有成员进行互操作. 5.7. Tip:我们不使用C++异常. 优点:•异常允许应用高层决定如何处理在底层嵌套函数中「不可能发生」的失败(failures),不用管那些含糊且容易 出错的错误代码(acgtyrant注:errorcode,我猜是C语言函数返回的非零int值)。
•很多现代语言都用异常。
引入异常使得C++与Python,Java以及其它类C++的语言更一脉相承。
•有些第三方C++库依赖异常,禁用异常就不好用了。
•异常是处理构造函数失败的唯一途径。
虽然可以用工厂函数(acgtyrant注:factoryfunction,出自C++的
种设计模式,即「简单工厂模式」)或Init()方法代替异常,但是前者要求在堆栈分配内存,后者会导致刚创建的实例处于”无效“状态。
•在测试框架里很好用。
缺点: •在现有函数中添加throw语句时,您必须检查所有调用点。
要么让所有调用点统统具备最低限度的异常安全保证,要么眼睁睁地看异常一路欢快地往上跑,最终中断掉整个程序。
举例,f()调用g(),g()又调用h(),且h抛出的异常被f捕获。
当心g,否则会没妥善清理好。
•还有更常见的,异常会彻底扰乱程序的执行流程并难以判断,函数也许会在您意料不到的地方返回。
您或许会加一大堆何时何处处理异常的规定来降低风险,然而开发者的记忆负担更重了。
•异常安全需要RAII和不同的编码实践.要轻松编写出正确的异常安全代码需要大量的支持机制.更进一步地说,为了避免读者理解整个调用表,异常安全必须隔绝从持续状态写到“提交”状态的逻辑.这一点有利有弊(因为你也许不得不为了隔离提交而混淆代码).如果允许使用异常,我们就不得不时刻关注这样的弊端,即使有时它们并不值得. •启用异常会增加二进制文件数据,延长编译时间(或许影响小),还可能加大地址空间的压力。
•滥用异常会变相鼓励开发者去捕捉不合时宜,或本来就已经没法恢复的「伪异常」。
比如,用户的输入不符合格 式要求时,也用不着抛异常。
如此之类的伪异常列都列不完。
结论:从表面上看来,使用异常利大于弊,尤其是在新项目中.但是对于现有代码,引入异常会牵连到所有相关代码.如果新项目允许异常向外扩散,在跟以前未使用异常的代码整合时也将是个麻烦.因为Google现有的大多数C++代码都没有异常处理,引入带有异常处理的新代码相当困难.鉴于Google现有代码不接受异常,在现有代码中使用异常比在新项目中使用的代价多少要大一些.迁移过程比较慢,也容易出错.我们不相信异常的使用有效替代方案,如错误代码,断言等会造成严重负担.我们并不是基于哲学或道德层面反对使用异常,而是在实践的基础上.我们希望在Google使用我们自己的开源项目,但项目中使用异常会为此带来不便,因此我们也建议不要在Google的开源项目中使用异常.如果我们需要把这些项目推倒重来显然不太现实.对于Windows代码来说,有个特例.(YuleFox注:对于异常处理,显然不是短短几句话能够说清楚的,以构造函数为例,很多C++书籍上都提到当构造失败时只有异常可以处理,Google禁止使用异常这一点,仅仅是为了自身的方便,说大了,无非是基于软件管理成本上,实际使用中还是自己决定) 5.8. TODO Tip:我们禁止使用RTTI. 定义:RTTI允许程序员在运行时识别C++类对象的类型.它通过使用typeid或者dynamic_cast完成.优点:RTTI的标准替代(下面将描述)需要对有问题的类层级进行修改或重构.有时这样的修改并不是我们所想要的,甚至是不可取的,尤其是在一个已经广泛使用的或者成熟的代码中.RTTI在某些单元测试中非常有用.比如进行工厂类测试时,用来验证一个新建对象是否为期望的动态类型.RTTI对于管理对象和派生对象的关系也很有用.在考虑多个抽象对象时RTTI也很好用.例如: boolBase::Equal(Base*other)=0;boolDerived::Equal(Base*other){ Derived*that=dynamic_cast(other);if(that==NULL) returnfalse;...} 缺点:在运行时判断类型通常意味着设计问题.如果你需要在运行期间确定一个对象的类型,这通常说明你需要考虑重新设计你的类.随意地使用RTTI会使你的代码难以维护.它使得基于类型的判断树或者switch语句散布在代码各处.如果以后要进行修改,你就必须检查它们.结论:RTTI有合理的用途但是容易被滥用,因此在使用时请务必注意.在单元测试中可以使用RTTI,但是在其他代码中请尽量避免.尤其是在新代码中,使用RTTI前务必三思.如果你的代码需要根据不同的对象类型执行不同的行为的话,请考虑用以下的两种替代方案之一查询类型:虚函数可以根据子类类型的不同而执行不同代码.这是把工作交给了对象本身去处理.如果这一工作需要在对象之外完成,可以考虑使用双重分发的方案,例如使用访问者设计模式.这就能够在对象之外进行类型判断.如果程序能够保证给定的基类实例实际上都是某个派生类的实例,那么就可以自由使用dynamic_cast.在这种情况下,使用dynamic_cast也是一种替代方案.基于类型的判断树是一个很强的暗示,它说明你的代码已经偏离正轨了.不要像下面这样: if(typeid(*data)==typeid(D1)){... }elseif(typeid(*data)==typeid(D2)){... }elseif(typeid(*data)==typeid(D3)){... 一旦在类层级中加入新的子类,像这样的代码往往会崩溃.而且,一旦某个子类的属性改变了,你很难找到并修改所有受影响的代码块.不要去手工实现一个类似RTTI的方案.反对RTTI的理由同样适用于这些方案,比如带类型标签的类继承体系.而且,这些方案会掩盖你的真实意图. 5.9. Tip:使用C++的类型转换,如static_cast<>().不要使用inty=(int)x或inty=int(x)等转换方式; 定义:C++采用了有别于C的类型转换机制,对转换操作进行归类.优点: C语言的类型转换问题在于模棱两可的操作;有时是在做强制转换(如(int)3.5),有时是在做类型转换(如(int)"hello").另外,C++的类型转换在查找时更醒目.缺点:恶心的语法.结论:不要使用C风格类型转换.而应该使用C++风格. •用static_cast替代C风格的值转换,或某个类指针需要明确的向上转换为父类指针时.•用const_cast去掉const限定符.•用reinterpret_cast指针类型和整型或其它指针之间进行不安全的相互转换.仅在你对所做一切了 然于心时使用.至于dynamic_cast参见5.8.运行时类型识别. 5.10. Tip:只在记录日志时使用流. 定义:流用来替代printf()和scanf().优点:有了流,在打印时不需要关心对象的类型.不用担心格式化字符串与参数列表不匹配(虽然在中使用printf也不存在这个问题).流的构造和析构函数会自动打开和关闭对应的文件.缺点:流使得pread()等功能函数很难执行.如果不使用printf风格的格式化字符串,某些格式化操作(尤其是常用的格式字符串%.*s)用流处理性能是很低的.流不支持字符串操作符重新排序(%1s),而这一点对于软件国际化很有用.结论:不要使用流,除非是日志接口需要.使用printf之类的代替.使用流还有很多利弊,但代码一致性胜过一切.不要在代码中使用流.拓展讨论:对这一条规则存在一些争论,这儿给出点深层次原因.回想一下唯一性原则(OnlyOneWay):我们希望在任何时候都只使用一种确定的I/O类型,使代码在所有I/O处都保持一致.因此,我们不希望用户来决定是使用流还是printf+read/write.相反,我们应该决定到底用哪一种方式.把日志作为特例是因为日志是一个非常独特的应用,还有一些是历史原因.流的支持者们主张流是不二之选,但观点并不是那么清晰有力.他们指出的流的每个优势也都是其劣势.流最大的优势是在输出时不需要关心打印对象的类型.这是一个亮点.同时,也是一个不足:你很容易用错类型,而编译器不会报警.使用流时容易造成的这类错误: cout< cerr<<"Errorconnectingto'"<bar()->hostname.first<<":"<bar()->hostname.second<<":"<bar()->hostname.first,foo->bar()->hostname.second,strerror(errno)); 你可能会说,“把流封装一下就会比较好了”,这儿可以,其他地方呢?
而且不要忘了,我们的目标是使语言更紧凑,而不是添加一些别人需要学习的新装备.每一种方式都是各有利弊,“没有最好,只有更适合”.简单性原则告诫我们必须从中选择其
一,最后大多数决定采用printf+read/write. 5.11. Tip:对于迭代器和其他模板对象使用前缀形式(++i)的自增,自减运算符. 定义:对于变量在自增(++i或i++)或自减(--i或i--)后表达式的值又没有没用到的情况下,需要确定到底是使用前置还是后置的自增(自减).优点:不考虑返回值的话,前置自增(++i)通常要比后置自增(i++)效率更高.因为后置自增(或自减)需要对表达式的值i进行一次拷贝.如果i是迭代器或其他非数值类型,拷贝的代价是比较大的.既然两种自增方式实现的功能一样,为什么不总是使用前置自增呢?
缺点:在C开发中,当表达式的值未被使用时,传统的做法是使用后置自增,特别是在for循环中.有些人觉得后置自增更加易懂,因为这很像自然语言,主语(i)在谓语动词(++)前.结论:对简单数值(非对象),两种都无所谓.对迭代器和模板类型,使用前置自增(自减). 5.12.const Tip:我们强烈建议你在任何可能的情况下都要使用const.此外有时改用C++11推出的constexpr更好。
定义:在声明的变量或参数前加上关键字const用于指明变量值不可被篡改(如constintfoo).为类中的函数加上const限定符表明该函数不会修改类成员变量的状态(如classFoo{intBar(charc)const;};).优点: 大家更容易理解如何使用变量.编译器可以更好地进行类型检测,相应地,也能生成更好的代码.人们对编写正确的代码更加自信,因为他们知道所调用的函数被限定了能或不能修改变量值.即使是在无锁的多线程编程中,人们也知道什么样的函数是安全的.缺点:const是入侵性的:如果你向一个函数传入const变量,函数原型声明中也必须对应const参数(否则变量需要const_cast类型转换),在调用库函数时显得尤其麻烦.结论:const变量,数据成员,函数和参数为编译时类型检测增加了一层保障;便于尽早发现错误.因此,我们强烈建议在任何可能的情况下使用const: •如果函数不会修改传你入的引用或指针类型参数,该参数应声明为const.•尽可能将函数声明为const.访问函数应该总是const.其他不会修改任何数据成员,未调用非const 函数,不会返回数据成员非const指针或引用的函数也应该声明成const.•如果数据成员在对象构造之后不再发生变化,可将其定义为const.然而,也不要发了疯似的使用const.像constint*const*constx;就有些过了,虽然它非常精确的描述了常量x.关注真正有帮助意义的信息:前面的例子写成constint**x就够了.关键字mutable可以使用,但是在多线程中是不安全的,使用时首先要考虑线程安全.const的位置:有人喜欢intconst*foo形式,不喜欢constint*foo,他们认为前者更一致因此可读性也更好:遵循了const总位于其描述的对象之后的原则.但是一致性原则不适用于此,“不要过度使用”的声明可以取消大部分你原本想保持的一致性.将const放在前面才更易读,因为在自然语言中形容词(const)是在名词(int)之前.这是说,我们提倡但不强制const在前.但要保持代码的一致性!
(Yang.Y注:也就是不要在一些地方把const写在类型前面,在其他地方又写在后面,确定一种写法,然后保持一致.) 5.13.constexpr Tip:在C++11里,用constexpr来定义真正的常量,或实现常量初始化。
定义:变量可以被声明成constexpr以表示它是真正意义上的常量,即在编译时和运行时都不变。
函数或构造函数也可以被声明成constexpr,以用来定义constexpr变量。
优点:如今constexpr就可以定义浮点式的真￿常量,不用再依赖字面值了;也可以定义用户自定义类型上的常量;甚至也可以定义函数调用所返回的常量。
缺点:若过早把变量优化成constexpr变量,将来又要把它改为常规变量时,挺麻烦的;当前对constexpr函数和构造函数中允许的限制可能会导致这些定义中解决的方法模糊。
结论:靠constexpr特性,方才实现了C++在接口上打造真正常量机制的可能。
好好用constexpr来定义真￿常量以及支持常量的函数。
避免复杂的函数定义,以使其能够与constexpr一起使用。
千万别痴心妄想地想靠constexpr来强制代码「内联」。
5.14. Tip:C++内建整型中,仅使用int.如果程序中需要不同大小的变量,可以使用中长度精确的整型,如int16_t.如果您的变量可能不小于2^31(2GiB),就用64位变量比如int64_t.此外要留意,哪怕您的值并不会超出int所能够表示的范围,在计算过程中也可能会溢出。
所以拿不准时,干脆用更大的类型。
定义:C++没有指定整型的大小.通常人们假定short是16位,int是32位,long是32位,longlong是64位.优点:保持声明统
一.缺点:C++中整型大小因编译器和体系结构的不同而不同.结论:定义了int16_t,uint32_t,int64_t等整型,在需要确保整型大小时可以使用它们代替short,unsignedlonglong等.在C整型中,只使用int.在合适的情况下,推荐使用标准类型如size_t和ptrdiff_t.如果已知整数不会太大,我们常常会使用int,如循环计数.在类似的情况下使用原生类型int.你可以认为int至少为32位,但不要认为它会多于32位.如果需要64位整型,用int64_t或uint64_t.对于大整数,使用int64_t.不要使用uint32_t等无符号整型,除非你是在表示一个位组而不是一个数值,或是你需要定义二进制补码溢出.尤其是不要为了指出数值永不会为负,而使用无符号类型.相反,你应该使用断言来保护数据.如果您的代码涉及容器返回的大小(size),确保其类型足以应付容器各种可能的用法。
拿不准时,类型越大越好。
小心整型类型转换和整型提升(acgtyrant注:integerpromotions,比如int与unsignedint运算时,前者被提升为unsignedint而有可能溢出),总有意想不到的后果。
关于无符号整数:有些人,包括一些教科书作者,推荐使用无符号类型表示非负数.这种做法试图达到自我文档化.但是,在C语言中,这一优点被由其导致的bug所淹没.看看下面的例子: for(unsignedinti=foo.Length()-1;i>=0;--i)... 上述循环永远不会退出!
有时会发现该bug并报警,但大部分情况下都不会.类似的bug还会出现在比较有符合变量和无符号变量时.主要是C的类型提升机制会致使无符号类型的行为出乎你的意料.因此,使用断言来指出变量为非负数,而不是使用无符号型!
5.15.64 Tip:代码应该对64位和32位系统友好.处理打印,比较,结构体对齐时应切记: •对于某些类型,printf()的指示符在32位和64位系统上可移植性不是很好.C99标准定义了一些可移植的格式化指示符.不幸的是,MSVC7.1并非全部支持,而且标准中也有所遗漏,所以有时我们不得不自己定义一个丑陋的版本(头文件inttypes.h仿标准风格): //printfmacrosforsize_t,inthestyleofinttypes.h#ifdef_LP64#define__PRIS_PREFIX"z"#else#define__PRIS_PREFIX#endif //Usethesemacrosaftera%inaprintfformatstring//togetcorrect32/64bitbehavior,likethis://size_tsize=records.size();//printf("%"PRIuS"\n",size);#definePRIdS__PRIS_PREFIX"d"#definePRIxS__PRIS_PREFIX"x"#definePRIuS__PRIS_PREFIX"u"#definePRIXS__PRIS_PREFIX"X"#definePRIoS__PRIS_PREFIX"o" void*(或其他指针类型)int64_tuint64_tsize_tptrdiff_t %lx%qd,%lld%qu,%llu,%llx%u%d %p%"PRId64"%"PRIu64",%"PRIx64"%"PRIuS",%"PRIxS"%"PRIdS" C99规定%zuC99规定%zd 注意PRI*宏会被编译器扩展为独立字符串.因此如果使用非常量的格式化字符串,需要将宏的值而不是宏名插入格式中.使用PRI*宏同样可以在%后包含长度指示符.例如,printf("x=%30"PRIuS"\n",x)在32位Linux上将被展开为printf("x=%30""u""\n",x),编译器当成printf("x=%30u\n",x)处理(Yang.Y注:这在MSVC6.0上行不通,VC6编译器不会自动把引号间隔的多个字符串连接一个长字符串). •记住sizeof(void*)!
=sizeof(int).如果需要一个指针大小的整数要用intptr_t. •你要非常小心的对待结构体对齐,尤其是要持久化到磁盘上的结构体(Yang.Y注:持久化-将数据按字节流顺序保存在磁盘文件或数据库中).在64位系统中,任何含有int64_t/uint64_t成员的类/结构体,缺省都以8字节在结尾对齐.如果32位和64位代码要共用持久化的结构体,需要确保两种体系结构下的结构体对齐一致.大多数编译器都允许调整结构体对齐.中可使用__attribute__((packed)).MSVC则提供了#pragmapack()和__declspec(align())(YuleFox注,解决方案的项目属性里也可以直接设置). •创建64位常量时使用LL或ULL作为后缀,如: int64_tmy_value=0×123456789LL;uint64_tmy_mask=3ULL<<48; •如果你确实需要32位和64位系统具有不同代码,可以使用#ifdef_LP64指令来切分32/64位代码.(尽量不要这么做,如果非用不可,尽量使修改局部化) 5.16. Tip:使用宏时要非常谨慎,尽量以内联函数,枚举和常量代替之. 宏意味着你和编译器看到的代码是不同的.这可能会导致异常行为,尤其因为宏具有全局作用域.值得庆幸的是,C++中,宏不像在C中那么必不可少.以往用宏展开性能关键的代码,现在可以用内联函数替代.用宏表示常量可被const变量代替.用宏“缩写”长变量名可被引用代替.用宏进行条件编译...这个,千万别这么做,会令测试更加痛苦(#define防止头文件重包含当然是个特例).宏可以做一些其他技术无法实现的事情,在一些代码库(尤其是底层库中)可以看到宏的某些特性(如用#字符串化,用##连接等等).但在使用前,仔细考虑一下能不能不使用宏达到同样的目的.下面给出的用法模式可以避免使用宏带来的问题;如果你要宏,尽可能遵守:•不要在.h文件中定义宏.•在马上要使用时才进行#define,使用后要立即#undef.•不要只是对已经存在的宏使用#undef,选择一个不会冲突的名称;•不要试图使用展开后会导致C++构造不稳定的宏,不然也至少要附上文档说明其行为.•不要用##处理函数,类和变量的名字。
5.17.0,nullptrNULL Tip:整数用
0,实数用0.0,指针用nullptr或NULL,字符(串)用'\0'.整数用
0,实数用0.0,这一点是毫无争议的.对于指针(地址值),到底是用
0,NULL还是nullptr.C++11项目用nullptr;C++03项目则用NULL,毕竟它 看起来像指针。
实际上,一些C++编译器对NULL的定义比较特殊,可以输出有用的警告,特别是sizeof(NULL)就和sizeof
(0)不一样。
字符(串)用'\0',不仅类型正确而且可读性好. 5.18.sizeof Tip:尽可能用sizeof(varname)代替sizeof(type).使用sizeof(varname)是因为当代码中变量类型改变时会自动更新.您或许会用sizeof(type)处理不涉及任 何变量的代码,比如处理来自外部或内部的数据格式,这时用变量就不合适了。
Structdata;Structdata;memset(&data,0,sizeof(data)); Warning:memset(&data,0,sizeof(Struct)); if(raw_size 定义:C++11中,若变量被声明成auto,那它的类型就会被自动匹配成初始化表达式的类型。
您可以用auto来复制初始化或绑定引用。
vectorv;...autos1=v[0];//创建一份v[0]的拷贝。
constauto&s2=v[0];//s2是v[0]的一个引用。
优点:C++类型名有时又长又臭,特别是涉及模板或命名空间的时候。
就像: sparse_hash_map::iteratoriter=m.find(val);返回类型好难读,代码目的也不够一目了然。
重构其: autoiter=m.find(val);好多了。
没有auto的话,我们不得不在同一个表达式里写同一个类型名两次,无谓的重复,就像: diagnostics::ErrorStatus*status=newdiagnostics::ErrorStatus("xyz");有了auto,可以更方便地用中间变量,显式编写它们的类型轻松点。
缺点:类型够明显时,特别是初始化变量时,代码才会够一目了然。
但以下就不一样了: autoi=x.Lookup(key);看不出其类型是啥,x的类型声明恐怕远在几百行之外了。
程序员必须会区分auto和constauto&的不同之处,否则会复制错东西。
auto和C++11列表初始化的合体令人摸不着头脑: autox
(3);//圆括号。
autoy{3};//大括号。
它们不是同一回事——x是int,y则是std::initializer_list.其它一般不可见的代理类型(acgtyrant注:normally-invisibleproxytypes,它涉及到C++鲜为人知的坑:WhyisvectornotaSTLcontainer?
)也有大同小异的陷阱。
如果在接口里用auto,比如声明头文件里的一个常量,那么只要仅仅因为程序员一时修改其值而导致类型变化的话——API要翻天覆地了。
结论:auto只能用在局部变量里用。
别用在文件作用域变量,命名空间作用域变量和类数据成员里。
永远别列表初始化auto变量。
auto还可以和C++11特性「尾置返回类型(trailingreturntype)」一起用,不过后者只能用在lambda表达式里。
5.20. Tip:你可以用列表初始化。
早在C++03里,聚合类型(aggregatetypes)就已经可以被列表初始化了,比如数组和不自带构造函数的结构 体: structPoint{intx;inty;};Pointp={1,2}; C++11中,该特性得到进一步的推广,任何对象类型都可以被列表初始化。
示范如下: //Vector接收了一个初始化列表。
vectorv{"foo","bar"}; //不考虑细节上的微妙差别,大致上相同。
//您可以任选其
一。
vectorv={"foo","bar"}; //可以配合new一起用。
autop=newvector{"foo","bar"}; //map接收了一些pair,列表初始化大显神威。
mapm={{
1,"one"},{
2,"2"}}; //初始化列表也可以用在返回类型上的隐式转换。
vectortest_function(){return{1,2,3};} //初始化列表可迭代。
for(inti:{-
1,-2,-3}){} //在函数调用里用列表初始化。
voidTestFunction2(vectorv){}TestFunction2({1,2,3}); 用户自定义类型也可以定义接收std::initializer_list的构造函数和赋值运算符,以自动列表初始化: classMyType{public://std::initializer_list专门接收init列表。
//得以值传递。
MyType(std::initializer_listinit_list){for(inti:init_list)append(i);}MyType&operator=(std::initializer_listinit_list){clear();for(inti:init_list)append(i);} };MyTypem{2,3,5,7}; 最后,列表初始化也适用于常规数据类型的构造,哪怕没有接收std::initializer_list的构造函数。
doubled{1.23};//MyOtherType没有std::initializer_list构造函数, //直接上接收常规类型的构造函数。
classMyOtherType{ public:explicitMyOtherType(string);MyOtherType(int,string); };MyOtherTypem={
1,"b"};//不过如果构造函数是显式的(explict),您就不能用`={}`了。
MyOtherTypem{"b"}; 千万别直接列表初始化auto变量,看下一句,估计没人看得懂: Warning:autod={1.23}; //d即是std::initializer_list autod=double{1.23};//善哉--d即为double,并非std::initializer_list.至于格式化,参见braced-initializer-list-format. 5.21.Lambda Tip:适当使用lambda表达式。
别用默认lambda捕获,所有捕获都要显式写出来。
定义:Lambda表达式是创建匿名函数对象的一种简易途径,常用于把函数当参数传,例如: std::sort(v.begin(),v.end(),[](intx,inty){returnWeight(x)•Lambdas,std::functions和std::bind可以搭配成通用回调机制(generalpurposecallbackmechanism);写接收有界函数为参数的函数也很容易了。
缺点:•Lambdas的变量捕获略旁门左道,可能会造成悬空指针。
•Lambdas可能会失控;层层嵌套的匿名函数难以阅读。
结论:•按format小用lambda表达式怡情。
•禁用默认捕获,捕获都要显式写出来。
打比方,比起[=](intx){returnx+n;},您该写成[n](intx){returnx+n;}才对,这样读者也好一眼看出n是被捕获的值。
•匿名函数始终要简短,如果函数体超过了五行,那么还不如起名(acgtyrant注:即把lambda表达式赋值给对象),或改用函数。
•如果可读性更好,就显式写出lambd的尾置返回类型,就像auto. 5.22.编程 Tip:不要使用复杂的模板编程 定义:模板编程指的是利用c++模板实例化机制是图灵完备性,可以被用来实现编译时刻的类型判断的一系列编程技巧优点:模板编程能够实现非常灵活的类型安全的接口和极好的性能,一些常见的工具比如GoogleTest,std::tuple,std::function和Boost.Spirit.这些工具如果没有模板是实现不了的缺点:•模板编程所使用的技巧对于使用c++不是很熟练的人是比较晦涩,难懂的.在复杂的地方使用模板的代码让人更不容易读懂,并且debug和维护起来都很麻烦•模板编程经常会导致编译出错的信息非常不友好:在代码出错的时候,即使这个接口非常的简单,模板内部复杂的实现细节也会在出错信息显示.导致这个编译出错信息看起来非常难以理解.•大量的使用模板编程接口会让重构工具(VisualAssistX,RefactorforC++等等)更难发挥用途.首先模板的代码会在很多上下文里面扩展开来,所以很难确认重构对所有的这些展开的代码有用,其次有些重构工具只对已经做过模板类型替换的代码的AST有用.因此重构工具对这些模板实现的原始代码并不有效,很难找出哪些需要重构. 结论:•模板编程有时候能够实现更简洁更易用的接口,但是更多的时候却适得其反.因此模板编程最好只用在少量的基 础组件,基础数据结构上,因为模板带来的额外的维护成本会被大量的使用给分担掉•在使用模板编程或者其他复杂的模板技巧的时候,你一定要再三考虑一下.考虑一下你们团队成员的平均水平是 否能够读懂并且能够维护你写的模板代码.或者一个非c++程序员和一些只是在出错的时候偶尔看一下代码的人能够读懂这些错误信息或者能够跟踪函数的调用流程.如果你使用递归的模板实例化,或者类型列表,或者元函数,又或者表达式模板,或者依赖SFINAE,或者sizeof的trick手段来检查函数是否重载,那么这说明你模板用的太多了,这些模板太复杂了,我们不推荐使用•如果你使用模板编程,你必须考虑尽可能的把复杂度最小化,并且尽量不要让模板对外暴漏.你最好只在实现里面使用模板,然后给用户暴露的接口里面并不使用模板,这样能提高你的接口的可读性.并且你应该在这些使用模板的代码上写尽可能详细的注释.你的注释里面应该详细的包含这些代码是怎么用的,这些模板生成出来的代码大概是什么样子的.还需要额外注意在用户错误使用你的模板代码的时候需要输出更人性化的出错信息.因为这些出错信息也是你的接口的一部分,所以你的代码必须调整到这些错误信息在用户看起来应该是非常容易理解,并且用户很容易知道如何修改这些错误 5.23.Boost Tip:只使用Boost中被认可的库. 定义:Boost库集是一个广受欢迎,经过同行鉴定,免费开源的C++库集.优点:Boost代码质量普遍较高,可移植性好,填补了C++标准库很多空白,如型别的特性,更完善的绑定器,更好的智能指针。
缺点:某些Boost库提倡的编程实践可读性差,比如元编程和其他高级模板技术,以及过度“函数化”的编程风格.结论:为了向阅读和维护代码的人员提供更好的可读性,我们只允许使用Boost一部分经认可的特性子集.目前允许使用以下库: •CallTraits:boost/call_traits.hpp•CompressedPair:pressed_pair.hpp• 定义:C++11有众多语言和库上的‘变革‘_。
优点:在二〇一四年八月之前,C++11一度是官方标准,被大多C++编译器支持。
它标准化很多我们早先就在用的C++扩展,简化了不少操作,大大改善了性能和安全。
缺点:C++11相对于前身,复杂极了:1300页vs800页!很多开发者也不怎么熟悉它。
于是从长远来看,前者特性对代码可读性以及维护代价难以预估。
我们说不准什么时候采纳其特性,特别是在被迫依赖老实工具的项目上。
和5.23.Boost库一样,有些C++11扩展提倡实则对可读性有害的编程实践——就像去除冗余检查(比如类型名)以帮助读者,或是鼓励模板元编程等等。
有些扩展在功能上与原有机制冲突,容易招致困惑以及迁移代价。
缺点:C++11特性除了个别情况下,可以用一用。
除了本指南会有不少章节会加以讨若干C++11特性之外,以下特性最好不要用: •尾置返回类型,比如用autofoo()->int代替intfoo().为了兼容于现有代码的声明风格。
•编译时合数,因为它涉及一个重模板的接口风格。
头文件,因为编译器尚不支持。
•默认lambda捕获。
acgtyrant
1.实际上,缺省参数会改变函数签名的前提是改变了它接收的参数数量,比如把voida()改成voida(intb=0),开发者改变其代码的初衷也许是,在不改变「代码兼容性」的同时,又提供了可选int参数的余地,然而这终究会破坏函数指针上的兼容性,毕竟函数签名确实变了。

2.此外把自带缺省参数的函数地址赋值给指针时,会丢失缺省参数信息。

3.我还发现滥用缺省参数会害得读者光只看调用代码的话,会误以为其函数接受的参数数量比实际上还要少。
4.friend实际上只对函数/类赋予了对其所在类的访问权限,并不是有效的声明语句。
所以除了在头文件类内部 写friend函数/类,还要在类作用域之外正式地声明一遍,最后在对应的文件加以定义。

5.本风格指南都强调了「友元应该定义在同一文件内,避免代码读者跑到其它文件查找使用该私有成员的类」。
那 么可以把其声明放在类声明所在的头文件,定义也放在类定义所在的文件。

6.由于友元函数/类并不是类的一部分,自然也不会是类可调用的公有接口,于是我主张全集中放在类的尾部, 即的数据成员之后,参考声明顺序。

7.对使用C++异常处理应具有怎样的态度?非常值得一读。

8.注意初始化const对象时,必须在初始化的同时值初始化。

9.用断言代替无符号整型类型,深有启发。
10.auto在涉及迭代器的循环语句里挺常用。
11.ShouldthetrailingreturntypesyntaxstyleethedefaultfornewC++11programs?
讨论了auto与尾置 返回类型一起用的全新编码风格,值得一看。

6. 最重要的一致性规则是命名管理.命名风格快速获知名字代表是什么东东:类型?
变量?
函数?
常量?
宏...?
甚至不需要去查找类型声明.我们大脑中的模式匹配引擎可以非常可靠的处理这些命名规则. 命名规则具有一定随意性,但相比按个人喜好命名,一致性更重要,所以不管你怎么想,规则总归是规则. 6.1. Tip:函数命名,变量命名,文件命名要有描述性;少用缩写。
尽可能给有描述性的命名,别心疼空间,毕竟让代码易于新读者理解很重要。
不要用只有项目开发者能理解的缩 写,也不要通过砍掉几个字母来缩写单词。
intprice_count_reader;intnum_errors;intnum_dns_connections; //无缩写//“num”本来就很常见//人人都知道“DNS”是啥 Warning: intn;intnerr;intp_conns;intwgc_connections;intpc_reader;intcstmr_id; //莫名其妙。
//怪缩写。
//怪缩写。
//只有贵团队知道是啥意思。
//"pc"有太多可能的解释了。
//有删减若干字母。
6.2. Tip:文件名要全部小写,可以包含下划线(_)或连字符(-).按项目约定来.如果并没有项目约定,”_”更好。
可接受的文件命名: *my_useful_***muusefulclass_//``_unittest``和``_regtest``已弃用。
C++文件要以结尾,头文件以.h结尾.专门插入文本的文件则以.inc结尾,参见1.1.Self-contained头文件。
不要使用已经存在于/usr/include下的文件名(Yang.Y注:即编译器搜索系统头文件的路径),如db.h.通常应尽量让文件名更加明确.,对应于类FooBar.内联函数必须放在.h文件中.如果内联函数比较短,就直接放在.h中. 6.3. Tip:类型名称的每个单词首字母均大写,不包含下划线:MyExcitingClass,MyExcitingEnum. 所有类型命名——类,结构体,类型定义(typedef),枚举——均使用相同约定.例如://classesandstructsclassUrlTable{...classUrlTableTester{...structUrlTableProperties{... //typedefstypedefhash_mapPropertiesMap; //enumsenumUrlTableErrors{... 6.4. Tip:变量名一律小写,单词之间用下划线连接.类的成员变量以下划线结尾,但结构体的就不用,如::a_local_variable,a_struct_data_member,a_class_data_member_. 普通变量命名:举例: stringtable_name;//可-用下划线。
stringtablename;//可-全小写。
Warning:stringtableName; //差-混合大小写。
类数据成员:不管是静态的还是非静态的,类数据成员都可以和普通变量一样,但要接下划线。
classTableInfo{... private:stringtable_name_;//可-尾后加下划线。
stringtablename_;//可。
staticPool*pool_;//可。
}; 结构体变量:不管是静态的还是非静态的,结构体数据成员都可以和普通变量一样,不用像类那样接下划线: structUrlTableProperties{stringname;intnum_entries; } 结构体与类的讨论参考结构体vs.类一节.全局变量:对全局变量没有特别要求,少用就好,但如果你要用,可以用g_或其它标志作为前缀,以便更好的区分局部变量. 6.5. Tip:在全局或类里的常量名称前加k:kDaysInAWeek.且除去开头的k之外每个单词开头字母均大写。
所有编译时常量,无论是局部的,全局的还是类中的,和其他变量稍微区别一下.k后接大写字母开头的单词: constintkDaysInAWeek=7; 这规则适用于编译时的局部作用域常量,不过要按变量规则来命名也可以。
6.6. Tip:常规函数使用大小写混合,取值和设值函数则要求与变量名匹配:MyExcitingFunction(),MyExcitingMethod(),my_exciting_member_variable(),set_my_exciting_member_variable(). 常规函数:函数名的每个单词首字母大写,没有下划线。
如果您的某函数出错时就要直接crash,那么就在函数名加上OrDie.但这函数本身必须集成在产品代码里,且平时也可能会出错。
AddTableEntry()DeleteUrl()OpenFileOrDie() 取值和设值函数:取值(essors)和设值(Mutators)函数要与存取的变量名匹配.这儿摘录一个类,num_entries_是该类的实例变量: classMyClass{public:...intnum_entries()const{returnnum_entries_;}voidset_num_entries(intnum_entries){num_entries_=num_entries;} private:intnum_entries_; }; 其它非常短小的内联函数名也可以用小写字母,例如.如果你在循环中调用这样的函数甚至都不用缓存其返回值,小写命名就可以接受. 6.7. Tip:名字空间用小写字母命名,并基于项目名称和目录结构:google_awesome_project.关于名字空间的讨论和如何命名,参考名字空间一节. 6.8. Tip:枚举的命名应当和常量或宏一致:kEnumName或是ENUM_NAME. . . UrlTableErrors(AlternateUrlTabl enumUrlTableErrors{kOK=0,kErrorOutOfMemory,kErrorMalformedInput, };enumAlternateUrlTableErrors{ OK=
0,OUT_OF_MEMORY=
1,MALFORMED_INPUT=
2,}; 2009年1月之前,我们一直建议采用宏的方式命名枚举值.由于枚举值和宏之间的命名冲突,直接导致了很多问题.由此,这里改为优先选择常量风格的命名方式.新代码应该尽可能优先使用常量风格.但是老代码没必要切换到常量风格,除非宏风格确实会产生编译期问题. 6.9. Tip:你并不打算使用宏,对吧?
如果你一定要用,像这样命名:MY_MACRO_THAT_SCARES_SMALL_CHILDREN. 参考预处理宏;通常不应该使用宏.如果不得不用,其命名像枚举命名一样全部大写,使用下划线: #defineROUND(x)...#definePI_ROUNDED3.0 6.10. Tip:如果你命名的实体与已有C/C++实体相似,可参考现有命名策略. bigopen():函数名,参照open()的形式uint:typedefbigpos:struct或class,参照pos的形式sparse_hash_map:STL相似实体;参照STL命名约定LONGLONG_MAX:常量,如同INT_MAX acgtyrant
1.感觉Google的命名约定很高明,比如写了简单的类QueryResult,接着又可以直接定义一个变量query_result,区分度很好;再次,类内变量以下划线结尾,那么就可以直接传入同名的形参,比如 TextQuery::TextQuery(std::stringword):word_(word){},其中word_自然是类内私有成员。

7. 注释虽然写起来很痛苦,但对保证代码可读性至关重要.下面的规则描述了如何注释以及在哪儿注释.当然也要记住:注释固然很重要,但最好的代码本身应该是自文档化.有意义的类型名和变量名,要远胜过要用注释解释的含糊不清的名字. 你写的注释是给代码读者看的:下一个需要理解你的代码的人.慷慨些吧,下一个人可能就是你!
7.1.风格 Tip:使用//或/**/,统一就好. //或/**/都可以;但//更常用.要在如何注释及注释风格上确保统
一. 7.2. Tip:在每一个文件开头加入版权公告,然后是文件内容描述. 法律公告和作者信息:每个文件都应该包含以下项,依次是: •版权声明(比如,Copyright2008GoogleInc.)•许可证.为项目选择合适的许可证版本(比如,Apache2.0,BSD,LGPL,GPL)•作者:标识文件的原始作者.如果你对原始作者的文件做了重大修改,将你的信息添加到作者信息里.这样当其他人对该文件有疑问时可以知道该联系谁.文件内容:紧接着版权许可和作者信息之后,每个文件都要用注释描述文件内容.通常,.h文件要对所声明的类的功能和用法作简单说明.文件通常包含了更多的实现细节或算法技巧讨论,如果你感觉这些实现细节或算法技巧讨论对于理解.h文件有帮助,可以将该注释挪到.h,并在中指出文档在.h.不要简单的在.h和间复制注释.这种偏离了注释的实际意义. 7.3. Tip:每个类的定义都要附带一份注释,描述类的功能和用法. //IteratesoverthecontentsofaGargantuanTable.Sampleusage: //GargantuanTable_Iterator*iter=table->NewIterator(); //for(iter->Seek("foo");!
iter->done();iter->Next()){ // process(iter->key(),iter->value()); //} //deleteiter; classGargantuanTable_Iterator{ ... }; 如果你觉得已经在文件顶部详细描述了该类,想直接简单的来上一句“完整描述见文件顶部”也不打紧,但务必确保有这类注释. 如果类有任何同步前提,文档说明之.如果该类的实例可被多线程访问,要特别注意文档说明多线程环境下相关的规则和常量使用. 7.4. Tip:函数声明处注释描述函数功能;定义处描述函数实现. 函数声明:注释位于声明之前,对函数功能及用法进行描述.注释使用叙述式(“Opensthefile”)而非指令式(“Openthefile”);注释只是为了描述函数,而不是命令函数做什么.通常,注释不会描述函数如何工作.那是函数定义部分的事情.函数声明处注释的内容: •函数的输入输出.•对类成员函数而言:函数调用期间对象是否需要保持引用参数,是否会释放这些参数.•如果函数分配了空间,需要由调用者释放.•参数是否可以为NULL.•是否存在函数使用上的性能隐患.•如果函数是可重入的,其同步前提是什么?
举例如下: //Returnsaniteratorforthistable.Itistheclient's//responsibilitytodeletetheiteratorwhenitisdonewithit,//anditmustnotusetheiteratoroncetheGargantuanTableobject//onwhichtheiteratorwascreatedhasbeendeleted.////Theiteratorisinitiallypositionedatthebeginningofthetable.////Thismethodisequivalentto://Iterator*iter=table->NewIterator();//iter->Seek("");//returniter; //Ifyouaregoingtoimmediatelyseektoanotherplaceinthe//returnediterator,itwillbefastertouseNewIterator()//andavoidtheextraseek.Iterator*GetIterator()const; 但

标签: #cd #cute #快捷键 #偏高 #cpdd #蛋白 #快捷键 #cf