8.2 文件 IO 与缓冲区


8.2 文件 IO 与缓冲区

本节摘要:标准库的文件 IO 围绕 FILE 指针展开:fopen 建立流、读写函数搬运数据、fclose 关闭并冲刷。数据从程序到磁盘要穿过三级缓冲(库的用户缓冲、内核页缓存、磁盘缓存),理解这个结构就理解了"为什么逐字节读很慢""为什么 fclose 不能忘""为什么 fprintf 的输出有时会丢"。本节讲全生命周期与性能要点。

FILE 指针的生命周期

#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 性能第一定律

图 1 三级缓冲结构与冲刷时机

图 1 三级缓冲结构与冲刷时机

读写函数家族与错误检查

文本族适合行与词;二进制族按字节计数,返回成功字节数:

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——读写切换必须隔一次定位或冲刷,这是标准的规定。

工程清单

  • fopen 必查 NULL,fclose 不忘调;打开失败的三大原因:不存在、无权限、句柄耗尽——perror 会告诉你哪个;
  • 批量读写优于逐字节;定长记录用 fread/fwrite;
  • 关键数据写完 fflush 甚至 fsync:掉电时页缓存里的数据会丢,数据库类程序靠 fsync 保持久性;
  • 跨平台文本用二进制模式显式控制换行
  • 临时文件用完即删,文件权限最小化——安全审计的必查项。

⚠️ 常见坑:写文件程序崩溃后文件是空的或截断的——缓冲区没冲刷就死了(第 2 章 printf 缓冲的文件版)。写完重要段落主动 fflush,别把可靠性托付给"正常退出时冲刷"。

💡 关键直觉:把文件 IO 想成"搬砖过三道门"——库缓冲、页缓存、磁盘,每道门都会攒一批再走。程序员的任务是控制"批量大小"与"什么时候强制开门"(fflush 与 fsync),其余交给体系结构。

本节要点回顾

  • FILE 指针生命周期:fopen 查 NULL、用完 fclose;w 模式会清空文件,打开前想清楚。
  • 三级缓冲:库用户缓冲、内核页缓存、磁盘缓存;系统调用贵,攒批是性能第一定律。
  • feof 只能事后判断:以读取返回值驱动循环,feof 区分"到尾"与"出错"。
  • fwrite 与 fread 按字节计数返回,二进制与定长记录首选。
  • 持久性靠 fsync:页缓存掉电即失,关键数据显式落盘。

下一节回到编译之前:预处理与条件编译,大型 C 工程的组织骨架。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U