文件操作从打开到缓冲区

深入理解 C 语言:文件操作从打开到缓冲区实战

文件是程序与外部世界交换数据的桥梁,也是数据持久化的核心手段。C 语言通过标准 I/O 库提供了一套完整、高效的文件操作接口。本文将从"为什么使用文件"出发,一路深入到文件打开关闭、顺序读写、随机读写、结束判定以及缓冲区机制。所有代码均保证可以直接编译运行,带你彻底吃透文件操作在内存与磁盘之间的处理逻辑。


目录


一、为什么使用文件?

程序运行时数据存放在内存中,一旦程序结束,内存回收,数据随之丢失。若想长期保存数据,就必须将数据写入文件(磁盘)。文件可以让数据独立于程序存在,实现持久化存储。

典型场景:通讯录保存联系人、游戏存档、日志记录、配置读取等。

代码示例:将用户输入的一句话保存到文件,再读取出来。

c 复制代码
#include <stdio.h>
#include <string.h>

int main(void) {
    // 写入文件
    FILE *fp = fopen("message.txt", "w");
    if (fp == NULL) {
        perror("fopen");
        return 1;
    }
    const char *msg = "Hello, file!";
    fputs(msg, fp);
    fclose(fp);

    // 读取文件
    char buf[100] = {0};
    fp = fopen("message.txt", "r");
    if (fp == NULL) {
        perror("fopen");
        return 1;
    }
    fgets(buf, sizeof(buf), fp);
    printf("读取内容: %s\n", buf);
    fclose(fp);
    return 0;
}

"要点总结:"

  1. 文件是数据持久化的基本单位

    内存数据程序退出即丢失,文件可以把数据保存到磁盘,供下次运行读取。

  2. 打开文件必须检查返回值

    fopen 返回 FILE *,失败返回 NULL。用 perror 可以打印失败原因,便于调试。

  3. 打开模式决定文件行为

    • "w":只写,文件不存在则创建,存在则清空。
    • "r":只读,文件必须存在,否则打开失败。
    • 还有 "a" 追加、"rb"/"wb" 二进制等模式。
  4. 写入后必须关闭文件

    fclose 会刷新缓冲区,把尚未写入磁盘的数据真正落盘,并释放文件相关资源。

    若只写入不关闭,程序异常结束时可能丢失数据。

  5. 读取时用返回值判断结束

    fgets 返回 NULL 表示读取结束或出错。

    结束后用 feof 判断是否正常到达文件尾,用 ferror 判断是否发生读写错误。


避坑提示:

  • 用 "w" 打开已存在的文件会清空原内容,操作重要文件前务必确认。
  • fgets 会保留换行符 '\n',打印时通常不需要额外加换行。
  • 缓冲区 buf 要足够大,否则一行过长会被截断,剩余内容留在流中。
  • fclose 后不要再次使用同一个 fp,否则行为未定义。
  • 文本文件在不同平台换行符可能不同,跨平台时建议用二进制模式或统一换行处理。

二、什么是文件?

从操作系统角度看,文件是磁盘上的数据集合;从 C 程序角度看,文件是通过 FILE 结构管理的字节流。C 语言中,文件分为程序文件 (源文件、目标文件、可执行文件)和数据文件(程序运行时读写的数据)。本章讨论的是数据文件。

文件名 :通常包含路径、主干和后缀,如 C:\data\test.txt。不写路径则默认当前目录。

C 标准库在程序启动时默认打开三个标准流:

  • stdin:标准输入,通常对应键盘。
  • stdout:标准输出,通常对应显示器。
  • stderr:标准错误,通常对应显示器。

它们的类型都是 FILE *。我们通过文件指针来操作文件。

代码示例:打印标准流指针,并演示文件指针的基本使用。

c 复制代码
#include <stdio.h>

int main(void) {
    printf("stdin  = %p\n", (void*)stdin);
    printf("stdout = %p\n", (void*)stdout);
    printf("stderr = %p\n", (void*)stderr);

    FILE *fp = fopen("test.txt", "w");
    if (fp == NULL) {
        perror("fopen");
        return 1;
    }
    printf("文件打开成功,FILE* = %p\n", (void*)fp);
    fclose(fp);
    return 0;
}

" 要点总结:"

  1. FILE 是描述文件的结构体类型

    它定义在 <stdio.h> 中,内部保存了文件描述符、缓冲区位置、读写状态、错误标志等信息。不同编译器实现细节不同,但用法统一。我们不需要关心其内部成员,只需通过 FILE * 来操作文件。

  2. 文件操作围绕 FILE * 进行

    fopen 返回 FILE *,失败返回 NULL。所有文件读写函数(fgetc、fputs、fread、fwrite、fprintf、fscanf 等)都需要传入 FILE *。关闭文件用 fclose。

  3. 标准流让常用输入输出无需显式打开

    C 程序启动时自动打开三个标准流:

    • stdin:标准输入,通常连接键盘,scanf 从它读取。
    • stdout:标准输出,通常连接显示器,printf 向它写入。
    • stderr:标准错误,通常也连接显示器,但不会被重定向到普通输出文件,用于打印错误信息。
      它们的类型都是 FILE *,可以直接用于 fprintf、fputs 等函数。
  4. 标准流与普通文件流没有本质区别

    它们都是 FILE *,都可以用相同的函数操作。例如 fprintf(stdout, "...") 等价于 printf("...")。区别在于标准流由系统自动管理,普通文件流需要手动 fopen / fclose。

  5. 文件操作的基本流程

    打开文件 → 检查是否成功 → 读写 → 检查读写结果 → 关闭文件 → 指针置空。每一步都不能省略,否则可能引发资源泄漏、数据丢失或程序崩溃。


" 避坑提示:"

  1. 打开文件后务必检查 fp 是否为 NULL

    文件不存在、路径错误、权限不足、磁盘满等都会导致 fopen 失败。不检查就直接使用,程序会崩溃。

    c 复制代码
    FILE *fp = fopen("test.txt", "w");
    if (fp == NULL) {
        perror("fopen");
        return EXIT_FAILURE;
    }
  2. 文件路径中的反斜杠 \ 在 C 字符串中需转义为 \\

    Windows 路径 C:\code\test.txt 中的 \t 会被当成制表符。正确写法是 "C:\\code\\test.txt",或者直接使用正斜杠 "C:/code/test.txt",C 标准库通常也能识别。

  3. 用 "w" 打开已存在的文件会清空原内容

    如果不想清空,应使用 "a" 追加模式,或先判断文件是否存在。操作重要文件前务必备份。

  4. fclose 后必须把 fp 置为 NULL

    关闭后文件流已释放,继续使用 fp 是未定义行为,可能崩溃或数据错乱。置空可避免野指针。

  5. 不要对 stdin、stdout、stderr 调用 fclose

    它们由系统自动管理,手动关闭会导致后续输入输出异常。标准流只在程序结束时由系统自动关闭。

  6. 建议检查 fclose 的返回值

    fclose 失败可能意味着数据未完整落盘。工程代码应检查:

    c 复制代码
    if (fclose(fp) != 0) {
        perror("fclose");
        return EXIT_FAILURE;
    }
    fp = NULL;

三、二进制文件和文本文件

根据数据的存储形式,文件分为:

  • 文本文件:以 ASCII 字符形式存储,每个字符占一个字节,便于人阅读。
  • 二进制文件:数据按内存中的二进制形式直接存储,不进行字符转换,节省空间但不可直接阅读。

示例 :整数 10000

  • 文本文件存储为字符 '1' '0' '0' '0' '0',占 5 字节。
  • 二进制文件存储为 4 字节的 int(小端序:0x10 0x27 0x00 0x00),占 4 字节。

代码示例:分别以文本和二进制写入整数,观察文件大小。

c 复制代码
#include <stdio.h>

int main(void) {
    int num = 10000;

    // 文本方式写入
    FILE *fp = fopen("text.txt", "w");
    if (fp == NULL) { perror("fopen"); return 1; }
    fprintf(fp, "%d", num);
    fclose(fp);

    // 二进制方式写入
    fp = fopen("binary.bin", "wb");
    if (fp == NULL) { perror("fopen"); return 1; }
    fwrite(&num, sizeof(int), 1, fp);
    fclose(fp);

    // 查看文件大小
    fp = fopen("text.txt", "rb");
    fseek(fp, 0, SEEK_END);
    long text_size = ftell(fp);
    fclose(fp);

    fp = fopen("binary.bin", "rb");
    fseek(fp, 0, SEEK_END);
    long bin_size = ftell(fp);
    fclose(fp);

    printf("text.txt 大小: %ld 字节\n", text_size);
    printf("binary.bin 大小: %ld 字节\n", bin_size);
    return 0;
}

" 要点总结:"

  1. 文本写入 vs 二进制写入的本质区别

    • fprintf(fp, "%d", num) 写入的是字符 '1','0','0','0','0',通常占 5 字节,没有换行符。
    • fwrite(&num, sizeof(int), 1, fp) 写入的是 num 在内存中的原始字节表示,大小是 sizeof(int)。常见平台 int 为 4 字节,所以 binary.bin 通常是 4 字节。
    • 二进制内容依赖字节序:小端机器上 10000 可能写成 10 27 00 00,大端机器上是 00 00 27 10,直接用文本编辑器打开会乱码。
  2. 文件打开与错误处理不完整

    • 写入时检查了 fopen 是否返回 NULL,但后面读取 text.txt 和 binary.bin 求大小时没有检查 fp == NULL。
    • 如果读取时文件打开失败,fseek(NULL, ...) 是未定义行为,通常会导致程序崩溃。
    • fseek、ftell、fprintf、fwrite、fclose 的返回值都没有检查。尤其 ftell 失败会返回 -1L。
  3. 模式与平台差异

    • "w" 是文本写,"wb" 是二进制写;"w" / "wb" 都会截断已有文件或创建新文件。
    • Windows 下文本模式可能把 \n 转换成 \r\n,但这里没写换行,所以影响不大。
    • 求文件大小时用 "rb" 更接近真实字节大小;不过 C 标准中文本流的 ftell 不保证一定是字节偏移,二进制流才是。跨平台求大小更推荐 stat、fseeko、ftello。
  4. 其他注意点

    • fclose 后数据才确保刷新到文件,再读取大小才准确。
    • 文件路径是相对当前工作目录,受权限、目录是否存在影响。
    • printf("... %ld ...", text_size) 与 long 类型匹配,格式正确。
    • 常见运行结果:text.txt 为 5 字节,binary.bin 为 4 字节(假设 sizeof(int) == 4)。

最关键的一个坑是:读取文件大小时没有判断 fopen 是否成功 。建议在每次 fopen 后都加:

c 复制代码
if (fp == NULL) {
    perror("fopen");
    return 1;
}

四、文件的打开和关闭

打开文件 :fopen(文件名, 打开模式)

关闭文件 :fclose(文件指针)

常用打开模式:

模式 含义
"r" 只读,文件必须存在
"w" 只写,文件不存在则创建,存在则清空
"a" 追加,文件不存在则创建,写入在末尾
"r+" 读写,文件必须存在
"w+" 读写,文件不存在则创建,存在则清空
"a+" 读写,追加
加 b 二进制模式,如 "rb"、"wb"

代码示例:打开文件并写入,再关闭。

c 复制代码
#include <stdio.h>

int main(void) {
    // 以只写方式打开,文件不存在则创建
    FILE *fp = fopen("demo.txt", "w");
    if (fp == NULL) {
        perror("fopen");
        return 1;
    }
    fputs("Hello, world!\n", fp);
    fclose(fp);  // 关闭文件,刷新缓冲区

    // 以只读方式打开,验证内容
    fp = fopen("demo.txt", "r");
    if (fp == NULL) {
        perror("fopen");
        return 1;
    }
    char buf[100];
    fgets(buf, sizeof(buf), fp);
    printf("文件内容: %s", buf);
    fclose(fp);
    return 0;
}

要点总结

  1. fopen 模式的含义

    • "w":只写,文件不存在则创建,存在则清空/截断。原内容会丢失。
    • "r":只读,文件必须存在,否则 fopen 返回 NULL。
    • 代码对两次 fopen 都做了 NULL 检查,这点是好的。
  2. fputs 的返回值没有检查

    • fputs 失败会返回 EOF,但代码没有判断。
    • 写入后调用 fclose 很重要,它会刷新缓冲区,确保数据真正写入文件。
    • fclose 的返回值也建议检查,因为磁盘满、写入错误可能在关闭时才暴露。
  3. fgets 的行为

    • fgets(buf, sizeof(buf), fp) 最多读 sizeof(buf)-1 个字符,并自动在末尾加 '\0'。
    • 遇到换行符 '\n' 或文件结束 EOF 会停止。
    • 如果读到换行符,换行符会保留在 buf 中 ,所以后面 printf("文件内容: %s", buf) 会原样输出换行。
    • buf 大小是 100,这里内容很短,安全;但如果一行超过 99 个字符,会被截断,剩余内容留在文件流中。
  4. 没有检查 fgets 是否读取成功

    • 如果 fgets 返回 NULL(例如文件为空或读取出错),buf 的内容是未定义的。

    • 此时继续 printf("%s", buf) 会使用未初始化数据,属于潜在 bug。

    • 更安全写法:

      c 复制代码
      if (fgets(buf, sizeof(buf), fp) != NULL) {
          printf("文件内容: %s", buf);
      } else {
          printf("读取失败或文件为空\n");
      }
  5. 文本模式与平台差异

    • "w" 和 "r" 是文本模式。在 Windows 上,写入 \n 可能被转换为 \r\n,读取时再转换回 \n。
    • 因此文件实际字节数可能和写入字符数不完全一致。

五、文件的顺序读写

顺序读写指从文件开头依次读写,主要函数:

函数 功能 适用于
fgetc / fputc 读写一个字符 所有文件
fgets / fputs 读写一行字符串 文本文件
fscanf / fprintf 格式化读写 文本文件
fread / fwrite 二进制块读写 二进制文件

代码示例:综合演示字符、字符串、格式化、二进制块读写。

c 复制代码
#include <stdio.h>

typedef struct {
    int id;
    char name[20];
    float score;
} Student;

int main(void) {
    // 1. 字符写入
    FILE *fp = fopen("chars.txt", "w");
    if (fp == NULL) { perror("fopen"); return 1; }
    fputc('A', fp);
    fputc('B', fp);
    fputc('C', fp);
    fclose(fp);

    // 2. 字符串写入
    fp = fopen("strings.txt", "w");
    fputs("Hello\n", fp);
    fputs("World\n", fp);
    fclose(fp);

    // 3. 格式化写入
    fp = fopen("format.txt", "w");
    fprintf(fp, "%d %s %.1f\n", 1, "Alice", 92.5);
    fprintf(fp, "%d %s %.1f\n", 2, "Bob", 88.0);
    fclose(fp);

    // 4. 二进制块写入
    Student s1 = {1, "Alice", 92.5f};
    Student s2 = {2, "Bob", 88.0f};
    fp = fopen("students.bin", "wb");
    fwrite(&s1, sizeof(Student), 1, fp);
    fwrite(&s2, sizeof(Student), 1, fp);
    fclose(fp);

    // 5. 读取验证
    printf("--- 字符文件 ---\n");
    fp = fopen("chars.txt", "r");
    int ch;
    while ((ch = fgetc(fp)) != EOF) {
        putchar(ch);
    }
    putchar('\n');
    fclose(fp);

    printf("--- 字符串文件 ---\n");
    fp = fopen("strings.txt", "r");
    char line[100];
    while (fgets(line, sizeof(line), fp) != NULL) {
        printf("%s", line);
    }
    fclose(fp);

    printf("--- 格式化文件 ---\n");
    fp = fopen("format.txt", "r");
    int id;
    char name[20];
    float score;
    while (fscanf(fp, "%d %s %f", &id, name, &score) == 3) {
        printf("%d %s %.1f\n", id, name, score);
    }
    fclose(fp);

    printf("--- 二进制文件 ---\n");
    fp = fopen("students.bin", "rb");
    Student stu;
    while (fread(&stu, sizeof(Student), 1, fp) == 1) {
        printf("%d %s %.1f\n", stu.id, stu.name, stu.score);
    }
    fclose(fp);

    return 0;
}

要点总结

  1. 文件打开与错误检查
  • 只有第一次 fopen("chars.txt", "w") 检查了 fp == NULL。

  • 后面所有 fopen 都没有检查。如果打开失败,fp 为 NULL,继续调用 fputs、fwrite、fgetc、fgets、fscanf、fread 等都是未定义行为,通常直接崩溃。

  • 每次 fopen 后都应加:

    c 复制代码
    if (fp == NULL) {
        perror("fopen");
        return 1;
    }
  • fclose 的返回值也没检查。磁盘满、写入失败可能在 fclose 时才暴露。

  1. 写入操作的返回值
  • fputc、fputs、fprintf、fwrite 的返回值都没有检查。
  • 例如:
    • fputc 失败返回 EOF;
    • fputs 失败返回 EOF;
    • fprintf 返回实际写入字符数,负值表示错误;
    • fwrite 返回成功写入的元素个数,应该判断是否等于预期值。
  • 生产代码中应检查这些返回值,避免"以为写成功了,其实没写全"。
  1. 文本模式 vs 二进制模式
  • 写字符、字符串、格式化文本用 "w" / "r",属于文本模式。
  • 写结构体用 "wb" / "rb",属于二进制模式,这是正确的。
  • 在 Windows 上,文本模式会把 \n 转换成 \r\n,读取时再转换回来;二进制模式不会转换。
  • "w" 会截断已有文件,文件不存在则创建。所以每次运行都会覆盖原文件。
  1. 读取操作的注意点
  • fgetc 返回 int,用 int ch 接收是正确的,这样才能区分 EOF 和普通字符 0xFF。
  • fgets 会保留换行符 \n,所以 printf("%s", line) 输出时通常会自带换行。
  • 如果一行超过 sizeof(line)-1,fgets 会截断,下一次继续读剩余部分,输出可能没有换行。
  • fscanf(fp, "%d %s %f", &id, name, &score) == 3 的循环条件是对的,但:
    • %s 没有指定宽度,name[20] 可能被超长字符串溢出。应写成 %19s。
    • 如果文件格式不对,fscanf 会提前结束,无法区分是正常 EOF 还是格式错误。
  • 循环结束后可以检查 feof / ferror 来确认原因。
  1. 结构体二进制读写的可移植性
c 复制代码
fwrite(&s1, sizeof(Student), 1, fp);
fread(&stu, sizeof(Student), 1, fp);

这种直接读写结构体的方式有几个问题:

  • 平台相关 :int 大小、float 表示、字节序、结构体对齐和填充都可能不同。
  • 填充字节 :Student 中可能有编译器插入的填充字节,内容不确定,写入文件后可能包含垃圾数据。
  • 跨平台不可读:小端机器写的文件,大端机器直接读会出错。
  • 安全/版本问题:结构体定义一变,旧文件就可能读不出来。
  • 如果只是本机临时保存,可以用;如果要跨平台或长期存储,建议逐字段序列化,例如固定宽度整数、固定长度字符串,并明确字节序。

六、文件的随机读写

随机读写指可以定位到文件的任意位置进行读写,主要函数:

  • fseek(fp, offset, origin):移动文件指针。
    origin:SEEK_SET(文件头)、SEEK_CUR(当前位置)、SEEK_END(文件尾)。
  • ftell(fp):返回当前偏移量。
  • rewind(fp):将指针移回文件开头。

代码示例:写入若干数据后,随机读取第三个结构体。

c 复制代码
#include <stdio.h>

typedef struct {
    int id;
    char name[20];
    float score;
} Student;

int main(void) {
    Student stu[3] = {
        {1, "Alice", 92.5f},
        {2, "Bob", 88.0f},
        {3, "Cindy", 95.5f}
    };

    // 写入三个学生到二进制文件
    FILE *fp = fopen("students.bin", "wb");
    if (fp == NULL) { perror("fopen"); return 1; }
    fwrite(stu, sizeof(Student), 3, fp);
    fclose(fp);

    // 随机读取第二个学生(下标1)
    fp = fopen("students.bin", "rb+");
    if (fp == NULL) { perror("fopen"); return 1; }

    // 定位到第二个学生:偏移 1 * sizeof(Student)
    fseek(fp, 1 * sizeof(Student), SEEK_SET);
    Student s;
    fread(&s, sizeof(Student), 1, fp);
    printf("第二个学生: %d %s %.1f\n", s.id, s.name, s.score);

    // 修改第三个学生
    fseek(fp, 2 * sizeof(Student), SEEK_SET);
    Student new_s = {3, "Cindy", 99.0f};
    fwrite(&new_s, sizeof(Student), 1, fp);

    // 定位到文件末尾,获取文件大小
    fseek(fp, 0, SEEK_END);
    long size = ftell(fp);
    printf("文件大小: %ld 字节\n", size);

    // 回到开头读取
    rewind(fp);
    while (fread(&s, sizeof(Student), 1, fp) == 1) {
        printf("记录: %d %s %.1f\n", s.id, s.name, s.score);
    }

    fclose(fp);
    return 0;
}

要点总结

  • fseek 可移动文件指针到任意位置。
  • ftell 获取当前偏移,常用于计算文件大小。
  • rewind 快速回到文件开头。
  • 随机读写常用于二进制文件,配合 fread/fwrite。

避坑提示

  • fseek 偏移量必须是 long 类型。
  • 文本文件中 fseek 行为可能受换行符转换影响,建议二进制模式使用。
  • 移动指针后读写前需确保位置有效。

一、核心函数:

1. fseek ------ 移动文件指针

c 复制代码
int fseek(FILE *stream, long offset, int whence);
  • offset:偏移量,单位是字节。
  • whence:基准位置,可取:
    • SEEK_SET:文件开头
    • SEEK_CUR:当前位置
    • SEEK_END:文件末尾
  • 返回值:成功返回 0,失败返回非 0。
  • 用途:随机读写时定位到任意记录。

2. ftell ------ 获取当前偏移

c 复制代码
long ftell(FILE *stream);
  • 返回当前文件指针相对于文件开头的字节偏移。

  • 失败返回 -1L,需检查。

  • 常配合 fseek(fp, 0, SEEK_END) 计算文件大小:

    c 复制代码
    fseek(fp, 0, SEEK_END);
    long size = ftell(fp);
    if (size == -1L) { perror("ftell"); }

3. rewind ------ 回到文件开头

c 复制代码
void rewind(FILE *stream);
  • 等价于 fseek(stream, 0, SEEK_SET),同时清除错误指示器。
  • 无返回值,不能直接判断是否成功。

4. fread / fwrite ------ 二进制块读写

c 复制代码
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
  • 返回值是成功读/写的元素个数,不是字节数。

  • 必须检查是否等于预期个数。

  • 随机读写常配合 fseek 定位到第 i 条记录:

    c 复制代码
    long offset = i * (long)sizeof(Record);
    fseek(fp, offset, SEEK_SET);
    fread(&rec, sizeof(Record), 1, fp);

二、随机读写要点:

  1. 按记录定长存储

    每条记录大小固定(如 sizeof(Student)),才能用 i * sizeof(Record) 直接定位。

  2. 偏移量计算

    c 复制代码
    long offset = index * (long)sizeof(Student);
    fseek(fp, offset, SEEK_SET);
    • sizeof 返回 size_t,fseek 需要 long,显式转换避免警告。
    • 大文件(>2GB)应使用 fseeko / ftello 或平台 64 位接口。
  3. 读写模式选择

    • "rb":只读二进制
    • "wb":只写二进制,会截断
    • "rb+":读写二进制,文件必须存在,不截断
    • "wb+":读写二进制,会截断
    • 随机读写通常用 "rb+"。
  4. 更新流的读写切换

    对 "rb+" 这类更新流,C 标准要求:

    • 写后要读 → 必须 fflush 或调用 fseek / rewind;
    • 读后要写 → 必须调用文件定位函数。
    • 否则行为未定义。
  5. 读取循环

    c 复制代码
    while (fread(&rec, sizeof(Record), 1, fp) == 1) {
        // 处理记录
    }
    if (ferror(fp)) { /* 读取出错 */ }
    • fread 返回 0 可能是 EOF,也可能是错误,用 feof / ferror 区分。
    • 文件大小不是记录大小整数倍时,末尾半条记录会被忽略。

三、避坑提示:

  1. 偏移量类型
  • fseek 第二个参数是 long,不是 size_t。

  • 直接写 fseek(fp, i * sizeof(Record), SEEK_SET); 可能产生隐式转换警告。

  • 推荐:

    c 复制代码
    fseek(fp, (long)(i * sizeof(Record)), SEEK_SET);
  • 大文件用 fseeko / ftello。

  1. 文本模式 vs 二进制模式
  • 文本模式("r" / "w")下,fseek 行为受换行符转换影响(尤其 Windows)。
  • 标准只保证:
    • fseek(fp, 0, SEEK_SET)
    • fseek(fp, ftell(fp), SEEK_SET)
    • fseek(fp, 0, SEEK_END)
      这些有限操作可移植。
  • 随机读写务必用二进制模式 ("rb" / "wb" / "rb+")。
  1. 移动指针后检查位置有效性
  • fseek 可能失败(如越界、设备不支持)。

  • 每次 fseek 后检查返回值:

    c 复制代码
    if (fseek(fp, offset, SEEK_SET) != 0) {
        perror("fseek");
        // 处理错误
    }
  • 读取前确认当前位置有数据,避免读到未初始化内容。

  1. 返回值检查不能省

    | 函数 | 失败返回 | 后果 |

    |---|---|---|

    | fopen | NULL | 后续操作崩溃 |

    | fseek | 非 0 | 位置错误 |

    | ftell | -1L | 大小错误 |

    | fread | < 预期个数 | 数据不完整 |

    | fwrite | < 预期个数 | 写入失败 |

    | fclose | EOF | 缓冲区未刷新,数据丢失 |

  2. 二进制结构体读写的可移植性

  • 直接 fwrite(&struct, sizeof(struct), 1, fp) 有以下问题:
    • 结构体填充字节内容不确定;
    • int、float 大小和字节序随平台变化;
    • 结构体定义变更导致旧文件不可读。
  • 跨平台或长期存储应逐字段序列化(固定宽度类型 + 明确字节序),或改用文本格式(CSV/JSON)。
  1. 字符串安全
  • 从文件读出的 char name[N] 不保证以 '\0' 结尾。

  • 使用前应手动补结尾:

    c 复制代码
    s.name[sizeof(s.name) - 1] = '\0';
  • 写入时确保不溢出,可用 snprintf 或 strncpy 并补 '\0'。

  1. 文件大小计算的正确姿势
c 复制代码
if (fseek(fp, 0, SEEK_END) != 0) { perror("fseek"); }
long size = ftell(fp);
if (size == -1L) { perror("ftell"); }
  • 二进制模式下结果才是准确的字节数。
  • 计算完大小后若要继续读,记得 rewind 或 fseek 回开头。
  1. 记录数不整时的处理
  • 文件大小 / sizeof(Record) 可能有余数。

  • 遍历时用 fread(...) == 1 判断,多余字节会被忽略。

  • 若需要严格校验,可先算记录数:

    c 复制代码
    long count = size / (long)sizeof(Record);

七、文件读取结束的判定

常见误区 :用 feof(fp) 判断文件是否结束。

正确做法 :根据读取函数的返回值判断。feof 只在读取函数因遇到文件尾而返回失败后,才返回真。

  • fgetc 返回 EOF 表示结束或错误。
  • fgets 返回 NULL 表示结束或错误。
  • fread 返回小于请求项数表示结束或错误。
  • feof(fp) 判断是否因文件尾结束。
  • ferror(fp) 判断是否因错误结束。

代码示例:正确读取文件并区分结束与错误。

c 复制代码
#include <stdio.h>

int main(void) {
    FILE *fp = fopen("students.txt", "r");
    if (fp == NULL) { perror("fopen"); return 1; }

    int ch;
    while ((ch = fgetc(fp)) != EOF) {
        putchar(ch);
    }

    // 判断结束原因
    if (feof(fp)) {
        printf("\n正常读到文件末尾。\n");
    } else if (ferror(fp)) {
        perror("读取错误");
    }

    fclose(fp);
    return 0;
}

二进制文件读取示例:

c 复制代码
#include <stdio.h>

enum { SIZE = 5 };

int main(void) {
    double a[SIZE] = {1.1, 2.2, 3.3, 4.4, 5.5};
    FILE *fp = fopen("doubles.bin", "wb");
    if (!fp) { perror("fopen"); return 1; }
    fwrite(a, sizeof *a, SIZE, fp);
    fclose(fp);

    double b[SIZE];
    fp = fopen("doubles.bin", "rb");
    if (!fp) { perror("fopen"); return 1; }
    size_t ret = fread(b, sizeof *b, SIZE, fp);

    if (ret == SIZE) {
        printf("读取成功: ");
        for (int i = 0; i < SIZE; i++) printf("%.1f ", b[i]);
        printf("\n");
    } else {
        if (feof(fp)) printf("意外到达文件末尾\n");
        else if (ferror(fp)) perror("读取错误");
    }
    fclose(fp);
    return 0;
}

要点总结

  • 不要用 feof 作为循环条件。
  • 读取函数的返回值才是判断依据。
  • feof 和 ferror 用于事后判断原因。

避坑提示

  • feof 在读取失败后才置位,提前调用无意义。
  • 二进制文件读取时,fread 返回值小于预期可能是文件尾或错误,需结合 feof/ferror。

八、文件缓冲区

C 标准 I/O 库使用缓冲区提高效率。数据先写入内存缓冲区,再批量写入磁盘。缓冲区分为:

  • 全缓冲:普通文件,缓冲区满才刷新。
  • 行缓冲:终端设备(如屏幕),遇换行刷新。
  • 无缓冲 :标准错误 stderr,立即输出。

刷新缓冲区:fflush(fp)。程序正常结束或 fclose 也会刷新。

代码示例:演示缓冲区行为。

c 复制代码
#include <stdio.h>

int main(void) {
    FILE *fp = fopen("buffer.txt", "w");
    if (fp == NULL) { perror("fopen"); return 1; }

    fputs("Hello, buffer!", fp);

    // 此时数据可能还在缓冲区,未写入磁盘
    printf("数据已写入缓冲区,按回车继续...\n");
    getchar();

    // 刷新缓冲区,强制写入磁盘
    fflush(fp);
    printf("缓冲区已刷新,数据已写入磁盘。\n");

    fclose(fp);
    return 0;
}

运行后,打开 buffer.txt 会看到内容。若注释掉 fflush,在按回车前文件可能为空(取决于系统)。

要点总结

  • 缓冲区减少系统调用,提高效率。
  • fflush 强制刷新,fclose 自动刷新。
  • 程序异常终止可能丢失缓冲区数据。

避坑提示

  • 调试时若文件内容未出现,可能因缓冲区未刷新。
  • 多线程或信号处理中注意缓冲区安全。
  • fflush(stdin) 不是标准行为,不要使用。

九、总结

文件操作是 C 语言中连接程序与磁盘的纽带。掌握以下心法,即可游刃有余:

  1. 为何用文件:持久化存储,数据独立于程序。
  2. 文件是什么 :FILE 结构,通过指针操作。
  3. 文本与二进制:文本可读,二进制高效,跨平台慎用二进制。
  4. 打开与关闭 :fopen 选对模式,fclose 必须调用。
  5. 顺序读写:字符、字符串、格式化、二进制块,各司其职。
  6. 随机读写 :fseek 定位,ftell 测偏移,rewind 回开头。
  7. 结束判定 :看返回值,feof/ferror 仅作事后分析。
  8. 缓冲区 :提高效率,fflush 强制刷新,fclose 自动刷新。

核心心法 :打开要判空,关闭要牢记;读写看返回,缓冲区要刷新。

掌握文件操作,你的 C 程序就能与整个世界交换数据。


如果觉得有用,欢迎点赞收藏支持一下!

相关推荐
SunnyDays10111 小时前
Java 合并 PDF 文件:完整合并、指定页面合并与流合并
java·开发语言·合并pdf
和侧2 小时前
MOS器件——ESD测试
笔记·学习
kimnoic2 小时前
Python操作xlwings的实例详解
开发语言·python
优化Henry2 小时前
光路异常告警排查实战:LTE室分与5G小区两个案例解析
运维·网络·学习·5g·tdd
Zwawa2 小时前
传感器模块都在交出什么:从模拟电压到数字协议
c语言·单片机·嵌入式
栗子~~2 小时前
java - logstash-logback-encoder 集成 demo
java·开发语言·logback
田里的水稻2 小时前
EI_模仿学习IL---工程链路
人工智能·深度学习·学习·机器学习·迁移学习
Linux-lucky2 小时前
43-Linux学习之旅之Shell 脚本(二)
linux·运维·学习·ubuntu·dash
XHGILY3333 小时前
职业院校实习计划制定:科学规划与落地执行策略深度解析
学习