在 C 基础语法 中,我们已经用过 #include <stdio.h> 来引入标准输入输出库。这篇文章来详细讲讲 C 的头文件机制:#include 到底做了什么,标准库还有哪些常用的头文件,以及如何用头文件组织多文件项目。
#include 的两种写法
C 语言的 #include 是一条预处理指令。所谓预处理,就是在正式编译之前,编译器先扫一遍代码,把所有 # 开头的指令处理掉。#include 的作用很简单粗暴:把目标文件的内容原封不动地复制粘贴到当前位置。
它有两种写法:
// 尖括号:从系统目录查找,用于标准库
#include <stdio.h>
#include <math.h>
// 双引号:先从当前目录找,找不到再去系统目录
// 用于自己写的头文件
#include "my_utils.h"标准库头文件用尖括号,自己写的用双引号。
来看一个完整的例子:
头文件一般用 .h 后缀,h 就是 header(头文件)的缩写。
从源码到可执行程序
你平时写完代码,一句 gcc hello.c -o hello 就能编译运行,但编译器背后其实分了三步干活。理解这三步,就能理解 #include 到底在做什么。
第一步:预处理(Preprocessing)
你可以单独执行预处理,看看效果:
gcc -E hello.c -o hello.i-E 表示只做预处理就停下来。打开 hello.i 你会发现,原来那行 #include <stdio.h> 消失了,取而代之的是几百行 stdio.h 的内容被原封不动地粘贴进来。这就是预处理的本质:文本替换,把所有 # 开头的指令处理掉。
第二步:编译(Compiling)
gcc -c hello.c -o hello.o-c 表示只编译不链接,生成目标文件 .o。这一步做的事情是检查语法和类型。
stdio.h 里面装的是函数的声明,比如 printf 的声明大概长这样:
int printf(const char *format, ...);注意,这里只有声明,没有函数体(也就是没有 {...} 包裹的具体代码)——它只是告诉编译器:「有一个叫 printf 的函数,接收这些参数,返回 int」。编译器看到你调用了 printf,就去检查参数类型对不对、返回值用得对不对,检查通过就放行了。
第三步:链接(Linking)
gcc hello.o -o helloprintf 的具体实现在哪呢?在系统预先编译好的库文件里(比如 Linux 上的 libc.so)。链接器会自动把你的 .o 文件和这些库文件连接在一起,让 printf 的调用指向它真正的实现,最终生成可执行文件。
当然,平时你不用手动分三步执行,gcc hello.c -o hello 就是一次性把三步全做了。但理解这三步很重要,因为它解释了一个关键分工:头文件负责声明(编译时用),库文件负责实现(链接时用)。#include 只是把声明拿过来,让编译器知道有这个函数可以调用。
刷题常用标准库头文件一览
下面列出刷题中最常用的标准库头文件,让你知道「有哪些东西可以用」。具体的用法和示例会在后续的 常用数据结构 和 算法题常用技巧 章节中详细介绍。
| 头文件 | 用途 | 典型函数/宏 |
|---|---|---|
stdio.h | 标准输入输出 | printf、scanf、puts |
stdlib.h | 通用工具(内存分配、排序、类型转换等) | malloc/free、qsort、atoi |
string.h | 字符串操作(求长度、比较、复制等) | strlen、strcmp、strcpy、memset |
math.h | 数学函数(开方、幂运算、取整等) | sqrt、pow、fabs、ceil/floor |
stdbool.h | 布尔类型(C99) | true、false |
limits.h | 类型极值 | INT_MAX、INT_MIN |
ctype.h | 字符判断 | isdigit、isalpha、tolower |
一般来说,刷题时你会用到的头文件就这几个。把它们一次性 include 进来就行,不用担心性能问题,编译器只会链接你实际用到的部分。
自定义头文件与多文件组织
当代码越写越多,全塞在一个文件里会很难维护。C 语言的做法是把代码分散到多个文件中,用头文件来连接它们。
规则很简单:
- 头文件(
.h):放函数声明和类型定义,告诉别人"我提供了哪些功能" - 源文件(
.c):放函数的具体实现
头文件保护
头文件有一个重要的东西叫头文件保护(header guard),用来防止同一个头文件被重复包含。
为什么会重复包含?假设 a.h 和 b.h 都 include 了 utils.h,而你的 main.c 同时 include 了 a.h 和 b.h,那 utils.h 就会被包含两次,导致重复声明的编译错误。
解决方法是在头文件里加上三条预处理指令:
// math_utils.h
#ifndef MATH_UTILS_H // if not defined:如果还没定义过这个宏
#define MATH_UTILS_H // define:那就定义它
// 函数声明
int factorial(int n);
int is_prime(int n);
#endif // 结束 #ifndef 的范围逻辑很简单:第一次 include 时 MATH_UTILS_H 还没定义,#ifndef 条件成立,内容被包含进来,同时 #define 定义了这个宏。第二次 include 时宏已经存在,#ifndef 条件不成立,整段内容被跳过。
宏的名字可以随便取,但习惯上用头文件名的大写加下划线(如 math_utils.h → MATH_UTILS_H),这样不容易和别人撞名。
多文件示例
下面是一个完整的多文件项目示例。main.c 通过 #include "math_utils.h" 引入自定义头文件,使用里面声明的函数:
如果你想在本地编译这个多文件项目,需要把所有 .c 文件都传给编译器:
gcc main.c math_utils.c -o main
./main注意只需要列出 .c 源文件,不需要列 .h 头文件——因为 #include 已经把头文件的内容复制到源文件里了。