您的位置:首页 > 编程语言 > C语言/C++

Google C++ Style Guide私人解读(1)

2011-05-21 00:23 429 查看


基于Revision 3.188

二 头文件

一般来说,每个.cc文件都应该有一个关联的.h文件。有一些常见的例外,如单元测试代码和只包含一个main()函数的.cc小文件,不在此列。
头文件的正确使用能够给您代码的可读性、尺寸和性能带来极大的不同。
以下规则会让您避开头文件使用的误区。

#define守卫

所有头文件都要用#define守卫来避免多次包含。符号命名格式应为<PROJECT>_<PATH>_<FILE>_H_。
为了保证唯一性,符号命名应该基于文件在项目源码树中的全路径。比如说,foo项目中的文件foo/src/bar/baz.h应该放置以下的守卫:
#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_

...

#endif // FOO_BAR_BAZ_H_

个人观点

Guard翻译成守卫有点无厘头,曾想翻译成“后卫”,似乎更二,还是将就吧。这一条没提到类库的头文件里怎么写,看了一下glog/src/glog/log_severity.h,是BASE_LOG_SEVERITY_H__。有个问题:如果重构代码时调整目录结构的话,为了遵守规范,所有头文件都得改。

头文件依赖关系

只要能用前递声明,就不用#include。
每当在.cc文件里包含一个头文件,你就引入了一个依赖关系,只要这个头文件有改动,你的.cc文件就得重新编译。如果你的头文件包含了别的头文件,那么这些“别的头文件”只要有任何风吹草动,任何代码只要包含了你的头文件,就得重新编译。因此,我们更希望把包含数量减少至最低限度,尤其是头文件中包含的头文件。
通过使用前递声明可以大量减少您自己的头文件需要包含的头文件数量。举个例子,如果你的头文件用到了File类,而你的用法并不需要涉及File类的声明(此处的“声明”疑为“定义”之误),那你就可以在头文件里使用前递声明“class File;”,而不必“#include "file/base/file.h"”。
那么,在头文件里对Foo这个类的哪些用法不需要它的定义呢?
l 可以声明Foo*和Foo&型的数据成员。
l 可以声明参数或返回值类型为Foo的函数(但不能定义)。(例外:如果参数类型为Foo或const Foo&,而Foo又有个未声明为explicit的单参数构造函数,我们就需要Foo的完整定义,以支持自动类型转换)
l 可以声明Foo类型的静态数据成员,因为静态数据成员是类外定义。
另一方面,如果继承了Foo类,或是声明了Foo型的数据成员,就必须包含Foo的头文件了。
有的时侯,有理由用指针成员(或者更好的scoped_ptr)代替对象成员。然而,这让代码难懂,更收到效率罚单,所以如果只是为了减少头文件包含,那还是避免这种做法为好。
当然,.如果cc文件用到某些类的话,一般都需要它们的定义,所以经常要包含好几个头文件。
注意:如果需要用到Foo这个符号,你应该自己引入它的定义,或者用#include,或者用前递声明。不要依赖间接包含进来的头文件引入的符号。有个例外:如果myfile.cc用到了Foo,那在myfile.h而非myfile.cc中#include(或前递声明)Foo是没问题。

个人观点

这条和Effective C++ 3rd的item 31一致。编译依赖也是个老话题了,小贝的《大规模C++程序设计》里就提过。比较困惑的一点是动态链接库,理论上讲,如果对一个动态链接库的头文件有任何修改,则所有依赖其的程序都应该重新编译一遍,才不违反ODL,但实际上只要不影响动态库里类的行为,不编译也说得过去,而且不编译才符合动态库的要义。如果改变了动态库里C++类的大小、删掉某个函数、增加/修改一个虚函数,就不能不重新编译所有依赖这个动态库的程序了,没有完善的module机制真是悲剧,也不知有没有朋友详细了解过C++0x的module提案,好不好使啊?
前递声明出来的类是incomplete class type,除了上面提到和类成员相关的几处外,incomplete type还能用在:全局变量声明(extern)、全局函数声明中的参数和返回值、模板实参,不过都有很多限制,还是不要使用的好。另外,声明一个返回值为incomplete type的虚函数时也有限制:在重写虚函数并使用covariant返回值的时候,返回值的类型必须有完整定义(否则谁知道它是不是子类呢)。
需要用到Foo时要求自己将其#include这条要求不错,一是看代码时从#include块一眼就能看出用到些什么,二是免得不小心用到不该用的东西,不过二暂时还没想到例子。

内联函数

函数很小,比如说10行以下时,才定义成内联函数。
定义:你能够将函数声明为允许编译器内联展开对它们的调用,而不是通过通常的函数调用机制来调用。
优点:只要内联函数很小,编译器将其内联展开就能生成更高效的代码。你可以放心地将set/get函数以及其它较短的性能关键函数声明为内联。
缺点:过度使用内联会让程序在实际中跑得更慢。根据函数的长短,将其内联可能导致生成的代码尺寸变大或变小。内联一个很小的get/set函数通常会减小代码尺寸,而内联一个很大的函数会使代码尺寸大大增加。在现代处理器上,通常尺寸更小的代码运行得更快,因为指令缓存将得到更好的利用。
决定:近期的经验总结是不要内联一个10行以上的函数。对待析构函数要小心,它们经常比乍看起来更长,因其隐含了对成员和基类析构函数的调用。
另一个有用的经验总结是:对具有循环或switch语句的函数进行内联在典型情况下效果不大(除非循环或switch语句一般不会执行)。
有件重要的事情必须知道:即使声明成内联,函数也不是总会内联;例如,虚函数和递归函数正常情况下都不会内联。递归函数通常不应内联,而将虚函数设为内联主要是为了将其定义放在类定义里,无论是出于方便考虑还是为了文档化其行为,如set/get函数。

个人观点

这条基本和Effective C++ 3rd中Item 30一样。
英语为啥支持这么长的句子呢,一堆which,看懂一句话既要look ahead,又要look back,谁有心得?

-inl.h文件

有必要时,可以用文件名后缀为-inl.h的文件放置复杂内联函数的定义。
内联函数的定义必须放在头文件里,这样编译器才能将此定义在调用处展开。然而,实现代码正确的位置还是在.cc文件里,我们不希望在.h文件中有太多的实际代码,除非这样做能提升可读性或性能。
如果一个内联函数的定义很短,几乎不包含什么逻辑,那就应该放在头文件里。例如,set/get函数一定要放在类定义中。如能方便函数的实现者和调用者,更复杂的内联函数也可以放到.h文件中——如果这样让头文件变得笨重,你也可以把函数代码放到一个独立的-inl.h文件里。这样把实现从类定义中分离了出来,而在必要时仍能够把实现包含进来。
-inl.h文件的另外一个用法是用来定义函数模板。这种方法能用来保持你的模板定义容易读懂。
不要忘了,一个.inl.h文件和别的头文件一样,需要一个#define守卫。

个人观点

除了能把template的定义和实现分开放,实在想不到这么做的理由……不过据说Linux里有这么搞的,一个.c文件到处包含。

函数形参的顺序

定义一个函数时,形参顺序是:先输入,后输出。
C/C++的形参或是函数的输入,或是函数的输出,或者既是输入又是输出。入参通常是值类型或const引用,而出参及输入/输出形参是非const指针。当安排函数形参的顺序时,将所有只做入参的形参放置在任何出参之前。特别地,为函数新增形参时,不要仅仅因为是新增的,就把它们放到参数列表末尾;新的只入形参仍要放到所有出参前面。
这并非一个硬性规定,既做输入又做输出的形参(常为类/结构)使情况变得复杂一些,而且,为了和相关的函数保持一致,可能需要放宽规则,就像你总是做的那样。

个人观点

先入后出顺序和出参类型的规定总感觉不太舒服,比方为std::string定义一个replace函数:
std::string& replace(

std::string& s,

const std::string& from,

const std::string& to

);

把s放在最前并声明为引用更符合我的口味,好在本条并非硬性规定(希望翻译没错)。

名字和包含顺序

为了可读性,也为了避免隐藏的依赖关系,使用标准顺序:C标准库,C++标准库,其它库的.h,你项目的.h。
所有的项目头文件在列出时都应该使用从项目源码目录开始的相对路径,不使用UNIX目录的快捷方式“.”(当前目录)和“..”(上级目录)。例如:google-awesome-project/src/base/logging.h在包含时应该写作:
#include "base/logging.h"
在dir/foo.cc和dir/foo_test.cc——主要用来实现和测试dir2/foo2.h中的内容——中,按以下方式安排你的头文件包含顺序:
1. dir2/foo2.h(推荐放在此处——请看下面详述)
2. C的系统文件。
3. C++系统文件。
4. 其它库的.h文件。
5. 你项目的.h文件。
这种推荐的排序减少了隐藏的依赖关系。我们想让每个头文件能单独编译通过。达到这一目标最容易的方法是保证每个头文件都是某个.cc文件中第一个#include的.h文件。
dir/foo.cc和dir2/foo2.h一般放在同一个目录里(如base/basictypes_test.cc和base/basictypes.h),但也可以在不同目录里。
在每段包含语句中都按字母序排序则更加宜人。
例如,google-awesome-project/src/foo/internal/fooserver.cc中的包含语句可能是这样的:
#include "foo/public/fooserver.h" // 推荐放在此处。
#include <sys/types.h>
#include <unistd.h>
#include <hash_map>
#include <vector>
#include "base/basictypes.h"
#include "base/commandlineflags.h"
#include "foo/public/bar.h"

个人观点

本条的内容比较考究,为了避免间接使用某个符号(“头文件依赖关系”一节中提到),每个头文件都是某个.cc文件里#include段的排头兵,这样头文件就不用在注释里写上“得先包含那个,再包含这个”了,值得参考。
至于各类头文件的包含顺序,google自己也未能遵守,如glog/src/logging.cc中的包含顺序就和上面描述的不同,还是按个人习惯来吧。
#include时要求写明头文件完整路径很好,如果遵守这个条例,可以避免一些诡异的问题。比如我自己遇到过的一个问题:
项目用到一个开源类库IM,需要升级到新版本。旧版IM的头文件在/usr/include下,库文件在/usr/lib下,新版IM则安装在/usr/local/include/IM和/usr/local/lib下。
由于安装程序建了个子目录IM存放头文件,我们就把代码从#include <IM.h>改成#include <IM/IM.h>后,编译器默认先搜索/usr/local/,再搜索/usr/,按理说应该没有问题。但我们在rebuild时链接不过,大大的见鬼。
查了半天,终于查到是IM的头文件IM.h写得有问题,这个天杀的IM.h的内容如下:
#include <IMCore.h> //
注意是尖括号

#include <IMXXX.h> //
同上

编译过程如下,编译器看到尖括号,就跳过当前目录,直接搜索系统头文件路径,不在/usr/local/include里?那就去/usr/include下把旧爱找回来吧,于是IMCore.h和IMXXX.h都是在/usr/include里的旧版本。但链接器却找到了正确版本的库,于是出错。真是库不如新,头不如故。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: