一个 .c 文件在 2,000 行以内都还好用。把程序拆分到多个文件后,每个部分可以单独编译,可以在其他程序中复用,也可以单独阅读。但 C 没有 import 机制,它只有负责粘贴文本的预处理器,以及最后把各个部分连接起来的链接器。
头文件(.h)就是这些部分之间共享的约定:它告诉每个源文件其他地方有哪些东西,但不包含具体实现。
声明与定义
整个设计建立在一个区别之上。
声明的意思是:这个东西存在于某处,它的样子是这样的。声明不会生成代码,可以出现任意多次:
int add(int a, int b); /* function declaration (prototype) */
extern int error_count; /* variable declaration */
struct Point { int x, y; }; /* type definition - safe to repeat per file */
定义则真正创建这个东西。在整个程序中它必须恰好出现一次:
int add(int a, int b) { return a + b; } /* function definition */
int error_count = 0; /* variable definition */
头文件放声明,源文件放定义。如果弄反了,链接器会报 "multiple definition of ...",看到这条错误信息,基本可以确定有定义跑进了头文件。
一个由两个文件组成的程序
下面是最小的实用拆分。一个声明了两个函数的头文件:
/* math_utils.h */
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
int add(int a, int b);
int max_of(int a, int b);
#endif
实现这两个函数的源文件,注意它包含了自己的头文件:
/* math_utils.c */
#include "math_utils.h"
int add(int a, int b) {
return a + b;
}
int max_of(int a, int b) {
return (a > b) ? a : b;
}
以及使用它们的程序:
/* main.c */
#include <stdio.h>
#include "math_utils.h"
int main(void) {
printf("add(3, 4) = %d\n", add(3, 4));
printf("max_of(3, 4) = %d\n", max_of(3, 4));
return 0;
}
把两个源文件一起编译:
gcc main.c math_utils.c -o app
./app
这里有两个细节值得注意。第一,math_utils.c 包含自己的头文件并不多余。这样编译器会检查每个定义是否与声明一致;如果你改了头文件里的原型却忘了改 .c 文件,会立刻得到一个错误,而不是到链接时才发现不匹配。
第二,math_utils.h 不在 gcc 的命令行中。头文件从来不单独编译,而是由 #include 粘贴进 .c 文件。把 .h 交给编译器,只会生成一个多余的预编译头文件,得不到可以链接的代码。
把同一个程序压缩到一个文件里,你可以直接在这里运行:
main 之前的声明,在真实版本中由头文件提供。这也正说明了函数原型和头文件其实是同一个思路,只是规模不同。
include 守卫
#include 粘贴的是文本,文本粘贴两次就会重复。对函数原型来说这没有问题,对 struct 来说却是致命的:
/* shapes.h WITHOUT a guard */
struct Point { int x, y; };
如果 main.c 同时包含了 shapes.h 和 canvas.h,而 canvas.h 也包含了 shapes.h,编译器就会在同一个翻译单元中看到 struct Point 被定义了两次,并报出 "redefinition of 'struct Point'" 后停止。在真实项目中,这种包含链会深到无法靠人工追踪。
解决办法是 include 守卫:用一个宏记录“这个头文件已经粘贴过了”。
/* shapes.h */
#ifndef SHAPES_H
#define SHAPES_H
struct Point { int x, y; };
struct Point origin_point(void);
#endif /* SHAPES_H */
第一次包含时,SHAPES_H 还没有定义,所以头文件的内容会保留下来,同时顺便定义 SHAPES_H。之后在同一个文件中再次包含时,会发现它已经定义,于是直接跳到 #endif。宏名在整个项目中必须唯一;常见的约定是根据路径取名为 FILENAME_H。
所有主流编译器都支持一种只有一行的替代写法:
/* shapes.h */
#pragma once
struct Point { int x, y; };
#pragma once 不会出现宏名冲突,也不会因为 #endif 处的笔误而失效。它唯一的缺点是不属于 C 标准,所以需要在少见编译器上构建的项目,应优先使用 #ifndef 写法。不管用哪种,每个头文件都要有守卫,没有例外,包括那些你认为不会被别处包含的头文件。
头文件里该放什么
应该放进 .h 的:
- 函数原型
struct、union和enum的定义typedef声明- 需要共享的宏
- 共享全局变量的
extern声明 - 头文件本身为了自给自足而需要的
#include
不应该放进 .h 的:
- 函数体(除非是有意写成
static inline) - 变量定义:头文件里的
int counter;会在每个包含它的文件中各定义一个变量,或者导致链接错误,具体取决于编译器 - 头文件本身用不到的
#include:它会把这个依赖强加给所有下游文件
“自给自足”值得定成一条规则:一个头文件在第一个被包含、前面没有任何其他内容时,也应该能通过编译。如果 shapes.h 用到了 size_t,它应该自己包含 <stddef.h>,而不是指望包含它的文件已经包含过了。
一个完整、规范的头文件:
/* inventory.h */
#ifndef INVENTORY_H
#define INVENTORY_H
#include <stddef.h> /* for size_t, used below */
#define MAX_NAME 64
typedef struct {
char name[MAX_NAME];
int quantity;
double price;
} Item;
/* shared across the program, defined once in inventory.c */
extern int item_count;
void inventory_add(const Item *item);
double inventory_total(void);
size_t inventory_size(void);
#endif /* INVENTORY_H */
用 extern 共享全局变量
全局变量必须在恰好一个 .c 文件中定义,在其他所有地方声明。extern 就是用来写声明的:
/* inventory.h - declaration, no storage */
extern int item_count;
/* inventory.c - the one definition */
#include "inventory.h"
int item_count = 0;
/* main.c - uses it, via the header */
#include <stdio.h>
#include "inventory.h"
int main(void) {
printf("%d items\n", item_count);
return 0;
}
如果去掉头文件里的 extern,每个包含它的文件都会定义自己的 item_count,好一点的情况是链接时报 "multiple definition",最坏的情况是得到两个互不相干的计数器。
相反的需求同样常见:某个变量或辅助函数只应在一个 .c 文件内部使用。在文件作用域使用 static 就能做到,它让这个名字具有内部链接,链接器看不到它,其他文件也就看不到:
/* inventory.c */
static Item storage[256]; /* private to this file */
static int find_slot(const char *name); /* private helper */
两个文件可以各自有一个 static int counter;,互不冲突。这就是 C 版本的私有成员,也是你应该首先考虑的默认做法:只有其他文件真正需要的东西才放进头文件。
编译更大的程序
列出所有文件的方法可行,但很慢,因为每次都会重新编译所有文件:
gcc main.c inventory.c report.c -o app
可扩展的做法是把每个源文件编译成目标文件,再把它们链接起来:
gcc -c main.c # produces main.o
gcc -c inventory.c # produces inventory.o
gcc -c report.c # produces report.o
gcc main.o inventory.o report.o -o app
这样修改 report.c 后,只需执行 gcc -c report.c 再重新链接。make 自动完成的正是这些记录工作:
app: main.o inventory.o report.o
gcc main.o inventory.o report.o -o app
%.o: %.c
gcc -Wall -Wextra -c $< -o $@
如果头文件放在子目录中,-Iinclude 会把这个目录加入尖括号形式的搜索路径。
错误及其含义
只要知道错误来自哪个阶段,这两类失败就很容易区分。
"undefined reference to 'add'":这是链接器错误。找到了声明,却没找到定义。可能是你忘了在命令行中列出那个 .c 文件,可能是函数被声明成了 static,也可能是两处的名字有一处拼错了。
"multiple definition of 'item_count'":同样是链接器错误,情况正好相反:有一个定义跑进了头文件,或者出现在两个源文件中。把它移到一个 .c 文件里,并在头文件中留下 extern 声明。
"redefinition of 'struct Item'":这是编译器错误,说明一个头文件被粘贴进同一个文件两次。加上 include 守卫即可。
"implicit declaration of function 'add'":这是编译器警告(在 C99 及之后的模式下是错误),说明编译器从没见过这个函数的原型。你忘了写 #include,或者头文件里没有声明它。
当程序跨越多个文件后,你通常接下来想做的,就是按平台或构建类型改变要编译的内容,这就是条件编译。
常见问题
.h 文件里放什么,.c 文件里放什么?
头文件放声明:函数原型、struct 和 typedef 定义、enum、宏,以及共享全局变量的 extern 声明。.c 文件放定义:函数体和真正的变量。一个简单的判断方法:头文件说明有什么,源文件说明它做什么。
include 守卫是什么?为什么需要它?
#include 的作用是粘贴文本,所以把一个头文件包含两次,它的内容就会被粘贴两次,其中的每个 struct 和 typedef 都会被重复定义,导致编译失败。include 守卫用 #ifndef MYHEADER_H / #define MYHEADER_H / #endif 把头文件包起来,第二次包含时宏已经定义过,就会跳过头文件的内容。
多个文件组成的 C 程序怎么编译?
在命令行中列出每个 .c 文件:gcc main.c math_utils.c -o app。不要把 .h 文件写进去:头文件由 #include 粘贴进来,不会单独编译。项目更大时,先编译成目标文件(gcc -c main.c),再把它们链接起来,Makefile 自动完成的正是这件事。
应该用 #pragma once 还是 #ifndef include 守卫?
两种都可以。#pragma once 只有一行,也不会出现宏名冲突,所有主流编译器都支持它,但它不属于 C 标准。#ifndef/#define/#endif 的写法是标准的,在任何地方都能用。在一个项目中选定一种并保持一致;如果要求最大的可移植性,就选 #ifndef 写法。