一般地,.cc[1]文件都有一个对应的.h文件。但是有一些常见的例外情况,比如单元测试和只含有main()函数的小型源文件。
头文件的正确运用,可以极大地提高代码的可读性,控制代码的规模和提高软件的性能。
下列规则有助于避免头文件使用中容易产生的诸多错误。
1.1 利用 #define防止多重包含
头文件应该使用#define定义预编译标识符来标识当前头文件,以来避免多重包含。标识符命名规则是:[项目名]_[路径名]_[文件名]_H_。
为了保证唯一性,标识符应当以头文件在项目中的全路径来命令。例如,在项目foo中的文件foo/src/bar/baz.h应该通过下面代码来进行保护:
#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_
...
#endif // FOO_BAR_BAZ_H_
可以使用前置声明时,就不要使用#include。
源代码文件中包含一个头文件就形成了一个新的依赖关系,一旦修改此头文件,就得重新编译源代码文件。如果某个头文件中包含了别的头文件,那么修改任何一个头文件,都得重新编译包含这个头文件的源代码文件。因此,要尽量减少包含关系,尤其是头文件的相互包含。
在头文件中使用前置声明,可以有效减少头文件中包含头文件的数量。例如,头文件中要使用File类,但是却用不到File类的声明。这时可以不包含File类的头文件(#include “file/base/file.h”),只要使用前置声明来声明类File就可以了。
那么,怎样才能在不使用类Foo的定义就可以使用类Foo呢?可以这样:
声明数据成员类型Foo*或Foo&。
声明类Foo的函数及其参数,甚至包括返回值。(这里有一个例外,就是当变量Foo或const Foo&具有单参数隐式转换的构造函数时,就需要类的完整声明来支持自动类型转换)
声明类Foo的静态数据成员变量。因为静态数据成员的定义不包含在类定义之内。
但是,如果你要继承Foo类或者有成员变量是Foo类型,则必须引用Foo类的头文件。
有时,可以使用指针(scoped_ptr更佳)来代替对象作为成员变量。但是,这将影响代码可读性并降低程序执行效率。因此,如果仅是出于减少包含头文件数量的考虑,就不要这么做了。
当然,.cc文件通常需要所有类的定义,因此需要包含多个头文件。
提示:如果要在在源代码文件中使用Foo,则应当主动通过#include或前置声明引入Foo的定义。不要依赖于传递关系包含的头文件。例外情况:如果Foo在myfile.cc文件中使用,则可以在myfile.h文件(而不是文件myfile.cc.)中包含(或前置声明)类Foo。
1.3 内联函数
只有当函数体足够小时(不多于十行)才使用内联函数。
定义:内联函数与普通函数不同,不需要经过函数调用机制来调用。在编译内联函数时,编译器是先把内联函数体内的代码拷贝到调用内联函数的位置(这个过程称为内联展开),替换原有的调用语句,然后再编译。
优点:函数体较小时,使用内联方式可以提升程序执行效率。对于取值函数(accessor)[2]、赋值函数(mutator)[3],以及所有函数体短小而又要求高执行效率的函数,都应当声明为内联函数。
劣势:内联函数的滥用将导致程序运行缓慢。使用内联方式可能会增大或减小程序的体积,这与函数体本身的大小有关。将函数体很小的取值函数声明为内联方式,通常会减小程序体积;而将一个比较大的函数声明为内联方式,则将明显增大程序体积。另一方面,在现代处理器上,短代码运行效率优于老式处理器,因为现代处理器有更好的指令缓存机制(因此,无须使用内联函数)。
结论:
一个比较好的规则就是当函数代码多于10行时,就不使用内联方式。要特别注意析构函数,它们的实际代码行数很可能大于看到的行数,因为析构函数中可能会隐含成员,还可能会包括父类的析构函数!
另一个好的规则是,具有循环体或分支(switch语句)结构的函数,不宜内联(除非这些循环或分支语句一般不会被执行)。
即使声明为内联函数,也不一定会被编译为内联函数,这一点很重要。比如虚函数和递归函数一般就不会编译为内联函数。通常,递归函数不应声明为内联函数。将虚函数声明为内联的主要是为了方便或者在文档中说明它的功能(比如取值和赋值函数)。
1.4 以inl.h为后缀的文件
复杂内联函数,应当在以-inl.h为后缀的头文件中进行定义。
内联函数的定义需要放在头文件内,这样编译器才可以在调用处将其内联展开。但是,实现代码应当放在.cc文件当中,除非可以提高可读性或运行效率,否则不应该把太多实现代码放在.h文件中。
如果内联函数足够短小(只含有极少或是没有逻辑语句),则应该将内联函数放进.h文件。比如,取值函数和赋值函数就应当放在类的定义里面。为了实现和调用的方便,更复杂的内联函数也应当定义在.h文件中。如果这样做影响到了代码的可读性,那么可以将这些内联函数放进一个单独的头文件(以-inl.h为后缀)。这样可以把内联函数的实现与类定义分开,同时,又不影响在别处包含内联函数实现。
以-inl.h为后缀的头文件的另一个用途是用来定义函数模板。这样做可以提高模板定义的可读性。
别忘了,与别的头文件一样,-inl.h为后缀的头文件同样应当利用 #define防止多重包含。
定义函数时,参数的顺序应为:输入参数在前,输出参数在后。
C/C++语言中函数的参数分为输入参数与输出参数,也可以既是输入又是输出参数(简称为输入/输出参数)。输入参数通常是值类型或者常引用类型,而输出参数与输入/输出参数则为非常指针。参数排序时,输入参数应排在其他参数前面。特别地,不能因为参数是新添加的而简单地将参数排到最后,要把新的输入参数排在输出参数前面。
这条规则并不要求严格遵守。因为输入/输出参数(通常是类或结构体)很特别,会影响到这条规则。有时为了保持与相关函数的一致性,则不得不违反这条规则。
为了可读性并避免隐性依赖(hidden dependencies[4]),包含头文件的顺序应当遵循这样的标准:C库文件,C++库文件,其他的库文件.h,本工程中的库文件.h。
项目中定义的头文件,都应该放在项目源文件夹中,并且包含语句中不应当含有UNIX文件夹缩写符号“.”(表示当前文件夹),或“..”(表示上一级文件夹)。例如,google-awesome-project/src/base/logging.h应当使用这样的包含语句:
#include "base/logging.h"
如果文件dir/foo.cc主要用来测试dir2/foo2.h,则应这样排列头文件包含顺序:
dir2/foo2.h(优先排序——下面会详细说明原因)
C库文件
C++库文件
其他.h库文件
当前项目.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" // Preferred location.
#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"