文件输入/输出

掌握文件流模式、文本与二进制传输、更新流方向切换、随机访问和可验证的关闭提交

学习目标

  • 能写出检查 fopen、每次传输与 fclose 返回值的单一清理路径,并区分 EOF 与 I/O 错误
  • 能比较 r/w/a 与带加号更新模式,正确安排同一流上输入、输出、fflush 和定位操作
  • 能实现文本行和定长二进制字段读写,并解释结构体快照为何不能作为跨平台文件格式

机制总览

文件输入/输出:机制路径

  1. 1

    为什么程序需要「文件」?

    printf 把字打到屏幕, scanf 从键盘读——程序一结束,数据就没了。要把成绩表、配置、游戏存档留下来,就得写到 磁盘文件 ,下次再读回来。

  2. 2

    fopen 与 fclose:打开与关闭

    FILE fp = fopen("scores.txt", "w"); if (fp == NULL) perror("scores.txt"); return 1; / 使用 fp ...

  3. 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)——像水管一样,字节从程序流向文件,或从文件流进程序。指向流的指针类型是

FILE *fp — 流(stream)是桥梁程序不直接碰硬盘扇区;通过 FILE 结构体 + 缓冲区按字节/行读写你的程序fprintf(fp, ...)fgets(buf, n, fp)fread(ptr, sz, n, fp)FILE *fp指向流对象读写stdio 缓冲行缓冲 / 全缓冲当前读写位置EOF / 错误标志stdin stdout stderr也是 FILE*磁盘文件scores.dat持久化字节序列预定义流stdin → 键盘 · stdout → 屏幕 · stderr → 诊断输出fopen 成功后 fp 与它们一样,都是指向 FILE 的指针;fclose 断开连接并刷新缓冲。
FILE* 是程序与外部文件之间的「句柄」:stdio 在内存中缓冲数据,批量与磁盘交换,你只需对 fp 调用读写函数。

这一章掌握:打开/关闭、读写函数、文本与二进制、以及用 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;
}

的第二个参数是模式字符串。选错模式会截断文件、无法读取或只能追加。

常用记忆:

  • 只读已有文件 → "r"
  • 新建或清空后写 → "w"
  • 在末尾追加"a"
  • 读写已有文件 → "r+"(须存在)
  • 读写并先清空 → "w+"
  • 读写且所有输出追加到末尾 → "a+"(不存在则创建)

b 可以与上述模式组合成 rbw+b 等二进制模式。模式字符串字符的合法组合由标准规定,不要自行拼接未定义形式。

与 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 保留传输结果,关闭失败也会把最终结果改为失败。

标准流程:打开 → 读写 → 检查 → 关闭

文件 I/O 标准流程① fopenfp = fopen("data.txt…失败 → NULL② 读写fgets / fprintf / fr…经缓冲区③ 检查feof(fp) ferror(fp)区分正常结束与错误④ fclosefclose(fp);刷新缓冲·释放fclose 刷新未写出的缓冲;fp 不可再使用
成对 fopen/fclose;中间任意 return 路径也要关闭已打开的文件,否则句柄与缓冲泄漏。
分步1 / 4

第一步:fopen 打开

文件 I/O 标准流程① fopenfp = fopen("data.txt…失败 → NULL② 读写fgets / fprintf / fr…经缓冲区③ 检查feof(fp) ferror(fp)区分正常结束与错误④ fclosefclose(fp);刷新缓冲·释放打开流:检查 fp != NULL 再使用
成对 fopen/fclose;中间任意 return 路径也要关闭已打开的文件,否则句柄与缓冲泄漏。

检查 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");

的空指针返回值本身不区分正常结束和错误,要查询 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

顺序读写时,流维护 。定位调用都要检查结果:

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/fscanffgets/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 写入两行 1020;再以 "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 失败。

资料与写作方式声明

本章以C Primer Plus, Sixth Edition, Chapter 13: File Input/Output权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…