c++ code style.docx
- 文档编号:6653623
- 上传时间:2023-05-10
- 格式:DOCX
- 页数:48
- 大小:68.36KB
c++ code style.docx
《c++ code style.docx》由会员分享,可在线阅读,更多相关《c++ code style.docx(48页珍藏版)》请在冰点文库上搜索。
c++codestyle
背景¶
C++是Google大部分开源项目的主要编程语言.正如每个C++程序员都知道的,C++有很多强大的特性,但这种强大不可避免的导致它走向复杂,使代码更容易产生bug,难以阅读和维护.
本指南的目的是通过详细阐述C++注意事项来驾驭其复杂性.这些规则在保证代码易于管理的同时,高效使用C++的语言特性.
风格,亦被称作可读性,也就是指导C++编程的约定.使用术语“风格”有些用词不当,因为这些习惯远不止源代码文件格式化这么简单.
使代码易于管理的方法之一是加强代码一致性.让任何程序员都可以快速读懂你的代码这点非常重要.保持统一编程风格并遵守约定意味着可以很容易根据“模式匹配”规则来推断各种标识符的含义.创建通用,必需的习惯用语和模式可以使代码更容易理解.在一些情况下可能有充分的理由改变某些编程风格,但我们还是应该遵循一致性原则,尽量不这么做.
本指南的另一个观点是C++特性的臃肿.C++是一门包含大量高级特性的庞大语言.某些情况下,我们会限制甚至禁止使用某些特性.这么做是为了保持代码清爽,避免这些特性可能导致的各种问题.指南中列举了这类特性,并解释为什么这些特性被限制使用.
Google主导的开源项目均符合本指南的规定.
注意:
本指南并非C++教程,我们假定读者已经对C++非常熟悉.
1.头文件¶
通常每一个 .cc 文件都有一个对应的 .h 文件.也有一些常见例外,如单元测试代码和只包含 main() 函数的 .cc 文件.
正确使用头文件可令代码在可读性、文件大小和性能上大为改观.
下面的规则将引导你规避使用头文件时的各种陷阱.
1.1.#define保护¶
Tip
所有头文件都应该使用 #define 防止头文件被多重包含,命名格式当是:
为保证唯一性,头文件的命名应该依据所在项目源代码树的全路径.例如,项目 foo 中的头文件 foo/src/bar/baz.h 可按如下方式保护:
#ifndefFOO_BAR_BAZ_H_#defineFOO_BAR_BAZ_H_…#endif//FOO_BAR_BAZ_H_
1.2.头文件依赖¶
Tip
能用前置声明的地方尽量不使用 #include.
当一个头文件被包含的同时也引入了新的依赖,一旦该头文件被修改,代码就会被重新编译.如果这个头文件又包含了其他头文件,这些头文件的任何改变都将导致所有包含了该头文件的代码被重新编译.因此,我们倾向于减少包含头文件,尤其是在头文件中包含头文件.
使用前置声明可以显著减少需要包含的头文件数量.举例说明:
如果头文件中用到类 File,但不需要访问 File 类的声明,头文件中只需前置声明 class File; 而无须 #include "file/base/file.h".
不允许访问类的定义的前提下,我们在一个头文件中能对类 Foo 做哪些操作?
∙我们可以将数据成员类型声明为 Foo * 或 Foo &.
∙我们可以将函数参数/返回值的类型声明为 Foo (但不能定义实现).
∙我们可以将静态数据成员的类型声明为 Foo,因为静态数据成员的定义在类定义之外.
反之,如果你的类是 Foo 的子类,或者含有类型为 Foo 的非静态数据成员,则必须包含 Foo 所在的头文件.
有时,使用指针成员(如果是 scoped_ptr 更好)替代对象成员的确是明智之选.然而,这会降低代码可读性及执行效率,因此如果仅仅为了少包含头文件,还是不要这么做的好.
当然 .cc 文件无论如何都需要所使用类的定义部分,自然也就会包含若干头文件.
1.3.内联函数¶
Tip
只有当函数只有10行甚至更少时才将其定义为内联函数.
定义:
当函数被声明为内联函数之后,编译器会将其内联展开,而不是按通常的函数调用机制进行调用.
优点:
当函数体比较小的时候,内联该函数可以令目标代码更加高效.对于存取函数以及其它函数体比较短,性能关键的函数,鼓励使用内联.
缺点:
滥用内联将导致程序变慢.内联可能使目标代码量或增或减,这取决于内联函数的大小.内联非常短小的存取函数通常会减少代码大小,但内联一个相当大的函数将戏剧性的增加代码大小.现代处理器由于更好的利用了指令缓存,小巧的代码往往执行更快。
结论:
一个较为合理的经验准则是,不要内联超过10行的函数.谨慎对待析构函数,析构函数往往比其表面看起来要更长,因为有隐含的成员和基类析构函数被调用!
另一个实用的经验准则:
内联那些包含循环或 switch 语句的函数常常是得不偿失(除非在大多数情况下,这些循环或 switch 语句从不被执行).
有些函数即使声明为内联的也不一定会被编译器内联,这点很重要;比如虚函数和递归函数就不会被正常内联.通常,递归函数不应该声明成内联函数.(YuleFox注:
递归调用堆栈的展开并不像循环那么简单,比如递归层数在编译时可能是未知的,大多数编译器都不支持内联递归函数).虚函数内联的主要原因则是想把它的函数体放在类定义内,为了图个方便,抑或是当作文档描述其行为,比如精短的存取函数.
1.4.-inl.h文件¶
Tip
复杂的内联函数的定义,应放在后缀名为 -inl.h 的头文件中.
内联函数的定义必须放在头文件中,编译器才能在调用点内联展开定义.然而,实现代码理论上应该放在 .cc 文件中,我们不希望 .h 文件中有太多实现代码,除非在可读性和性能上有明显优势.
如果内联函数的定义比较短小,逻辑比较简单,实现代码放在 .h 文件里没有任何问题.比如,存取函数的实现理所当然都应该放在类定义内.出于编写者和调用者的方便,较复杂的内联函数也可以放到 .h 文件中,如果你觉得这样会使头文件显得笨重,也可以把它萃取到单独的 -inl.h 中.这样把实现和类定义分离开来,当需要时包含对应的 -inl.h 即可。
-inl.h 文件还可用于函数模板的定义.从而增强模板定义的可读性.
别忘了 -inl.h 和其他头文件一样,也需要 #define 保护.
1.5.函数参数的顺序¶
Tip
定义函数时,参数顺序依次为:
输入参数,然后是输出参数.
C/C++函数参数分为输入参数,输出参数,和输入/输出参数三种.输入参数一般传值或传 const 引用,输出参数或输入/输出参数则是非-const 指针.对参数排序时,将只输入的参数放在所有输出参数之前.尤其是不要仅仅因为是新加的参数,就把它放在最后;即使是新加的只输入参数也要放在输出参数.
这条规则并不需要严格遵守.输入/输出两用参数(通常是类/结构体变量)把事情变得复杂,为保持和相关函数的一致性,你有时不得不有所变通.
1.6. #include 的路径及顺序¶
Tip
使用标准的头文件包含顺序可增强可读性,避免隐藏依赖:
C库,C++库,其他库的 .h,本项目内的 .h.
项目内头文件应按照项目源代码目录树结构排列,避免使用UNIX特殊的快捷目录 . (当前目录)或 .. (上级目录).例如, google-awesome-project/src/base/logging.h 应该按如下方式包含:
#include“base/logging.h”
又如, dir/foo.cc 的主要作用是实现或测试 dir2/foo2.h 的功能, foo.cc 中包含头文件的次序如下:
1.dir2/foo2.h (优先位置,详情如下)
2.C系统文件
3.C++系统文件
4.其他库的 .h 文件
5.本项目内 .h 文件
这种排序方式可有效减少隐藏依赖.我们希望每一个头文件都是可被独立编译的(yospaly译注:
即该头文件本身已包含所有必要的显式依赖),最简单的方法是将其作为第一个 .h 文件 #included 进对应的 .cc.
dir/foo.cc 和 dir2/foo2.h 通常位于同一目录下(如 base/basictypes_unittest.cc 和 base/basictypes.h),但也可以放在不同目录下.
按字母顺序对头文件包含进行二次排序是不错的主意(yospaly译注:
之前已经按头文件类别排过序了).
举例来说, google-awesome-project/src/foo/internal/fooserver.cc 的包含次序如下:
#include"foo/public/fooserver.h" //优先位置 #include
译者(YuleFox)笔记¶
1.避免多重包含是学编程时最基本的要求;
2.前置声明是为了降低编译依赖,防止修改一个头文件引发多米诺效应;
3.内联函数的合理使用可提高代码执行效率;
4.-inl.h 可提高代码可读性(一般用不到吧:
D);
5.标准化函数参数顺序可以提高可读性和易维护性(对函数参数的堆栈空间有轻微影响,我以前大多是相同类型放在一起);
6.包含文件的名称使用 . 和 .. 虽然方便却易混乱,使用比较完整的项目路径看上去很清晰,很条理,包含文件的次序除了美观之外,最重要的是可以减少隐藏依赖,使每个头文件在“最需要编译”(对应源文件处:
D)的地方编译,有人提出库文件放在最后,这样出错先是项目内的文件,头文件都放在对应源文件的最前面,这一点足以保证内部错误的及时发现了.
2.作用域¶
2.1.名字空间¶
Tip
鼓励在 .cc 文件内使用匿名名字空间.使用具名的名字空间时,其名称可基于项目名或相对路径.不要使用 using关键字.
定义:
名字空间将全局作用域细分为独立的,具名的作用域,可有效防止全局作用域的命名冲突.
优点:
虽然类已经提供了(可嵌套的)命名轴线(YuleFox注:
将命名分割在不同类的作用域内),名字空间在这基础上又封装了一层.
举例来说,两个不同项目的全局作用域都有一个类 Foo,这样在编译或运行时造成冲突.如果每个项目将代码置于不同名字空间中,project1:
:
Foo 和 project2:
:
Foo 作为不同符号自然不会冲突.
缺点:
名字空间具有迷惑性,因为它们和类一样提供了额外的(可嵌套的)命名轴线.
在头文件中使用匿名空间导致违背C++的唯一定义原则(OneDefinitionRule(ODR)).
结论:
根据下文将要提到的策略合理使用命名空间.
2.1.1.匿名名字空间¶
∙在 .cc 文件中,允许甚至鼓励使用匿名名字空间,以避免运行时的命名冲突:
namespace { //.cc文件中 //名字空间的内容无需缩进 enum { kUNUSED, kEOF, kERROR }; //经常使用的符号 bool AtEof() {return pos_ == kEOF; } //使用本名字空间内的符号EOF } //namespace
然而,与特定类关联的文件作用域声明在该类中被声明为类型,静态数据成员或静态成员函数,而不是匿名名字空间的成员.如上例所示,匿名空间结束时用注释 // namespace 标识.
∙不要在 .h 文件中使用匿名名字空间.
2.1.2.具名的名字空间¶
具名的名字空间使用方式如下:
∙用名字空间把文件包含, gflags 的声明/定义,以及类的前置声明以外的整个源文件封装起来,以区别于其它名字空间:
//.h文件namespacemynamespace{//所有声明都置于命名空间中//注意不要使用缩进classMyClass{public:
…voidFoo();};}//namespacemynamespace
//.cc文件namespacemynamespace{//函数定义都置于命名空间中voidMyClass:
:
Foo(){…}}//namespacemynamespace
通常的 .cc 文件包含更多,更复杂的细节,比如引用其他名字空间的类等.
#include“a.h”DEFINE_bool(someflag,false,“dummyflag”);classC;//全局名字空间中类C的前置声明namespacea{classA;}//a:
:
A的前置声明namespaceb{…codeforb…//b中的代码}//namespaceb
∙不要在名字空间 std 内声明任何东西,包括标准库的类前置声明.在 std 名字空间声明实体会导致不确定的问题,比如不可移植.声明标准库下的实体,需要包含对应的头文件.
∙最好不要使用 ``using``关键字,以保证名字空间下的所有名称都可以正常使用.
//禁止——污染名字空间 using namespace foo;
∙在 .cc 文件, .h 文件的函数,方法或类中,可以使用 ``using``关键字.
//允许:
.cc文件中 //.h文件的话,必须在函数,方法或类的内部使用 using :
:
foo:
:
bar;
∙在 .cc 文件, .h 文件的函数,方法或类中,允许使用名字空间别名.
//允许:
.cc文件中 //.h文件的话,必须在函数,方法或类的内部使用 namespace fbz = :
:
foo:
:
bar:
:
baz;
2.2.嵌套类¶
Tip
当公有嵌套类作为接口的一部分时,虽然可以直接将他们保持在全局作用域中,但将嵌套类的声明置于名字空间内是更好的选择.
定义:
在一个类内部定义另一个类;嵌套类也被称为 成员类(memberclass).
classFoo{private:
//Bar是嵌套在Foo中的成员类classBar{…};};
优点:
当嵌套(或成员)类只被外围类使用时非常有用;把它作为外围类作用域内的成员,而不是去污染外部作用域的同名类.嵌套类可以在外围类中做前置声明,然后在 .cc 文件中定义,这样避免在外围类的声明中定义嵌套类,因为嵌套类的定义通常只与实现相关.
缺点:
嵌套类只能在外围类的内部做前置声明.因此,任何使用了 Foo:
:
Bar* 指针的头文件不得不包含类 Foo 的整个声明.
结论:
不要将嵌套类定义成公有,除非它们是接口的一部分,比如,嵌套类含有某些方法的一组选项.
2.3.非成员函数,静态成员函数,和全局函数¶
Tip
使用静态成员函数或名字空间内的非成员函数,尽量不要用裸的全局函数.
优点:
某些情况下,非成员函数和静态成员函数是非常有用的,将非成员函数放在名字空间内可避免污染全局作用域.
缺点:
将非成员函数和静态成员函数作为新类的成员或许更有意义,当它们需要访问外部资源或具有重要的依赖关系时更是如此.
结论:
有时,把函数的定义同类的实例脱钩是有益的,甚至是必要的.这样的函数可以被定义成静态成员,或是非成员函数.非成员函数不应依赖于外部变量,应尽量置于某个名字空间内.相比单纯为了封装若干不共享任何静态数据的静态成员函数而创建类,不如使用命名空间.
定义在同一编译单元的函数,被其他编译单元直接调用可能会引入不必要的耦合和链接时依赖;静态成员函数对此尤其敏感.可以考虑提取到新类中,或者将函数置于独立库的名字空间内.
如果你必须定义非成员函数,又只是在 .cc 文件中使用它,可使用匿名名字空间或 static 链接关键字(如 static int Foo() {...})限定其作用域.
2.4.局部变量¶
Tip
将函数变量尽可能置于最小作用域内,并在变量声明时进行初始化.
C++允许在函数的任何位置声明变量.我们提倡在尽可能小的作用域中声明变量,离第一次使用越近越好.这使得代码浏览者更容易定位变量声明的位置,了解变量的类型和初始值.特别是,应使用初始化的方式替代声明再赋值,比如:
int i; i = f(); //坏——初始化和声明分离 nt j = g(); //好——初始化时声明
注意,GCC可正确实现了 for (int i = 0; i < 10; ++i) (i 的作用域仅限 for 循环内),所以其他 for 循环中可以重新使用 i.在 if 和while 等语句中的作用域声明也是正确的,如:
while(constchar*p=strchr(str,‘/’))str=p+1;
Warning
如果变量是一个对象,每次进入作用域都要调用其构造函数,每次退出作用域都要调用其析构函数.
//低效的实现 for (int i = 0; i < 1000000; ++i) { Foo f; //构造函数和析构函数分别调用1000000次!
f.DoSomething(i); }
在循环作用域外面声明这类变量要高效的多:
Foo f; //构造函数和析构函数只调用1次 for (int i = 0; i < 1000000; ++i) { f.DoSomething(i); }
2.5.静态和全局变量¶
Tip
禁止使用 class 类型的静态或全局变量:
它们会导致很难发现的bug和不确定的构造和析构函数调用顺序.
静态生存周期的对象,包括全局变量,静态变量,静态类成员变量,以及函数静态变量,都必须是原生数据类型(POD:
PlainOldData):
只能是 int, char, float,和 void,以及POD类型的数组/结构体/指针.永远不要使用函数返回值初始化静态变量;不要在多线程代码中使用非const 的静态变量.
不幸的是,静态变量的构造函数,析构函数以及初始化操作的调用顺序在C++标准中未明确定义,甚至每次编译构建都有可能会发生变化,从而导致难以发现的bug.比如,结束程序时,某个静态变量已经被析构了,但代码还在跑–其它线程很可能–试图访问该变量,直接导致崩溃.
所以,我们只允许POD类型的静态变量.本条规则完全禁止 vector (使用C数组替代), string (使用 const char*),及其它以任意方式包含或指向类实例的东东,成为静态变量.出于同样的理由,我们不允许用函数返回值来初始化静态变量.
如果你确实需要一个 class` 类型的静态或全局变量, 可以考虑在 ``main() 函数或 pthread_once() 内初始化一个你永远不会回收的指针.
Note
yospaly译注:
上文提及的静态变量泛指静态生存周期的对象,包括:
全局变量,静态变量,静态类成员变量,以及函数静态变量.
译者(YuleFox)笔记¶
1.cc 中的匿名名字空间可避免命名冲突,限定作用域,避免直接使用 using 关键字污染命名空间;
2.嵌套类符合局部使用原则,只是不能在其他头文件中前置声明,尽量不要 public;
3.尽量不用全局函数和全局变量,考虑作用域和命名空间限制,尽量单独形成编译单元;
4.多线程中的全局变量(含静态成员变量)不要使用 class 类型(含STL容器),避免不明确行为导致的bug.
5.作用域的使用,除了考虑名称污染,可读性之外,主要是为降低耦合,提高编译/执行效率.
3.类¶
类是C++中代码的基本单元.显然,它们被广泛使用.本节列举了在写一个类时的主要注意事项.
3.1.构造函数的职责¶
Tip
构造函数中只进行那些没什么意义的(trivial,YuleFox注:
简单初始化对于程序执行没有实际的逻辑意义,因为成员变量“有意义”的值大多不在构造函数中确定)初始化,可能的话,使用 Init() 方法集中初始化有意义的(non-trivial)数据.
定义:
在构造函数体中进行初始化操作.
优点:
排版方便,无需担心类是否已经初始化.
缺点:
在构造函数中执行操作引起的问题有:
∙构造函数中很难上报错误, 不能使用异常.
∙操作失败会造成对象初始化失败,进入不确定状态.
∙如果在构造函数内调用了自身的虚函数,这类调用是不会重定向到子类的虚函数实现.即使当前没有子类化实现,将来仍是隐患.
∙如果有人创建该类型的全局变量(虽然违背了上节提到的规则),构造函数将先 main() 一步被调用,有可能破坏构造函数中暗含的假设条件.例如, gflags 尚未初始化.
结论:
如果对象需要进行有意义的(non-trivial)初始化,考虑使用明确的 Init() 方法并(或)增加一个成员标记用于指示对象是否已经初始化成功.
3.2.默认构造函数¶
Tip
如果一个类定义了若干成员变量又没有其它构造函数,必须定义一个默认构造函数.否则编译器将自动生产一个很糟糕的默认构造函数.
定义:
new 一个不带参数的类对象时,会调用这个类的默认构造函数.用 new[] 创建数组时,默认构造函数则总是被调用.
优点:
默认将结构体初始化为“无效”值,使调试更方便.
缺点:
对代码编写者来说,这是多余的工作.
结论:
如果类中定义了成员变量,而且没有提供其它构造函数,你必须定义一个(不带参数的)默认构造函数.把对象的内部状态初始化成一致/有效的值无疑是更合理的方式.
这么做的原因是:
如果你没有提供其它构造函数,又没有定义默认构造函数,编译器将为你自动生成一个.编译器生成的构造函数并不会对对象进行合理的初始化.
如果你定义的类继承现有类,而你又没有增加新的成员变量,则不需要为新类定义默认构造函数.
3.3.显式构造函数¶
Tip
对单个参数的构造函数使用C++关键字 explicit.
定义:
通常,如果构造函数只有一个参数,可看成是一种隐式转换.打个比方,如果你定义了 Foo:
:
Foo(string name),接着把一个字符串传给一个以Foo 对象为参数的函数,构造函数 Foo:
:
Foo(string name) 将被调用,并将该字符串转换为一个 Foo 的临时对象传给调用函数.看上去很方便,但如果你并不希望如此通过转换生成一个新对象的话,麻烦也随之而来.为避免构造函数被调用造成隐式转换,可以将其声明为explicit.
优点:
避免不合时宜的变换.
缺点:
无
结论:
所有单参数构造函数都必须是显式的.在类定义中,将关键字 explici
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- c+ code style