C 语言头文件简介

目录汇总:C 语言零基础入门教程

当把程序分割为几个源文件时,问题也随之产生了:某文件中的函数如何调用定义在其他文件中的函数呢?函数如何访问其他文件中的外部变量呢?两个文件如何共享同一个宏定义或类型定义呢?答案取决于 #include 指令,此指令使得在任意数量的源文件中共享信息成为可能,这些信息可以是函数原型、宏定义、类型定义等。

#include 指令告诉预处理器打开指定的文件,并且把此文件的内容插入当前文件中。因此,如果想让几个源文件可以访问相同的信息,可以把此信息放入一个文件中,然后利用 #include 指令把该文件的内容带进每个源文件中。按照此种方式包含的文件称为头文件(有时称为包含文件)。本文后面将更详细地讨论头文件。根据惯例,头文件的扩展名为 .h。

注意: C 标准使用术语“源文件”来指代程序员编写的全部文件,包括 .c 文件和 .h 文件。本 C 语言零基础入门教程 中的“源文件”仅指 .c 文件。

一、#include 指令

#include 指令主要有两种书写格式。第一种格式用于属于 C 语言自身库的头文件:

1
[#include指令(格式 1] #include <文件名>

第二种格式用于所有其他头文件,也包含任何自己编写的文件:

1
[#include指令(格式 2] #include "文件名"

这两种格式间的细微差异在于编译器定位头文件的方式。 下面是大多数编译器遵循的规则。

  • #include <文件名>: 搜寻系统头文件所在的目录(或多个目录)。(例如,在 UNIX 系统中,通常把系统头文件保存在目录 /usr/include 中。)

  • #include "文件名": 先搜寻当前目录,然后搜寻系统头文件所在的目录(或多个目录)。

通常可以改变搜寻头文件的位置,这种改变经常利用如 -I 路径这样的命令行选项来实现。

注意

不要在包含自己编写的头文件时使用尖括号:

1
#include <myheader.h>  /*** WRONG ***/

这是因为预处理器可能在保存系统头文件的地方寻找 myheader.h(但显然是找不到的)。

#include 指令中的文件名可以含有帮助定位文件的信息,比如目录的路径或驱动器号:

1
2
#include "c:\cprogs\utils.h"    /* Windows path */
#include "/cprogs/utils.h"      /* UNIX path */

虽然 #include 指令中的双引号使得文件名看起来像字面串,但是预处理器不会把它们作为字面串来处理。(这是幸运的,因为在上面的 Windows 例子中,字面串中出现的 \c\u 会被作为转义序列处理。)

可移植性技巧 通常最好的做法是在 #include 指令中不包含路径或驱动器的信息。当把程序转移到其他机器上,或者更糟的情况是转移到其他操作系统上时,这类信息会使编译变得很困难。

例如,下面的这些 #include 指令指定了驱动器或路径信息,而这些信息不可能一直是有效的:

1
2
3
#include "d:utils.h"
#include "\cprogs\include\utils.h"
#include "d:\cprogs\include\utils.h"

下列这些指令相对好一些。它们没有指定驱动器,而且使用的是相对路径而不是绝对路径:

1
2
#include "utils.h"
#include "..\include\utils.h"

#include 指令还有一种不太常用的格式:

1
[#include指令(格式 3] #include 记号

其中记号是任意预处理记号序列。预处理器会扫描这些记号,并替换遇到的宏。宏替换完成以后,#include 指令的格式一定与前面两种之一相匹配。第三种 #include 指令的优点是可以用宏来定义文件名,而不需要把文件名“硬编码”到指令里面去,如下所示:

1
2
3
4
5
6
7
8
9
#if defined(IA32)
  #define CPU_FILE "ia32.h"
#elif defined(IA64)
  #define CPU_FILE "ia64.h"
#elif defined(AMD64)
  #define CPU_FILE "amd64.h"
#endif

#include CPU_FILE

二、共享宏定义和类型定义

大多数大型程序包含需要由几个源文件(或者,最极端的情况是用于全部源文件)共享的宏定义和类型定义。这些定义应该放在头文件中。

例如,假设正在编写的程序使用名为 BOOLTRUEFALSE 的宏。(C99 中不需要这么做,因为 <stdbool.h> 头中定义了类似的宏。)我们把这些定义放在一个名为 boolean.h 的头文件中,这样做比在每个需要的源文件中重复定义这些宏更有意义:

1
2
3
#define BOOL int
#define TRUE 1
#define FALSE 0

任何需要这些宏的源文件只需简单地包含下面这一行:

1
#include "boolean.h"

在下面的图中,两个文件都包含了 boolean.h。

共享宏定义和类型定义

类型定义在头文件中也是很普遍的。例如,不用定义 BOOL 宏,而是可以用 typedef 创建一个 Bool 类型。如果这样做,boolean.h 文件将有下列显示:

1
2
3
#define TRUE 1
#define FALSE 0
typedef int Bool;

把宏定义和类型定义放在头文件中有许多显而易见的好处。首先,不把定义复制到需要它们的源文件中可以节约时间。其次,程序变得更加容易修改。改变宏定义或类型定义只需要编辑单独的头文件,而不需要修改使用宏或类型的诸多源文件。最后,不需要担心由于源文件包含相同宏或类型的不同定义而导致的矛盾。

三、共享函数原型

假设源文件包含函数 f 的调用,而函数 f 是定义在另一个文件 foo.c 中的。调用没有声明的函数 f 是非常危险的。如果没有函数原型可依赖,编译器会假定函数 f 的返回类型是 int 类型的,并假定形式参数的数量和函数 f 的调用中的实际参数的数量是匹配的。通过默认实参提升(C 语言函数中的形式参数和实际参数),实际参数自身自动转换为“标准格式”。编译器的假定很可能是错误的,但是,因为一次只能编译一个文件,所以是没有办法进行检查的。如果这些假定是错误的,那么程序很可能无法工作,而且没有线索可以用来查找原因。(基于这个原因,C99 禁止在编译器看到函数声明或定义之前对函数进行调用。)

注意

当调用在其他文件中定义的函数 f 时,要始终确保编译器在调用之前已看到函数 f 的原型。

我们的第一个想法是在调用函数 f 的文件中声明它。这样可以解决问题,但是可能产生维护方面的“噩梦”。假设有 50 个源文件要调用函数 f,如何能确保函数 f 的原型在所有文件中都一样呢?如何能保证这些原型和 foo.c 文件中函数 f 的定义相匹配呢?如果以后函数 f 发生了改变,如何能找到所有用到此函数的文件呢?

解决办法是显而易见的:把函数 f 的原型放进一个头文件中,然后在所有调用函数 f 的地方包含这个头文件。既然在文件 foo.c 中定义了函数 f,我们把头文件命名为 foo.h。除了在调用函数 f 的源文件中包含 foo.h,还需要在 foo.c 中包含它,从而使编译器可以验证 foo.h 中函数 f 的原型和 foo.c 中 f 的函数定义相匹配。

注意

在含有函数 f 定义的源文件中始终包含声明函数 f 的头文件。如果不这样做,则可能导致难以发现的错误,因为在程序别处对函数 f 的调用可能会和函数 f 的定义不匹配。

如果文件 foo.c 包含其他函数,大多数函数应该在包含函数 f 的声明的那个头文件中声明。毕竟,文件 foo.c 中的其他函数大概会与函数 f 有关。任何含有函数 f 调用的文件都可能会需要文件 foo.c 中的其他一些函数。然而,仅用于文件 foo.c 的函数不需要在头文件中声明,如果声明则容易造成误解。

为了说明头文件中函数原型的使用,一起回到 C 语言源文件简介 中的 RPN 计算器示例。文件 stack.c 包含函数 make_emptyis_emptyis_fullpushpop 的定义。这些函数的原型应该放在头文件 stack.h 中:

1
2
3
4
5
void make_empty(void);
int is_empty(void);
int is_full(void);
void push(int i);
int pop(void);

(为了避免使示例复杂化,函数 is_empty 和函数 is_full 将不再返回 Boolean 类型值而返回 int 类型值。)文件 calc.c 中将包含 stack.h 以便编译器检查在后面的文件中出现的栈函数的任何调用。文件 stack.c 中也将包含 stack.h 以便编译器验证 stack.h 中的函数原型是否与 stack.c 中的定义相匹配。下面这张图展示了 stack.h、stack.c 和 calc.c。

共享函数原型

四、共享变量声明

外部变量(C 语言外部变量简介)在文件中共享的方式与函数的共享很类似。为了共享函数,要把函数的定义放在一个源文件中,然后在需要调用此函数的其他文件中放置声明。共享外部变量的方法和此方式非常类似。

目前不需要区别变量的声明和它的定义。为了声明变量 i,可以这样写:

1
int i;             /* declares i and defines it as well */

这样不仅声明 iint 类型的变量,而且也对 i 进行了定义,从而使编译器为 i 留出了空间。为了声明变量 i 而不是定义它,需要在变量声明的开始处放置 extern 关键字:

1
extern int i;      /* declares i without defining it */

extern 告诉编译器,变量 i 是在程序中的其他位置定义的(很可能是在不同的源文件中),因此不需要为 i 分配空间。

顺便说一句,extern 可以用于所有类型的变量。在数组的声明中使用 extern 时,可以省略数组的长度:

1
extern int a[];

因为此刻编译器不用为数组 a 分配空间,所以也就不需要知道数组 a 的长度了。

为了在几个源文件中共享变量 i,首先把变量 i 的定义放置在一个文件中:

1
int i;

如果需要对变量 i 初始化,可以把初始化器放在这里。在编译这个文件时,编译器会为变量 i 分配内存空间,而其他文件将包含变量 i 的声明:

1
extern int i;

通过在每个文件中声明变量 i,使得在这些文件中可以访问或修改变量 i。然而,由于关键字 extern 的存在,编译器不会在每次编译这些文件时都为变量 i 分配额外的内存空间。

当在文件中共享变量时,会面临和共享函数时相似的挑战:确保变量的所有声明和变量的定义一致。

注意

当同一个变量的声明出现在不同文件中时,编译器无法检查声明是否和变量定义相匹配。例如,一个文件可以包含定义

1
int i;

同时另一个文件包含声明

1
extern long i;

这类错误可能导致程序的行为异常。

为了避免声明和变量的定义不一致,通常把共享变量的声明放置在头文件中。需要访问特定变量的源文件可以包含相应的头文件。此外,含有变量定义的源文件需要包含含有相应变量声明的头文件,这样编译器就可以检查声明与定义是否匹配。

虽然在文件中共享变量是 C 语言界中的长期惯例,但是它有重大的缺陷。

五、嵌套包含

头文件自身也可以包含 #include 指令。虽然这种做法可能看上去有点奇怪,但实际上是十分有用的。思考含有下列原型的 stack.h 文件:

1
2
int is_empty(void);
int is_full(void);

由于这些函数只能返回 0 或 1,将它们的返回类型声明为 Bool 类型而不是 int 类型是一个很好的主意:

1
2
Bool is_empty(void);
Bool is_full(void);

当然,我们需要在 stack.h 中包含文件 boolean.h 以便在编译 stack.h 时可以使用 Bool 的定义。(在 C99 中应包含 <stdbool.h> 而不是 boolean.h,并把这两个函数的返回类型声明为 bool 而不是 Bool。)

传统上,C 程序员避免使用嵌套包含。(C 语言的早期版本根本不允许嵌套包含。)但是,这种对嵌套包含的偏见正在逐渐减弱,一个原因就是嵌套包含在 C++ 语言中很普遍。

六、保护头文件

如果源文件包含同一个头文件两次,那么可能产生编译错误。当头文件包含其他头文件时,这种问题十分普遍。例如,假设 file1.h 包含 file3.h,file2.h 包含 file3.h,而 prog.c 同时包含 file1.h 和 file2.h(如下图所示),那么在编译 prog.c 时,file3.h 就会被编译两次。

保护头文件

两次包含同一个头文件不总是会导致编译错误。如果文件只包含宏定义、函数原型和/或变量声明,那么不会有任何困难。然而,如果文件包含类型定义,则会导致编译错误。

安全起见,保护全部头文件避免多次包含可能是个好主意,那样的话可以在稍后添加类型定义,不用冒因忘记保护文件而可能产生的风险。此外,在程序开发期间,避免同一个头文件的不必要重复编译可以节省一些时间。

为了防止头文件多次包含,用 #ifndef#endif 指令来封闭文件的内容。例如,可以用如下方式保护文件 boolean.h:

1
2
3
4
5
6
7
8
#ifndef BOOLEAN_H
#define BOOLEAN_H

#define TRUE 1
#define FALSE 0
typedef int Bool;

#endif

在首次包含这个文件时,没有定义宏 BOOLEAN_H,所以预处理器允许保留 #ifndef#endif 之间的多行内容。但是如果再次包含此文件,那么预处理器将把 #ifndef#endif 之间的多行内容删除。

宏的名字(BOOLEAN_H)并不重要,但是,给它取类似于头文件名的名字是避免和其他的宏冲突的好方法。由于不能把宏命名为 BOOLEAN.H(标识符不能含有句点),像 BOOLEAN_H 这样的名字是个很好的选择。

七、头文件中的 #error 指令

#error 指令(C 语言中一些不常用的指令)经常放置在头文件中,用来检查不应该包含头文件的条件。例如,如果头文件中用到了一个在最初的 C89 标准之前不存在的特性,为了避免把头文件用于旧的非标准编译器,可以在头文件中包含 #ifdef 指令来检查 __STDC__ 宏(C 语言宏定义简介)是否存在:

1
2
3
#ifndef __STDC__
#error This header requires a Standard C compiler
#endif

请参阅

(完)

comments powered by Disqus