头文件与标准库

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 hello

printf 的具体实现在哪呢?在系统预先编译好的库文件里(比如 Linux 上的 libc.so)。链接器会自动把你的 .o 文件和这些库文件连接在一起,让 printf 的调用指向它真正的实现,最终生成可执行文件。

当然,平时你不用手动分三步执行,gcc hello.c -o hello 就是一次性把三步全做了。但理解这三步很重要,因为它解释了一个关键分工:头文件负责声明(编译时用),库文件负责实现(链接时用)#include 只是把声明拿过来,让编译器知道有这个函数可以调用。

刷题常用标准库头文件一览

下面列出刷题中最常用的标准库头文件,让你知道「有哪些东西可以用」。具体的用法和示例会在后续的 常用数据结构算法题常用技巧 章节中详细介绍。

头文件用途典型函数/宏
stdio.h标准输入输出printfscanfputs
stdlib.h通用工具(内存分配、排序、类型转换等)malloc/freeqsortatoi
string.h字符串操作(求长度、比较、复制等)strlenstrcmpstrcpymemset
math.h数学函数(开方、幂运算、取整等)sqrtpowfabsceil/floor
stdbool.h布尔类型(C99)truefalse
limits.h类型极值INT_MAXINT_MIN
ctype.h字符判断isdigitisalphatolower

一般来说,刷题时你会用到的头文件就这几个。把它们一次性 include 进来就行,不用担心性能问题,编译器只会链接你实际用到的部分。

自定义头文件与多文件组织

当代码越写越多,全塞在一个文件里会很难维护。C 语言的做法是把代码分散到多个文件中,用头文件来连接它们。

规则很简单:

  • 头文件(.h:放函数声明和类型定义,告诉别人"我提供了哪些功能"
  • 源文件(.c:放函数的具体实现

头文件保护

头文件有一个重要的东西叫头文件保护(header guard),用来防止同一个头文件被重复包含。

为什么会重复包含?假设 a.hb.h 都 include 了 utils.h,而你的 main.c 同时 include 了 a.hb.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.hMATH_UTILS_H),这样不容易和别人撞名。

多文件示例

下面是一个完整的多文件项目示例。main.c 通过 #include "math_utils.h" 引入自定义头文件,使用里面声明的函数:

如果你想在本地编译这个多文件项目,需要把所有 .c 文件都传给编译器:

gcc main.c math_utils.c -o main
./main

注意只需要列出 .c 源文件,不需要列 .h 头文件——因为 #include 已经把头文件的内容复制到源文件里了。

了解了头文件和标准库之后,下一篇我们来讲 函数,然后深入学习 指针内存管理