本节摘要:标准库的文件 IO 围绕 FILE 指针展开:fopen 建立流、读写函数搬运数据、fclose 关闭并冲刷。数据从程序到磁盘要穿过三级缓冲(库的用户缓冲、内核页缓存、磁盘缓存),理解这个结构就理解了"为什么逐字节读很慢""为什么 fclose 不能忘""为什么 fprintf 的输出有时会丢"。本节讲全生命周期与性能要点。
#include <stdio.h> int main(void) { FILE *fp = fopen("scores.txt", "r"); /* 打开:建立流,失败返回 NULL */ if (fp == NULL) { perror("打开失败"); /* 打印系统错误原因 */ return 1; } int score; int count = 0, total = 0; while (fscanf(fp, "%d", &score) == 1) { /* 循环读到不匹配为止 */ total += score; count++; } printf("平均 %d\n", count ? total / count : 0); fclose(fp); /* 关闭:冲刷缓冲并释放资源 */ return 0; }
fopen 的契约:成功返回 FILE 指针(指向库内部的不透明结构,第 7 章的 FILE 正是"不透明指针"教材),失败返回 NULL——必须检查。模式字符串决定用途:
| 模式 | 含义 | 文件不存在时 | 文件存在时 |
|---|---|---|---|
| r | 只读 | 失败 | 从头读 |
| w | 只写 | 创建 | 清空! |
| a | 追加 | 创建 | 在末尾写 |
| r+ | 读写 | 失败 | 从头 |
| w+ | 读写 | 创建 | 清空! |
| a+ | 读追加 | 创建 | 写总在末尾 |
| rb / wb | 二进制模式 | 同上 | 同上 |
"w 清空已有文件"是新手第一大事故:想"打开看看"却用 w,文件瞬间归零。二进制模式后缀 b 在 Linux 上无差别,在 Windows 上决定换行是否转换——跨平台文本统一 LF、显式用二进制模式,是可移植写法。
fclose 的契约:冲刷未写出的缓冲、归还文件描述符与 FILE 结构。忘了 fclose 的后果三连:缓冲数据丢失、文件描述符泄漏(进程打开数有上限,长期运行的程序会"打不开新文件")、加锁文件不释放。
一次 fwrite 的数据之旅:
你的缓冲区 ↓ fwrite 拷贝进 C 库的用户缓冲区(通常 4KB,fopen 时由 malloc 建立) ↓ 缓冲满或冲刷时,write 系统调用 内核页缓存(操作系统统一管理) ↓ 内核择机或 fsync 强制 磁盘(含磁盘自身缓存)
系统调用有固定开销(上下文切换),库缓冲的意义就是攒够一大批再过一次马路。验证:
/* 版本一:每次 1 字节,写 10 万次 */ for (int i = 0; i < 100000; i++) fputc('x', fp); /* 版本二:攒 10 万字节,一次 fwrite */ char buf[100000]; memset(buf, 'x', sizeof buf); fwrite(buf, 1, sizeof buf, fp);
两版落盘内容相同,耗时差几十到几百倍——版本一虽然也有库缓冲,但函数调用本身有开销;若绕过库直接用系统调用逐字节写,差距还会再放大两个数量级。批量是 IO 性能第一定律。

文本族适合行与词;二进制族按字节计数,返回成功字节数:
size_t n = fread(buf, sizeof buf[0], count, fp); if (n < count) { if (feof(fp)) { /* 正常读到文件尾 */ } else { /* 读取错误:配合 perror 输出原因 */ } }
feof 的正确用法是"事后判断":它不预测未来,只在读取失败后告诉你"是因为到尾了吗"。拿 feof 当循环条件会多读一轮——fscanf 那个 == 1 的循环写法(8.2 开头)才是正解:以读取函数的返回值驱动循环。
fseek 与 ftell 随机定位(二进制文件里按记录大小跳转是随机访问文件的常规操作);重读刚写的内容前先 fseek 或 fflush——读写切换必须隔一次定位或冲刷,这是标准的规定。
⚠️ 常见坑:写文件程序崩溃后文件是空的或截断的——缓冲区没冲刷就死了(第 2 章 printf 缓冲的文件版)。写完重要段落主动 fflush,别把可靠性托付给"正常退出时冲刷"。
💡 关键直觉:把文件 IO 想成"搬砖过三道门"——库缓冲、页缓存、磁盘,每道门都会攒一批再走。程序员的任务是控制"批量大小"与"什么时候强制开门"(fflush 与 fsync),其余交给体系结构。
下一节回到编译之前:预处理与条件编译,大型 C 工程的组织骨架。