C 语言文件操作完全指南:从打开到关闭
文件操作是 C 语言从"小黑框计算器"迈向"能存数据的实用程序"的关键一步。本文将系统梳理 C 标准库中的文件 I/O 函数,以及与之紧密配合的字符串格式化函数,帮你建立完整的输入输出知识体系。
一、文件操作的基本流程
无论读写什么文件,核心流程永远是四步:
plaintext
打开文件 → 读写数据 → 关闭文件 → 指针置空
对应四个核心环节:
| 步骤 | 函数 | 作用 |
|---|---|---|
| 打开 | fopen |
建立程序和文件的连接 |
| 关闭 | fclose |
断开连接,释放资源 |
| 检测 | ferror / feof |
判断读写过程中是否出错或到达末尾 |
| 善后 | fp = NULL |
防止野指针 |
二、打开与关闭:fopen 与 fclose
2.1 fopen 函数
c
FILE *fp = fopen("文件名", "打开模式");
打开模式是重点,也是最容易踩坑的地方:
| 模式 | 含义 | 文件不存在 | 文件存在 |
|---|---|---|---|
"r" |
只读(文本) | 返回 NULL |
从开头读 |
"w" |
只写(文本) | 创建新文件 | 清空原内容 |
"a" |
追加(文本) | 创建新文件 | 在末尾追加 |
"r+" |
读写(文本) | 返回 NULL |
从开头读写 |
"w+" |
读写(文本) | 创建新文件 | 清空原内容 |
"a+" |
读写追加(文本) | 创建新文件 | 读从头,写从尾 |
加 b |
二进制模式 | --- | 不转换换行符 |
关键记忆点:
- 带
w的模式会清空文件,慎用 - 带
+表示既能读又能写 - 带
b表示按原始字节处理,不做换行符转换(Windows 下尤其重要)
2.2 标准错误处理模板
c
FILE *fp = fopen("data.txt", "r");
if (fp == NULL) {
perror("打开文件失败"); // 自动打印系统错误原因
return 1;
}
// ... 读写操作 ...
fclose(fp);
fp = NULL; // 好习惯:防止悬空指针
2.3 fclose 的重要性
操作系统对单个程序能同时打开的文件数量有限制(通常是几百到几千个)。忘记 fclose 会导致:
- 资源泄漏:后续无法打开新文件
- 数据丢失:缓冲区里的内容可能没来得及写入磁盘
三、文本读写:字符与字符串
3.1 单字符读写:fgetc 与 fputc
c
// 逐字符读取文件
FILE *fp = fopen("test.txt", "r");
if (fp == NULL) {
perror("fopen");
return 1;
}
int ch; // 注意用 int,不是 char!因为要容纳 EOF
while ((ch = fgetc(fp)) != EOF) {
putchar(ch); // 输出到屏幕
}
fclose(fp);
为什么 fgetc 返回 int 而不是 char?
因为 EOF 通常定义为 -1。如果用 unsigned char 接收,当文件里真的有一个字节的值是 255(无符号)或 -1(有符号)时,会和 EOF 混淆。用 int 可以安全区分。
3.2 字符串读写:fgets 与 fputs
c
FILE *fp = fopen("note.txt", "r");
char buffer[100];
// 每次读取一行(最多99个字符,预留1个给'\0')
if (fgets(buffer, sizeof(buffer), fp) != NULL) {
printf("读到: %s", buffer);
}
fclose(fp);
fgets 的三要素:
- 读到换行符
\n会停止,并把\n也存入缓冲区 - 读到文件尾或达到
size-1上限也会停止 - 自动在末尾补
\0
安全陷阱 :sizeof(buffer) 千万别写错数组名。如果 arr1 和 arr2 大小不同,却写成 fgets(arr2, sizeof(arr1), fp),会导致缓冲区溢出或读不满。
3.3 格式化读写:fprintf 与 fscanf
这两个函数和 printf、scanf 几乎一模一样,只是多了一个文件指针参数:
c
FILE *fp = fopen("score.txt", "w");
if (fp != NULL) {
fprintf(fp, "姓名: %s, 分数: %d\n", "张三", 95);
fclose(fp);
}
读取时:
c
FILE *fp = fopen("score.txt", "r");
char name[20];
int score;
fscanf(fp, "姓名: %s, 分数: %d\n", name, &score);
printf("%s 考了 %d 分\n", name, score);
fclose(fp);
注意 :
fscanf的格式字符串必须和文件内容严格匹配,一个空格对不上就会导致解析失败。
四、二进制读写:fread 与 fwrite
当需要读写结构体、数组或图片等非文本数据时,必须用二进制模式。
c
// 写入一个整数数组
int arr[5] = {10, 20, 30, 40, 50};
FILE *fp = fopen("data.bin", "wb"); // 必须带 b
fwrite(arr, sizeof(int), 5, fp);
// 参数:数据地址, 每个元素大小, 元素个数, 文件指针
fclose(fp);
读取回来:
c
int arr[5];
FILE *fp = fopen("data.bin", "rb");
size_t count = fread(arr, sizeof(int), 5, fp);
// 返回值是成功读到的元素个数,不是字节数!
if (count == 5) {
printf("全部读取成功\n");
}
fclose(fp);
文本模式 vs 二进制模式的本质区别:
- 文本模式下,Windows 会把文件里的
\r\n自动转成\n给你看,写入时反过来 - 二进制模式下,你读到什么就是什么,一字节不差
五、文件定位:fseek、ftell 与 rewind
文件内部有一个"光标"(文件位置指示器),记录着下一次读写从哪里开始。
5.1 fseek:移动光标
c
fseek(fp, offset, origin);
| origin | 含义 | 常用场景 |
|---|---|---|
SEEK_SET (0) |
从文件开头 | 跳到固定位置 |
SEEK_CUR (1) |
从当前位置 | 回退或跳过若干字节 |
SEEK_END (2) |
从文件末尾 | 测文件大小 |
示例:修改文件中间内容
c
FILE *fp = fopen("log.txt", "r+");
fputs("This is an apple.", fp); // 先写入完整内容
// 把光标拨到第9个字节('n'的位置)
fseek(fp, 9, SEEK_SET);
// 覆盖写入 " sam"
fputs(" sam", fp);
fclose(fp);
执行过程解析:
plaintext
原始内容: T h i s i s a n a p p l e .
位置: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
↑
fseek到这里
覆盖: 空格 s a m
结果: T h i s i s a 空格 s a m p l e .
最终文件内容变成 This is a sample.,实现了精准替换。
示例:先读后写时的回退技巧
c
fputs("abcdefghi", fp);
fflush(fp);
fseek(fp, 1, SEEK_SET); // 跳到位置1(字符'b')
int ch = fgetc(fp); // 读取'b',指针自动移到'c'(位置2)
fseek(fp, -1, SEEK_CUR); // 从当前位置回退1,回到'b'(位置1)
fputs("hello", fp); // 从'b'开始覆盖
这里第二次 fseek 使用负数偏移 -1,配合 SEEK_CUR,实现了"读了一个字符后退回原位再写入"的效果。如果不回退,fputs 会从 c 的位置开始覆盖,结果就错了。
5.2 ftell:获取当前位置
c
fseek(fp, 0, SEEK_END);
long size = ftell(fp); // 返回值就是文件总字节数
5.3 rewind:一键回开头
c
rewind(fp); // 等价于 fseek(fp, 0, SEEK_SET);
六、错误检测:ferror、feof 与 perror
文件操作出错时,不会直接崩溃,而是设置错误标志。你需要主动检查。
6.1 perror:人类可读的错误信息
c
FILE *fp = fopen("不存在的文件.txt", "r");
if (fp == NULL) {
perror("打开失败"); // 输出:打开失败: No such file or directory
}
6.2 feof 与 ferror:区分"正常结束"和"出错了"
c
int ch;
while ((ch = fgetc(fp)) != EOF) {
putchar(ch);
}
if (feof(fp)) {
printf("\n正常到达文件末尾\n");
} else if (ferror(fp)) {
printf("\n读取过程中发生错误\n");
}
重要区分:
EOF可能是"读到结尾",也可能是"读取出错"feof只在尝试读取失败后才能准确判断是不是因为到结尾了ferror检测是否发生了读写错误
6.3 clearerr:重置错误标志
一旦出错或到达 EOF,文件指针的错误标志会被设置,后续操作可能受影响。可以用 clearerr(fp) 手动清除这些标志。
七、字符串格式化:sprintf 与 sscanf
虽然这两个函数操作的是字符串 而非文件,但它们与 fprintf/fscanf 属于同一族格式化 I/O 函数,在文件操作中经常配合使用。
如果把 I/O 家族按目标分类:
| 目标 | 输出(写) | 输入(读) |
|---|---|---|
| 屏幕/控制台 | printf |
scanf |
| 文件 | fprintf |
fscanf |
| 字符串(字符数组) | sprintf |
sscanf |
7.1 sprintf:把数据"打印"到字符串里
c
int fileno = 7;
char path[64];
sprintf(path, "data/file_%d.txt", fileno);
// path 内容: "data/file_7.txt"
安全陷阱 :sprintf 不会检查目标数组够不够大,极易缓冲区溢出。现代 C 开发请改用 snprintf:
c
snprintf(path, sizeof(path), "data/file_%d.txt", fileno);
// 最多写 sizeof(path)-1 个字符,超长自动截断
7.2 sscanf:从字符串里"扫描"提取数据
c
char line[] = "width=1920 height=1080";
int w, h;
sscanf(line, "width=%d height=%d", &w, &h);
// 提取后: w=1920, h=1080
与文件操作配合的典型场景:
c
FILE *fp = fopen("config.txt", "r");
char buffer[128];
while (fgets(buffer, sizeof(buffer), fp) != NULL) {
char key[32];
int value;
// 从读到的每一行里解析 "key = value" 格式
if (sscanf(buffer, "%s = %d", key, &value) == 2) {
printf("配置项: %s = %d\n", key, value);
}
}
fclose(fp);
这里 fgets 负责从文件安全地读一行,sscanf<