文件读写能否同时进行?
1. 核心陷阱:读写转换必须有“中转站”
书里提到的核心规则是:在同一个文件流上,输入(读)和输出(写)操作之间,必须插入一个“文件定位函数”。
常用的“中转站”函数包括:
fseek():移动文件指针(最常用)。fsetpos():也是移动指针。rewind():回到文件开头。fflush():如果是从“写”切换到“读”,可以用它刷新缓冲区(但从“读”切到“写”必须用定位函数)。
2. 为什么会有这个奇怪的规定?
这涉及到 C 语言标准库的**内部缓冲区(Buffer)**机制:
- 为了提高效率,当你读取文件时,系统并不会只读一个字节,而是先预读一大块放在内存里。
- 同样的,当你写入时,数据也会先攒在缓冲区里,等多了再一起写进硬盘。
- 问题所在 :同一个文件流通常只有一个内部状态。如果你刚读完,缓冲区里还残留着后面没处理的数据,这时候你突然下令“写”,库函数内部的指针、状态位和缓冲区就会陷入混乱。
所有的混乱都源于一个核心矛盾:你的程序以为自己在直接操作硬盘,但实际上你是在操作一个“中间商”——缓冲区(Buffer)。
3. 模拟演示:如果没有 fseek 会发生什么?
假设我们有一个文件 test.txt,里面的内容是:ABCDEFGHIJ(10 个字节)。
我们想实现一个简单的逻辑:读出前 3 个字符(ABC),然后立刻把后面的三个字符(DEF)改写成(XYZ)。
逻辑上的期待:
文件变成:ABCXYZGHIJ
现实中的混乱过程:
-
第一步:执行
fread(buf, 3, 1, fp)- 中间商(缓冲区)出场 :为了省事,它不会只读 3 个字节。它直接从硬盘里搬了一大块(比如全部 10 个字节)存到自己的“临时仓库”(缓冲区)里。
- 指针状态 :程序认为读指针在
C后面,但缓冲区的内部指针可能已经指到了J甚至更远。
-
第二步:直接执行
fwrite("XYZ", 3, 1, fp)(没调 fseek)- 由于没有中转指令 :标准库此时处于“读取模式”。你突然塞给它一个“写入”任务,它会发生 认知失调 。
- 混乱 A(覆盖错误) :它可能会把
XYZ写在它预读的缓冲区末尾,而不是你想要的D位置。 - 混乱 B(数据丢失) :它可能直接报错返回,或者更糟糕——它把缓冲区里还没处理完的数据(GHIJ)当成垃圾清理掉,导致文件后面全乱了。
4. 四个”中转站”函数的使用场景
4.1 fseek() — 读完后跳到指定位置再写(最通用)
场景: 把文件第 4~6 字节(DEF)改写为 XYZ。
#include <stdio.h>
int main(void) {
FILE *fp = fopen("test.txt", "r+"); // r+ 表示可读可写
char buf[4] = {0};
fread(buf, 3, 1, fp); // 读前3字节 → buf = "ABC",内部指针在位置3
printf("读到:%s\n", buf);
// 读→写切换:必须先 fseek
fseek(fp, 0, SEEK_CUR); // 偏移量为0的fseek,仅起"清空读状态"的作用
fwrite("XYZ", 3, 1, fp); // 把位置3~5的 DEF 改为 XYZ
fclose(fp);
return 0;
}
// 文件结果:ABCXYZGHIJ
fseek(fp, N, SEEK_SET/SEEK_CUR/SEEK_END)三个基准:
SEEK_SET:从文件头偏移 N 字节SEEK_CUR:从当前位置偏移 N 字节(N=0 即”原地清状态”)SEEK_END:从文件尾偏移 N 字节(N 通常为负数)
4.2 fsetpos() — 保存位置后回来继续写(搭档 fgetpos() 使用)
场景: 记录某处位置,读完一段内容后,精确回到该位置写入。
#include <stdio.h>
int main(void) {
FILE *fp = fopen("test.txt", "r+");
// 跳到位置3(D的位置),先记下来
fseek(fp, 3, SEEK_SET);
fpos_t mark;
fgetpos(fp, &mark); // 保存当前位置到 mark
// 继续读后面的内容
char buf[4] = {0};
fread(buf, 3, 1, fp); // 读 DEF
printf("读到:%s\n", buf);
// 读→写切换:用 fsetpos 回到 mark 处
fsetpos(fp, &mark); // 回到位置3,同时清除读状态
fwrite("XYZ", 3, 1, fp); // 覆盖 DEF → XYZ
fclose(fp);
return 0;
}
fsetposvsfseek:
fpos_t是不透明类型,跨平台更安全(在某些系统上文件偏移量超过long的范围)- 必须与
fgetpos配套,不能手动构造fpos_t的值
4.3 rewind() — 写完后回到开头重新读取验证
场景: 向文件写完数据后,回到开头重新读出来校验。
#include <stdio.h>
int main(void) {
FILE *fp = fopen("test.txt", "w+"); // w+ 创建并可读写
// 先写入数据
fputs("HELLO", fp);
// 写→读切换:用 rewind 回到开头
rewind(fp); // 等价于 fseek(fp, 0, SEEK_SET) + 清除错误标志
char buf[6] = {0};
fread(buf, 5, 1, fp);
printf("验证读到:%s\n", buf); // 输出 HELLO
fclose(fp);
return 0;
}
rewind的额外作用:它还会清除错误标志(clearerr),而fseek(fp, 0, SEEK_SET)不会。 适合”写完全部内容,从头再读”的场景,不适合跳到任意位置。
4.4 fflush() — 写→读切换时强制把缓冲区内容落盘
场景: 写完一段后,立刻读回同一文件的内容(注意:只能用于写切读,反过来必须用定位函数)。
#include <stdio.h>
int main(void) {
FILE *fp = fopen("test.txt", "w+");
fputs("WORLD", fp);
// 写→读切换:fflush 强制把缓冲区的 "WORLD" 真正写进文件
fflush(fp); // 刷新写缓冲区
// 现在才能回头读(还需要 fseek 定位)
fseek(fp, 0, SEEK_SET);
char buf[6] = {0};
fread(buf, 5, 1, fp);
printf("读到:%s\n", buf); // 输出 WORLD
fclose(fp);
return 0;
}重要限制:
fflush只对输出流(写缓冲区)有效- 读→写 切换时
fflush无效,必须用fseek/fsetpos/rewindfclose内部会自动调用fflush,关闭前无需手动调用
5. 四个函数对比总结
| 函数 | 方向 | 能否指定位置 | 特殊能力 | 典型场景 |
|---|---|---|---|---|
fseek | 读↔写 | ✅ 任意偏移 | — | 在文件任意位置切换读写 |
fsetpos | 读↔写 | ✅ 已保存的位置 | 跨平台大文件安全 | 保存位置后精确跳回 |
rewind | 写→读 | ❌ 只能回开头 | 额外清除错误标志 | 写完从头读取验证 |
fflush | 写→读 | ❌ 需配合fseek | 强制落盘 | 写完立刻读同一文件 |