一、头文件

通常每一个.cc文件(C++的源文件)都有一个对应的.h文件(头文件),也有一些例外,入单元测试代码和只包含main()的.cc文件。

正确使用头文件可令代码的可读性、文件大小和性能上大为改观。

下面的规则将引导你规避使用头文件时的各种麻烦。

1.#define 的保护

所有头文件都应该使用#define防止头文件被多重包含(multiple inclusion),命名格式当是:<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_

2.头文件依赖

使用前置声明(forward declarations)尽量减少.h文件中#include的数量。

当一个头文件被包含的同时会引入了一项新的依赖(dependency),只要该头文件被修改,代码就要重新编译。如果你的头文件包含了其他头文件,这些头文件的的任何改变也将导致那些包含了你的头文件重新编译。因此,我们宁可尽量减少包含头文件,尤其那些包含了其他头文件的。

使用前置声明可以显著减少需要包含的头文件的数量。举例说明:头文件中用到类File,但不需要访问File的声明,则头文件只需前置声明class File;无需#include “file/base/file.h”。

在头文件如何做到使用类Foo而无需访问类的定义呢?

  1. 将数据成员类型声明为Foo*或Foo&;

  2. 参数、返回类型为Foo的函数只声明(但不定义实现);

  3. 静态数据成员的类型可以被声明为Foo,因为静态数据成员的定义在类定义之外。

另一方面,如果你的类是Foo的子类,或者含有类型Foo的非静态数据成员,则必须为之包含头文件。

有时,使用指针成员(pointer members,如果是scope_ptr更好)替代对象成员(object members)的确更有意义。然而,这样的做法会降低代码可读性及执行效率。如果仅仅为了少包含头文件,还是不要这样替代的好。

当然,.cc文件无论如何都需要所使用类的定义部分,自然也就会包含若干头文件。

3.内联函数

只有当函数只有10行甚至更少才会将其定义为内联函数(inline function)。

定义(Definition):当函数被声明为内联函数之后,编译器可能会将其内联展开,无需按通常的函数调用机制调用内联函数。

优点:当函数体积比小的时候,内联该函数可以令目标代码更加有效。对于存取函数(accessor、mutator)以及其他一些比较短的关键执行函数。

缺点:滥用内联将导致程序变慢,内联有可能使目标代码或增或减,这取决于被内联函数的大小。内联较短小的存取函数通常会减少代码量,但内联一个很大的函数(如果编译器允许的话)将戏剧性增加代码量。在现代处理器上,由于更好的利用指令缓存(instruction cache),小巧的代码往往执行的更快。

结论:一个比较得当的处理规则是,不要内联超过10行的函数。对于析构函数应该慎重对待。析构函数往往比其表面看起来要长,因为有一些隐式的成员和基类析构函数(如果有的话)被调用。

另一有用的处理规则:内含那些包含循环或switch语句的函数是得不偿失的,除非在大多数情况下,这些循环或switch语句从不执行。

重要的是,虚函数和递归函数即使被声明为内联也不一定是内联函数。通常,递归函数不应该被声明为内联(递归调用堆栈的展开并不像循环那么简单,比如递归层数在编译器可能是未知的,大多数编译器都不支持内联递归函数)。析构函数内联的主要原因是其定义在类的定义中,为了方便抑或对其行为给出文档。

4. -inl.h文件

复杂的内联函数的定义,应放在后缀名为-inl.h的头文件中。

在头文件中给出内联函数的定义,可令编译器将其在调用处内联展开。然而,实现代码应该完全放在.cc文件中,我们不希望.h文件中出现太多的实现代码,除非这样做有可读性和效率上的明显优势。

如果内联函数的定义比较小、逻辑比较简单,其实现代码可以放在.h文件中。例如,存取函数的实现理所当然都放在类的定义中。出于实现和调用的方便,较复杂的内联函数也可以放到.h文件中,如果你觉得这样会使头文件显得笨重,还可以将其分离到单独的-inl.h中。这样即把实现和定义分离开来,当需要包含实现所在的-inl.h即可。

-inl.h文件还可以用于函数模板的定义,从而使得模板定义可读性增强。

要提醒一点的是,-inl.h和其他头文件一样,也需要#define保护。

5. 函数参数顺序(Function Parameter Ordering)

定义函数时,参数顺序为:输入参数在前,输出参数在后。

C/C++函数参数氛围输入参数和输出参数两种,有时输入参数也会是输出(值被修改时)。输入参数一般传值或常量引用(const references),输出参数或输入/输出参数为非常量指针(non-const pointer)。对参数排序时,将所有的输入参数置于输出参数之前。不要仅仅因为是新添的参数,就将其置于最后,而是应该置于输出参数之前。

这一点并不必须遵循的规则,输入/输出两用参数(通常是类/结构体变量)混在其中,会使得规则难以遵循。

6. 包含文件的名称和次序

将包含次序标准化可增强可读性、避免隐藏依赖(hidden dependencies,主要是指包含的文件中编译时),次序如下:C库、C++库、其他库.h、项目内的.h。

项目内头文件应该按照项目源代码目录树结果排列,并避免使用UNIX文件路径.(当前目录)和..(父目录)。例如,google-awesome-project/src/base/logging.h应该像这样被包含:

#include "base/logging.h"

dir/foo.cc主要作用是执行或测试dir2/foo2.h的功能, foo.cc中包含头文件的次序如下:

dir2/foo.h (优先位置,详细如下)

C系统文件

C++系统文件

其他库头文件

本项目内头文件

这种排序方式可有效减少隐藏依赖,我们希望每一个头文件独立编译。最简单的实现方式是将其作为第一个.h文件包含在对应的.cc中。

dir/foo.cc和dir2/foo2.h通常位于项目目录下(像base/basictypes_unittest.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"

猜你喜欢

转载自blog.csdn.net/weixin_28712713/article/details/85318408