Google C++ 风格指南内容整理

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Google C++ 风格指南内容整理相关的知识,希望对你有一定的参考价值。

之前一直没有全面的看过Google C++风格指南,现在很多公司进行C++开发都要求按照Google C++风格。在这个网站 http://zh-google-styleguide.readthedocs.org/en/latest/contents/  有人已经把其翻译成中文。为了便于以后查看,下面的内容完全是来自于这个网站,只是把多个网页的内容整理放在了一起。

 

1.      头文件

通常每一个.cc文件都有一个对应的.h文件。也有一些常见例外,如单元测试代码和只包含main()函数的.cc文件。

#define保护:所有头文件都应该使用#define 防止头文件被多重包含, 命名格式当是:<PROJECT>_<PATH>_<FILE>_H_

为保证唯一性,头文件的命名应该依据所在项目源代码树的全路径。例如,项目foo中的头文件foo/src/bar/baz.h可按如下方式保护:

 

[cpp] view plain copy
 技术分享技术分享
  1. #ifndef FOO_BAR_BAZ_H_  
  2. #define FOO_BAR_BAZ_H_  
  3. …  
  4. #endif // FOO_BAR_BAZ_H_  
头文件依赖:能用前置声明的地方尽量不使用 #include.

 

当一个头文件被包含的同时也引入了新的依赖,一旦该头文件被修改,代码就会被重新编译。如果这个头文件又包含了其他头文件,这些头文件的任何改变都将导致所有包含了该头文件的代码被重新编译。因此,我们倾向于减少包含头文件,尤其是在头文件中包含头文件。

使用前置声明可以显著减少需要包含的头文件数量。举例说明:如果头文件中用到类 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 应该按如下方式包含:

 

[cpp] view plain copy
 技术分享技术分享
  1. #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文件#include进对应的.cc.

dir/foo.cc和 dir2/foo2.h通常位于同一目录下(如 base/basictypes_unittest.cc 和 base/basictypes.h),但也可以放在不同目录下。

按字母顺序对头文件包含进行二次排序是不错的主意(yospaly译注:之前已经按头文件类别排过序了)。

举例来说,google-awesome-project/src/foo/internal/fooserver.cc的包含次序如下:

 

[cpp] view plain copy
 技术分享技术分享
  1. #include "foo/public/fooserver.h" // 优先位置  
  2. #include <sys/types.h>  
  3. #include <unistd.h>  
  4. #include <hash_map>  
  5. #include <vector>  
  6. #include "base/basictypes.h"  
  7. #include "base/commandlineflags.h"  
  8. #include "foo/public/bar.h"  
译者(YuleFox)笔记

 

(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文件中,允许甚至鼓励使用匿名名字空间,以避免运行时的命名冲突:

 

[cpp] view plain copy
 技术分享技术分享
  1. namespace {                             // .cc 文件中  
  2.   
  3. // 名字空间的内容无需缩进  
  4. enum { kUNUSED, kEOF, kERROR };         // 经常使用的符号  
  5. bool AtEof() { return pos_ == kEOF; }   // 使用本名字空间内的符号 EOF  
  6.   
  7. // namespace  
然而,与特定类关联的文件作用域声明在该类中被声明为类型,静态数据成员或静态成员函数,而不是匿名名字空间的成员.如上例所示,匿名空间结束时用注释 // namespace 标识.

 

不要在.h文件中使用匿名名字空间.

具名的名字空间使用方式如下:

(1)、用名字空间把文件包含,gflags的声明/定义,以及类的前置声明以外的整个源文件封装起来,以区别于其它名字空间:

 

[cpp] view plain copy
 技术分享技术分享
  1. // .h 文件  
  2. namespace mynamespace {  
  3.   
  4. // 所有声明都置于命名空间中  
  5. // 注意不要使用缩进  
  6. class MyClass {  
  7.     public:  
  8.     …  
  9.     void Foo();  
  10. };  
  11.   
  12. // namespace mynamespace  
[cpp] view plain copy
 技术分享技术分享
  1. // .cc 文件  
  2. namespace mynamespace {  
  3.   
  4. // 函数定义都置于命名空间中  
  5. void MyClass::Foo() {  
  6.     …  
  7. }  
  8.   
  9. // namespace mynamespace  
通常的.cc文件包含更多,更复杂的细节,比如引用其他名字空间的类等.

 

 

[cpp] view plain copy
 技术分享技术分享
  1. #include “a.h”  
  2.   
  3. DEFINE_bool(someflag, false, “dummy flag”);  
  4.   
  5. class C;                    // 全局名字空间中类 C 的前置声明  
  6. namespace a { class A; }    // a::A 的前置声明  
  7.   
  8. namespace b {  
  9.   
  10. …code for b…                // b 中的代码  
  11.   
  12. // namespace b  
(2)、不要在名字空间std内声明任何东西,包括标准库的类前置声明.在std名字空间声明实体会导致不确定的问题, 比如不可移植.声明标准库下的实体,需要包含对应的头文件.

 

(3)、最好不要使用"using"关键字, 以保证名字空间下的所有名称都可以正常使用.

 

[cpp] view plain copy
 技术分享技术分享
  1. // 禁止 —— 污染名字空间  
  2. using namespace foo;  
(4)、在.cc文件,.h文件的函数,方法或类中,可以使用"using"关键字.

 

 

[cpp] view plain copy
 技术分享技术分享
  1. // 允许: .cc 文件中  
  2. // .h 文件的话, 必须在函数, 方法或类的内部使用  
  3. using ::foo::bar;  
(5)、在.cc文件,.h文件的函数,方法或类中,允许使用名字空间别名.

 

 

[cpp] view plain copy
 技术分享技术分享
  1. // 允许: .cc 文件中  
  2. // .h 文件的话, 必须在函数, 方法或类的内部使用  
  3.   
  4. namespace fbz = ::foo::bar::baz;  
嵌套类:当公有嵌套类作为接口的一部分时,虽然可以直接将他们保持在全局作用域中,但将嵌套类的声明置于名字空间内是更好的选择.

 

在一个类内部定义另一个类;嵌套类也被称为成员类(memberclass).

 

[cpp] view plain copy
 技术分享技术分享
  1. class Foo {  
  2.   
  3. private:  
  4.     // Bar是嵌套在Foo中的成员类  
  5.     class Bar {  
  6.         …  
  7.     };  
  8.   
  9. };  
优点:当嵌套(或成员)类只被外围类使用时非常有用;把它作为外围类作用域内的成员, 而不是去污染外部作用域的同名类.嵌套类可以在外围类中做前置声明,然后在.cc文件中定义,这样避免在外围类的声明中定义嵌套类, 因为嵌套类的定义通常只与实现相关.

 

缺点:嵌套类只能在外围类的内部做前置声明.因此,任何使用了Foo::Bar*指针的头文件不得不包含类Foo的整个声明.

结论:不要将嵌套类定义成公有,除非它们是接口的一部分,比如,嵌套类含有某些方法的一组选项.

非成员函数、静态成员函数和全局函数:使用静态成员函数或名字空间内的非成员函数,尽量不要用裸的全局函数.

优点:某些情况下,非成员函数和静态成员函数是非常有用的,将非成员函数放在名字空间内可避免污染全局作用域.

缺点:将非成员函数和静态成员函数作为新类的成员或许更有意义,当它们需要访问外部资源或具有重要的依赖关系时更是如此.

结论:(1)、有时,把函数的定义同类的实例脱钩是有益的,甚至是必要的.这样的函数可以被定义成静态成员,或是非成员函数.非成员函数不应依赖于外部变量,应尽量置于某个名字空间内.相比单纯为了封装若干不共享任何静态数据的静态成员函数而创建类,不如使用命名空间.(2)、定义在同一编译单元的函数,被其他编译单元直接调用可能会引入不必要的耦合和链接时依赖;静态成员函数对此尤其敏感.可以考虑提取到新类中,或者将函数置于独立库的名字空间内.(3)、如果你必须定义非成员函数,又只是在.cc文件中使用它,可使用匿名名字空间或static链接关键字(如static int Foo() {...})限定其作用域.

局部变量:将函数变量尽可能置于最小作用域内,并在变量声明时进行初始化.

C++允许在函数的任何位置声明变量.我们提倡在尽可能小的作用域中声明变量,离第一次使用越近越好.这使得代码浏览者更容易定位变量声明的位置,了解变量的类型和初始值.特别是,使用初始化的方式替代声明再赋值, 比如:

 

[cpp] view plain copy
 技术分享技术分享
  1. int i;  
  2. i = f(); // 坏——初始化和声明分离  
  3. int j = g(); // 好——初始化时声明  
注意,GCC可正确实现了 for(int i = 0; i < 10; ++i) (i的作用域仅限for循环内),所以其他for循环中可以重新使用i.在if和while等语句中的作用域声明也是正确的,如:

 

 

[cpp] view plain copy
 技术分享技术分享
  1. while (const char* p = strchr(str, ‘/’)) str = p + 1;  
Warning:如果变量是一个对象,每次进入作用域都要调用其构造函数,每次退出作用域都要调用其析构函数.

 

 

[cpp] view plain copy

以上是关于Google C++ 风格指南内容整理的主要内容,如果未能解决你的问题,请参考以下文章

C++代码风格指南总结

往期精选 | 分享Google C++风格指南

Google C++ 编程风格指南:类

转---Google Python编程风格指南

Google Java编程风格指南

Google的Java编程风格指南

(c)2006-2024 SYSTEM All Rights Reserved IT常识