C 语言文件 IO 与模块化
更新时间:2026-08-25。本文是
languages/c/主题高手层第 3 篇。前面写的都是单文件小程序,真正的 C 工程是多文件协作的。本文讲两件事:文件 IO(读数据、写结果),以及多文件模块化(头文件/源文件/构建)。
本文要回答的问题
- 怎么用 C 读写文件?
fopen的模式字符串各代表什么? - 多个
.c文件怎么组织?头文件里该放什么、不该放什么? extern和static在多文件协作中各起什么作用?
一、文件 IO 基础
1.1 打开与关闭
c
FILE *fp = fopen("data.txt", "r"); // 打开只读
if (fp == NULL) {
perror("fopen"); // 打印错误原因
return 1;
}
/* ... 使用 ... */
fclose(fp); // 记得关闭fopen 模式字符串:
| 模式 | 含义 |
|---|---|
"r" | 只读(文件必须存在) |
"w" | 只写(覆盖,不存在则创建) |
"a" | 追加(在末尾写) |
"r+" | 读写 |
"rb" / "wb" | 二进制模式(加 b) |
1.2 读与写
c
// 逐行读
char line[256];
while (fgets(line, sizeof(line), fp) != NULL) {
printf("%s", line);
}
// 格式化写
fprintf(fp, "value = %d\n", 42);
// 二进制读写(按块)
int buf[100];
fread(buf, sizeof(int), 100, fp);
fwrite(buf, sizeof(int), 100, fp);1.3 标准流
stdin(标准输入)、stdout(标准输出)、stderr(标准错误)是三个预定义的文件指针:
c
fprintf(stdout, "正常输出\n");
fprintf(stderr, "错误信息\n"); // 走错误流,可单独重定向理解标准流,才能理解 shell 的重定向(>, 2>, |),这也是 strace 观察系统调用 时看到的 read/write 的来源。
二、多文件模块化
2.1 头文件与源文件的职责分工
一个模块通常拆成两个文件:
| 文件 | 放什么 | 不放什么 |
|---|---|---|
.h(头文件) | 函数声明、类型定义、宏、常量 | 函数实现、全局变量定义 |
.c(源文件) | 函数实现 | — |
c
// mathutil.h —— 声明
#ifndef MATHUTIL_H
#define MATHUTIL_H
int add(int a, int b); // 声明,供其他文件调用
#endifc
// mathutil.c —— 实现
#include "mathutil.h"
int add(int a, int b) {
return a + b;
}c
// main.c —— 使用
#include "mathutil.h"
int main(void) {
int r = add(3, 4);
return 0;
}2.2 头文件保护(include guard)
#ifndef / #define / #endif 防止头文件被重复包含(重复包含会导致重复定义错误)。现代写法也可以用 #pragma once。
2.3 extern 与 static 的链接属性
c
// globals.c
int global_counter = 0; // 全局变量,外部可见
static int internal = 0; // static:仅本文件可见c
// main.c
extern int global_counter; // 声明使用其他文件的全局变量| 关键字 | 作用 |
|---|---|
extern | 声明"这个符号在别的文件定义" |
static | 限制符号只在当前文件可见(内部链接) |
最佳实践:全局变量尽量用 static 限制作用域,避免符号冲突(对应编译链接篇的 multiple definition 错误)。
三、Makefile 构建
多文件项目手动编译很麻烦,用 Makefile 自动化:
makefile
# 简单 Makefile
CC = gcc
CFLAGS = -Wall -Wextra -O2
app: main.o mathutil.o
$(CC) $(CFLAGS) -o app main.o mathutil.o
main.o: main.c mathutil.h
$(CC) $(CFLAGS) -c main.c
mathutil.o: mathutil.c mathutil.h
$(CC) $(CFLAGS) -c mathutil.c
clean:
rm -f app *.omake自动判断哪些文件变了、只重新编译需要的部分(增量构建)。- 本站 demos/cpu-demo 就用 Makefile,
make/make run/make release/make clean。
本站约定:demo 默认
-O0 -g(保留符号便于 perf),make release用-O2(演示优化效果)。
四、模块化最佳实践
- 一个模块一对
.h/.c:接口与实现分离。 - 头文件最小化:能不在头文件暴露的就不暴露。
- 用
static隐藏内部函数:只把对外接口暴露在头文件。 - 全局变量谨慎:尽量用
static,或通过函数封装访问。 - Makefile 管理依赖:避免手动记编译顺序。
五、与本站主线衔接
- 系统调用视角:
fopen/fread底层是open/read系统调用,见 strace 观察。 - 缓冲 IO vs 直接 IO:stdio 有用户态缓冲,与内核页缓存的关系见 L2 IO 子系统。
- 构建与优化:Makefile 里的
-O选项影响生成代码,见 L4 编译器选项。
一句话总结
文件 IO 与模块化 = 用 fopen/fread 读写数据 + 用"头文件放声明、源文件放实现"组织多文件 + 用 extern/static 控制符号可见性 + 用 Makefile 自动化构建;掌握这些,你就能从"写小程序"进阶到"写工程"。