文件输入/输出
掌握文件流模式、文本与二进制传输、更新流方向切换、随机访问和可验证的关闭提交
学习目标
- 能写出检查 fopen、每次传输与 fclose 返回值的单一清理路径,并区分 EOF 与 I/O 错误
- 能比较 r/w/a 与带加号更新模式,正确安排同一流上输入、输出、fflush 和定位操作
- 能实现文本行和定长二进制字段读写,并解释结构体快照为何不能作为跨平台文件格式
机制总览
文件输入/输出:机制路径
- 1
为什么程序需要「文件」?
printf 把字打到屏幕, scanf 从键盘读——程序一结束,数据就没了。要把成绩表、配置、游戏存档留下来,就得写到 磁盘文件 ,下次再读回来。
- 2
fopen 与 fclose:打开与关闭
FILE fp = fopen("scores.txt", "w"); if (fp == NULL) perror("scores.txt"); return 1; / 使用 fp ...
- 3
标准流程:打开 → 读写 → 检查 → 关闭
本节把「标准流程:打开 → 读写 → 检查 → 关闭」放回《文件输入/输出》的输入、状态变化与输出路径中理解。
章级决策实验
文件输入/输出:机制与证据
切换《文件输入/输出》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。
选择推理阶段
当前阶段 · 为什么程序需要「文件」?
printf 把字打到屏幕, scanf 从键盘读——程序一结束,数据就没了。要把成绩表、配置、游戏存档留下来,就得写到 磁盘文件 ,下次再读回来。
可核验证据
用 -Wall -Wextra -pedantic 构建本节示例,再以正常值、边界值和失败输入核对「为什么程序需要「文件」?」的实际行为。
学完《文件输入/输出》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。
失效—证据矩阵
文件输入/输出:失效与核验
为什么程序需要「文件」?
典型失效
若只记语法而忽略「为什么程序需要「文件」?」的类型范围、求值顺序或资源边界,输入变化后可能出现未定义行为或错误结果。
核验证据
用 -Wall -Wextra -pedantic 构建本节示例,再以正常值、边界值和失败输入核对「为什么程序需要「文件」?」的实际行为。
fopen 与 fclose:打开与关闭
典型失效
若只记语法而忽略「fopen 与 fclose:打开与关闭」的类型范围、求值顺序或资源边界,输入变化后可能出现未定义行为或错误结果。
核验证据
用 -Wall -Wextra -pedantic 构建本节示例,再以正常值、边界值和失败输入核对「fopen 与 fclose:打开与关闭」的实际行为。
标准流程:打开 → 读写 → 检查 → 关闭
典型失效
若只记语法而忽略「标准流程:打开 → 读写 → 检查 → 关闭」的类型范围、求值顺序或资源边界,输入变化后可能出现未定义行为或错误结果。
核验证据
用 -Wall -Wextra -pedantic 构建本节示例,再以正常值、边界值和失败输入核对「标准流程:打开 → 读写 → 检查 → 关闭」的实际行为。
为什么程序需要「文件」?
printf 把字打到屏幕,scanf 从键盘读——程序一结束,数据就没了。要把成绩表、配置、游戏存档留下来,就得写到磁盘文件,下次再读回来。
C 不让你直接操作硬盘扇区,而是提供一套标准 I/O(stdio):你把 fopen 打开的文件当成流(stream)——像水管一样,字节从程序流向文件,或从文件流进程序。指向流的指针类型是 ↡stdio 中表示一个已打开 I/O 流的不透明结构体指针;fopen 返回,fclose 释放。。
这一章掌握:打开/关闭、读写函数、文本与二进制、以及用 fseek/ftell 在文件里「跳转」。
fopen 与 fclose:打开与关闭
#include <stdio.h>
FILE *fp = fopen("scores.txt", "w");
if (fp == NULL) {
perror("scores.txt");
return 1;
}
/* 使用 fp ... */
if (fclose(fp) == EOF) {
perror("scores.txt");
return 1;
}↡按 mode 关联程序与路径上的文件,成功返回 FILE*,失败 NULL。需包含 stdio.h 头文件。 的第二个参数是模式字符串。选错模式会截断文件、无法读取或只能追加。
常用记忆:
- 只读已有文件 →
"r" - 新建或清空后写 →
"w" - 在末尾追加 →
"a" - 读写已有文件 →
"r+"(须存在) - 读写并先清空 →
"w+" - 读写且所有输出追加到末尾 →
"a+"(不存在则创建)
b 可以与上述模式组合成 rb、w+b 等二进制模式。模式字符串字符的合法组合由标准规定,不要自行拼接未定义形式。
↡刷新待写数据并解除流与文件的关联;成功返回0,失败返回EOF。调用后无论成功失败都不能再使用该FILE指针。 与 fopen 成对。缓冲输出的错误可能直到刷新或关闭才被发现,所以“前面的 fprintf 成功”不能替代检查 fclose。调用 fclose 后流已解除关联,即使它报告失败也不能再次 fclose 同一指针。
更新流:加号模式需要方向切换协议
↡以带加号模式打开、同时允许输入和输出的流;在读写方向切换时必须按标准要求执行刷新或文件定位操作。 不能任意交替调用读写函数:
- 输出后改为输入:中间执行
fflush(fp)或文件定位函数 - 输入后改为输出:中间执行文件定位函数,除非刚才的输入操作遇到 EOF
int update_counter(void) {
FILE *fp = fopen("counter.bin", "r+b");
unsigned value;
int ok = 0;
if (fp == NULL)
return 1;
if (fread(&value, sizeof value, 1, fp) != 1)
goto cleanup;
value++;
if (fseek(fp, 0L, SEEK_SET) != 0)
goto cleanup; /* 输入切到输出前重新定位 */
if (fwrite(&value, sizeof value, 1, fp) != 1)
goto cleanup;
ok = 1;
cleanup:
if (fclose(fp) == EOF)
ok = 0;
return ok ? 0 : 1;
}示例只演示方向切换,不是可移植文件格式:unsigned 的宽度和字节序仍由实现决定。ok 保留传输结果,关闭失败也会把最终结果改为失败。
标准流程:打开 → 读写 → 检查 → 关闭
第一步:fopen 打开
检查 fp != NULL。路径错误、权限不足、模式与文件状态不匹配都会失败。
格式化与行 I/O:fprintf / fscanf / fgets / fputs
与终端上的 printf/scanf 类似,只是第一个参数换成 FILE *:
if (fprintf(fp, "%s %d\n", name, score) < 0)
/* 记录写错误 */;
if (fscanf(fp, "%63s %d", name, &score) == 2)
/* 仅在两个转换都成功时使用 name 和 score */;%63s 只适用于容量至少 64 的 name,并会在空白处停止;复杂记录更适合先读整行再解析。fscanf 返回成功赋值的项目数,不能忽略。
fgets 在遇到换行时会把该换行存入数组;若缓冲区先满,则本次结果没有换行且同一记录仍有剩余:
char line[256];
while (fgets(line, sizeof line, fp) != NULL) {
fputs(line, stdout);
}
if (ferror(fp))
perror("grades.txt");↡从流读取至多size-1个字符;若遇换行会存入换行,成功时追加空字符并返回目标指针;尚未读到字符就遇EOF或错误时返回空指针。 的空指针返回值本身不区分正常结束和错误,要查询 feof/ferror。fputs 写字符串且不自动加换行;返回 EOF 表示失败,也应检查。gets 已被 C11 移除。
块 I/O:fread 与 fwrite
把内存中的字节块原样写入文件,适合数组、结构体快照:
int data[] = {10, 20, 30};
if (fwrite(data, sizeof data[0], 3, fp) != 3)
/* 写入不完整 */;
int buf[3];
size_t n = fread(buf, sizeof buf[0], 3, fp);
/* n 为实际读到的元素个数 */fread/fwrite 参数:ptr 内存地址、size 每个元素字节数、count 元素个数、stream 流。返回值是成功读/写的元素个数;小于 count 时检查 feof/ferror。
文本 vs 二进制
在模式后加 b 即二进制:fopen("game.sav", "wb")。有些系统不区分文本和二进制,但可移植代码仍应按数据语义选择模式。二进制流不会执行文本流允许的实现定义转换;文本流可能进行换行映射,并可能对字符集合或文件尾表示作处理。
保存 struct Player { int hp; char name[32]; } 时常用:
fwrite(&player, sizeof player, 1, fp);
fread(&player, sizeof player, 1, fp);直接写结构体对象表示只适合同一程序和明确受控 ABI 下的临时数据。结构体可能含填充字节,成员宽度、字节序和浮点表示会变化,指针成员写出的地址也没有持久化意义;未初始化填充还可能泄露进程内存片段。稳定格式应逐字段定义宽度、字节序、版本和长度,再显式编码。文本格式也必须定义编码、转义和数值规则,不能仅凭“JSON/CSV”名称自动获得兼容性。
随机访问:fseek、ftell、rewind
顺序读写时,流维护 ↡流中下一次读写所使用的位置状态;二进制流常可按字符偏移,文本流的位置可能只能通过ftell返回的令牌恢复。。定位调用都要检查结果:
if (fseek(fp, 5L * (long)sizeof(int), SEEK_SET) != 0)
perror("seek");
long position = ftell(fp);
if (position == -1L)
perror("tell");
rewind(fp); /* 回到开头并清除 EOF 与错误状态,但不返回失败信息 */fseek 的 whence 可为 SEEK_SET、SEEK_CUR、SEEK_END。对文本流,可移植定位通常只使用零偏移,或把同一流先前 ftell 返回的值原样交给 SEEK_SET;ftell 的值不必是字节数。二进制流的偏移以字符为单位,但实现也可以不支持相对 SEEK_END 的任意定位。成功的 fseek 会清除 EOF 状态并撤销 ungetc 的效果;需要可诊断地回到开头时,用 fseek 而不是无返回值的 rewind。
代码详解:持久化成绩表
场景一:文本写入多行
#include <stdio.h>
int main(void) {
FILE *fp = fopen("grades.txt", "w");
int ok = 1;
if (fp == NULL) return 1;
if (fprintf(fp, "Alice 95\n") < 0 ||
fprintf(fp, "Bob 88\n") < 0)
ok = 0;
if (fclose(fp) == EOF)
ok = 0;
return ok ? 0 : 1;
}场景二:逐行读回并打印
#include <stdio.h>
int main(void) {
char line[128];
FILE *fp = fopen("grades.txt", "r");
int ok = 1;
if (fp == NULL) return 1;
while (fgets(line, sizeof line, fp) != NULL) {
if (fputs(line, stdout) == EOF) {
ok = 0;
break;
}
}
if (ferror(fp))
ok = 0;
if (fclose(fp) == EOF)
ok = 0;
return ok ? 0 : 1;
}场景三:二进制保存 int 数组
#include <stdio.h>
int main(void) {
int scores[] = {90, 85, 78};
int buf[3];
FILE *fp = fopen("scores.bin", "wb");
if (fp == NULL) return 1;
if (fwrite(scores, sizeof scores[0], 3, fp) != 3) {
fclose(fp);
return 1;
}
if (fclose(fp) == EOF)
return 1;
fp = fopen("scores.bin", "rb");
if (fp == NULL) return 1;
if (fread(buf, sizeof buf[0], 3, fp) != 3) {
fclose(fp);
return 1;
}
if (fclose(fp) == EOF)
return 1;
return 0;
}容易踩的坑
小结
FILE*代表已打开的流;stdio 经缓冲区与磁盘交换数据fopen/fclose成对;失败返回 NULL 必须检查- 模式:
r只读、w清空写、a追加;加b为二进制;+允许读写 - 更新流切换读写方向时需要 fflush 或定位操作;具体规则取决于切换方向和是否遇到 EOF
- 文本:
fprintf/fscanf、fgets/fputs;块:fread/fwrite - 读写由本次函数返回值驱动,再用 feof/ferror 区分短读原因;输出还要检查 fclose
- 文本 ftell 值不保证是字节偏移;稳定二进制格式应逐字段定义宽度、字节序和版本
练习
问题 1(问答型) "a" 与 "w" 打开已存在文件时,行为有何关键区别?
问题 2(改代码型) 下面循环有什么问题?如何改?
while (!feof(fp)) {
fscanf(fp, "%d", &n);
printf("%d\n", n);
}问题 3(综合实现型) 写程序:用 "w" 创建 data.txt 写入两行 10 和 20;再以 "r" 读回并打印两数之和。
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 流(stream)
与 FILE 关联的字节流抽象;读写按顺序经过 stdio 缓冲区。stdin/stdout/stderr 也是流。详见 FilePointerDiagram。
- FILE *
指向表示打开文件的状态结构;fopen 返回,传给各类 stdio 函数。
- fopen
打开文件并关联流;mode 决定读/写/追加/二进制。失败 NULL。
- fclose
刷新待写数据并解除流关联;成功返回 0,失败返回 EOF。关闭错误可能是输出提交的首次错误信号,调用后不得再使用该流。
- 文本模式
默认 r/w/a;可能转换换行;适合人类可读文件。见 BinaryVsTextIODiagram。
- 二进制模式
rb/wb/ab 等;字节原样传输;适合 fread/fwrite 与 struct。
- fgets
读取至多 size-1 个字符,遇到换行会保存换行,成功时追加空字符。无换行结果可能是文件末尾,也可能是记录截断。
- fread / fwrite
按元素大小与个数读写内存块;返回成功元素个数。
- fseek
设置流当前位置;whence 为 SEEK_SET/CUR/END。
- ftell
返回可用于恢复流位置的 long 值,失败为 -1L。二进制流上常对应字符偏移,文本流上不保证是字节数。
- feof
流是否在读到 EOF 后处于结束状态;不宜单独作为循环唯一条件。
- 更新流(update stream)
以带加号模式打开、同时允许输入输出的流。切换方向时须按规则使用 fflush 或文件定位函数,不能任意交替读写。
- 文件位置指示器
流维护的下一次读写位置状态。fseek 修改、ftell 查询;文本流位置可能只能作为不透明令牌恢复。
- ferror
查询流的错误指示器。短读或 fgets 返回空指针后,与 feof 配合区分正常输入结束和 I/O 失败。