文件操作:fopen、fclose、fread、fwrite 的常见错误

文件操作,说白了就是程序跟硬盘上的文件打交道。很多初学者觉得这玩意儿简单,不就是打开、读写、关闭嘛。嗯,表面上看确实就这几步,但实际项目中,我见过太多因为文件操作翻车的案例了。

我个人习惯把文件操作比作「借书」:fopen 是借书,fclose 是还书,fread/fwrite 是看书和写笔记。你借了书不还,图书馆迟早找你麻烦;你借了书没借到,后面的事就别干了。道理就这么简单,但坑就藏在细节里。

一、fopen 的常见错误

1. 忘记检查返回值

这是最基础的错误,也是最容易犯的。fopen 返回 NULL 表示打开失败,但很多人直接拿来就用。

FILE *fp = fopen("data.bin", "rb");
// 错误:没检查就直接用
fread(buf, 1, 100, fp);  // fp 可能是 NULL,直接崩溃
警告:fopen 失败的原因很多——文件不存在、权限不够、路径错误、磁盘满了。不检查返回值,等于在雷区里裸奔。

正确的做法:

FILE *fp = fopen("data.bin", "rb");
if (fp == NULL) {
    perror("fopen failed");
    return -1;
}

我在项目中遇到过一回,客户说程序偶尔崩溃,查了两天才发现是某个配置文件被误删了,fopen 返回 NULL,但代码里没检查,直接往下走就段错误了。从那以后,我写任何文件操作,第一件事就是检查返回值。

2. 模式字符串写错

fopen 的第二个参数是模式字符串,比如 "r"、"w"、"a"、"rb"、"wb" 等等。写错了,后果很严重。

模式 含义 常见错误
"r" 只读,文件必须存在 用 "r" 打开不存在的文件 → 返回 NULL
"w" 只写,文件存在则清空,不存在则创建 想追加数据却用了 "w" → 原有数据全没了
"a" 追加,文件存在则末尾写入,不存在则创建 想覆盖写入却用了 "a" → 数据永远追加在末尾
"rb"/"wb" 二进制模式读写 文本模式下读写二进制文件 → 数据被篡改(Windows 下 \n 变 \r\n)
提示:在 Windows 下,文本模式和二进制模式差别很大。文本模式下,\n 会被自动转换成 \r\n,读的时候反过来。如果你在读写二进制文件(比如图片、音频),一定要用 "rb"/"wb",否则数据会损坏。

二、fclose 的常见错误

1. 忘记关闭文件

你想想看,每次 fopen 都会占用一个文件描述符。系统能同时打开的文件数是有限的(通常 1024 个)。如果你只开不关,迟早会耗尽资源。

for (int i = 0; i < 10000; i++) {
    FILE *fp = fopen("log.txt", "a");
    fprintf(fp, "entry %d\n", i);
    // 忘记 fclose!
}

这段代码跑到第 1024 次左右,fopen 就会返回 NULL,因为文件描述符用完了。更可怕的是,程序退出时,未关闭的文件缓冲区里的数据可能没写回磁盘,导致数据丢失。

警告:fclose 不仅释放资源,还会刷新缓冲区。只 fopen 不 fclose,等于借了书不还,还把人家的笔记扔垃圾桶里。

2. 重复关闭

有些人为了保险,在多个地方调用 fclose,结果重复关闭导致未定义行为。

FILE *fp = fopen("data.bin", "rb");
// ... 读写操作 ...
fclose(fp);
// ... 又过了几行 ...
fclose(fp);  // 错误!fp 已经是无效指针

重复关闭可能引发崩溃,也可能悄无声息地破坏内存。我的习惯是:fclose 之后立刻把 fp 置为 NULL,这样即使不小心再次调用 fclose(NULL),也是安全的(标准库允许 fclose(NULL) 什么都不做)。

fclose(fp);
fp = NULL;

三、fread 和 fwrite 的常见错误

1. 返回值检查不完整

fread 返回实际读取的元素个数,fwrite 返回实际写入的元素个数。很多人只检查是否等于 0,但忽略了部分读写的情况。

char buf[1024];
size_t ret = fread(buf, 1, 1024, fp);
if (ret == 0) {
    // 错误:只检查了完全失败
    printf("read failed\n");
}
// 如果只读了 500 字节,ret == 500,但代码认为读满了

正确的做法是:

size_t ret = fread(buf, 1, sizeof(buf), fp);
if (ret != sizeof(buf)) {
    if (feof(fp)) {
        printf("文件已读完,实际读取 %zu 字节\n", ret);
    } else if (ferror(fp)) {
        printf("读取错误\n");
    }
}
关键点:fread 返回值小于请求数,不一定是错误。可能是文件结束了,也可能是发生了错误。要用 feof() 和 ferror() 来区分。

2. 缓冲区大小不匹配

fread 和 fwrite 的第二个参数是「每个元素的大小」,第三个参数是「元素个数」。很多人搞混这两个参数,或者缓冲区大小跟参数不匹配。

int arr[100];
// 错误:元素大小写错了
fwrite(arr, sizeof(int), 100, fp);  // 正确
fwrite(arr, 100, sizeof(int), fp);  // 错误!语义完全变了

第一个参数是「每个元素大小」,第二个是「元素个数」。虽然最终写入的字节数一样(都是 400 字节),但语义不同。如果发生部分写入,返回值代表的是「完整写入的元素个数」,参数顺序错了,返回值就失去了意义。

我个人习惯统一写成:fread(buf, 1, sizeof(buf), fp),这样返回值就是实际读取的字节数,直观好理解。

3. 文件指针位置混乱

fread 和 fwrite 不会自动重置文件指针位置。如果你先写后读,或者先读后写,文件指针的位置决定了你读到/写到哪里。

FILE *fp = fopen("data.bin", "wb");
fwrite(data, 1, 100, fp);
// 此时文件指针在 100 字节处
fread(buf, 1, 100, fp);  // 错误!文件指针在末尾,读不到任何数据

正确的做法是用 fseek 或 rewind 调整文件指针:

fwrite(data, 1, 100, fp);
rewind(fp);  // 回到文件开头
fread(buf, 1, 100, fp);
提示:fopen 的 "w+" 模式允许读写,但每次切换读写操作前,必须调用 fseek、rewind 或 fflush。这是标准库的要求,不遵守会导致未定义行为。

四、知识体系总览

下面这张图把文件操作的常见错误串起来了,你可以对照着检查自己的代码:

文件操作常见错误知识体系 fopen 错误 fclose 错误 fread 错误 fwrite 错误 未检查返回值 模式字符串写错 忘记关闭文件 重复关闭 返回值检查不完整 缓冲区大小不匹配 返回值检查不完整 缓冲区大小不匹配 核心原则:检查返回值 → 匹配模式 → 及时关闭 → 管理指针 避坑指南:fclose 后立即置 NULL,fread/fwrite 用 feof/ferror 判断

五、避坑指南总结

我曾经在一个嵌入式项目里吃过文件操作的亏。当时写日志文件,fopen 用了 "w" 模式,结果每次重启程序都把之前的日志清空了。排查了半天才发现是模式用错了,应该用 "a" 追加模式。嗯,从那以后我写文件操作都会多看一眼模式字符串。

这里给你几个我自己的习惯,你可以参考:

  • fopen 后立刻检查 NULL,并用 perror 打印错误原因
  • fclose 后立刻置 NULL,防止野指针
  • fread/fwrite 的返回值必须跟请求数比较,不要只检查 0
  • 读写切换时记得 fseek 或 rewind
  • 二进制文件一律用 "rb"/"wb",别偷懒用 "r"/"w"

一句话总结:文件操作没有想象中那么难,但每一个细节都可能让你翻车。检查、检查、再检查——这是我在这个领域摔过跟头后最深的体会。


公众号:蓝海资料掘金营,微信deep3321