文件读写能否同时进行?

← 返回 MOC | ← 主页|←C++基础语法缓冲区问题


1. 核心陷阱:读写转换必须有“中转站”

书里提到的核心规则是:在同一个文件流上,输入(读)和输出(写)操作之间,必须插入一个“文件定位函数”。

常用的“中转站”函数包括:

  • fseek() :移动文件指针(最常用)。
  • fsetpos() :也是移动指针。
  • rewind() :回到文件开头。
  • fflush() :如果是从“写”切换到“读”,可以用它刷新缓冲区(但从“读”切到“写”必须用定位函数)。

2. 为什么会有这个奇怪的规定?

这涉及到 C 语言标准库的**内部缓冲区(Buffer)**机制:

  • 为了提高效率,当你读取文件时,系统并不会只读一个字节,而是先预读一大块放在内存里。
  • 同样的,当你写入时,数据也会先攒在缓冲区里,等多了再一起写进硬盘。
  • 问题所在 :同一个文件流通常只有一个内部状态。如果你刚读完,缓冲区里还残留着后面没处理的数据,这时候你突然下令“写”,库函数内部的指针、状态位和缓冲区就会陷入混乱。

所有的混乱都源于一个核心矛盾:你的程序以为自己在直接操作硬盘,但实际上你是在操作一个“中间商”——缓冲区(Buffer)。


3. 模拟演示:如果没有 fseek 会发生什么?

假设我们有一个文件 test.txt,里面的内容是:ABCDEFGHIJ(10 个字节)。 我们想实现一个简单的逻辑:读出前 3 个字符(ABC),然后立刻把后面的三个字符(DEF)改写成(XYZ)。

逻辑上的期待:

文件变成:ABCXYZGHIJ

现实中的混乱过程:

  1. 第一步:执行 fread(buf, 3, 1, fp)

    • 中间商(缓冲区)出场 :为了省事,它不会只读 3 个字节。它直接从硬盘里搬了一大块(比如全部 10 个字节)存到自己的“临时仓库”(缓冲区)里。
    • 指针状态 :程序认为读指针在 C 后面,但缓冲区的内部指针可能已经指到了 J 甚至更远。
  2. 第二步:直接执行 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;
}

fsetpos vs fseek

  • 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/rewind
  • fclose 内部会自动调用 fflush,关闭前无需手动调用

5. 四个函数对比总结

函数方向能否指定位置特殊能力典型场景
fseek读↔写✅ 任意偏移在文件任意位置切换读写
fsetpos读↔写✅ 已保存的位置跨平台大文件安全保存位置后精确跳回
rewind写→读❌ 只能回开头额外清除错误标志写完从头读取验证
fflush写→读❌ 需配合fseek强制落盘写完立刻读同一文件