文件操作: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,直接崩溃
正确的做法:
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) |
二、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,因为文件描述符用完了。更可怕的是,程序退出时,未关闭的文件缓冲区里的数据可能没写回磁盘,导致数据丢失。
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");
}
}
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" 模式,结果每次重启程序都把之前的日志清空了。排查了半天才发现是模式用错了,应该用 "a" 追加模式。嗯,从那以后我写文件操作都会多看一眼模式字符串。
这里给你几个我自己的习惯,你可以参考:
- fopen 后立刻检查 NULL,并用 perror 打印错误原因
- fclose 后立刻置 NULL,防止野指针
- fread/fwrite 的返回值必须跟请求数比较,不要只检查 0
- 读写切换时记得 fseek 或 rewind
- 二进制文件一律用 "rb"/"wb",别偷懒用 "r"/"w"
一句话总结:文件操作没有想象中那么难,但每一个细节都可能让你翻车。检查、检查、再检查——这是我在这个领域摔过跟头后最深的体会。
公众号:蓝海资料掘金营,微信deep3321