3. 文件的打开与关闭:fopen与fclose深度解析
文件操作的第一步是什么?说白了,就是打开文件。你想想看,你要读写一个文件,总得先跟操作系统打个招呼,告诉它你要干嘛。这个打招呼的动作,在C语言里就是fopen函数。而用完文件之后,还得礼貌地关掉,这就是fclose的活儿。
我刚开始学嵌入式那会儿,总觉得打开文件就是走个过场。直到有一次,我在一个数据采集项目里,因为忘记检查fopen的返回值,程序在SD卡没插好的情况下直接跑飞了。嗯,从那以后,我再也不敢小看这个函数了。
3.1 fopen函数:打开文件的钥匙
fopen的原型长这样:
FILE *fopen(const char *filename, const char *mode);
它接受两个参数:文件名和打开模式。返回值是一个FILE指针。如果打开失败,返回NULL。就这么简单,但坑全在第二个参数里。
核心要点:永远、永远、永远要检查fopen的返回值是否为NULL。这不是代码风格问题,这是程序健壮性的底线。
3.2 文件打开模式:r/w/a/r+/w+/a+ 及其二进制变体
打开模式决定了你能对文件做什么。我习惯把它们分成三类:只读、只写、读写。咱们一个一个看。
| 模式 | 含义 | 文件不存在时 | 文件存在时 |
|---|---|---|---|
"r" |
只读 | 返回NULL | 正常打开,位置在文件开头 |
"w" |
只写 | 创建新文件 | 清空内容,从头写 |
"a" |
追加 | 创建新文件 | 保留内容,位置在文件末尾 |
"r+" |
读写(不创建) | 返回NULL | 正常打开,位置在开头 |
"w+" |
读写(创建/覆盖) | 创建新文件 | 清空内容,从头读写 |
"a+" |
读写(追加模式) | 创建新文件 | 保留内容,读从头,写末尾 |
你可能会问:"r+"和"w+"有什么区别?我举个例子你就明白了。假设你有一个日志文件,里面存着昨天的数据。用"r+"打开,你可以读它,也可以修改它,但文件必须已经存在。用"w+"打开,如果文件存在,它会被清空——这往往不是你想要的。
我的习惯:在嵌入式开发中,我几乎只用"r"、"w"、"a"和它们的二进制版本。读写模式"r+"和"w+"我很少用,因为容易搞混文件位置指针,调试起来很头疼。
3.3 文本模式 vs 二进制模式
在Windows系统上,文本模式和二进制模式有区别。文本模式下,'\n'会被自动转换成'\r\n'(写入时)或'\r\n'转换成'\n'(读取时)。二进制模式则不做任何转换。
在Linux和macOS上,两者没有区别。但为了可移植性,我建议:
- 处理文本文件(如配置文件、日志)时,用
"r"、"w"、"a" - 处理二进制文件(如固件、图片、音频)时,用
"rb"、"wb"、"ab"
我曾经踩过的坑:在一个跨平台项目中,我在Windows上用"w"模式写了一个二进制数据文件,结果在Linux上读出来总是多几个字节。查了半天才发现是文本模式下的换行符转换搞的鬼。从那以后,只要涉及二进制数据,我必用"wb"。
3.4 fclose函数:用完记得关门
fclose的原型很简单:
int fclose(FILE *stream);
成功返回0,失败返回EOF。它的作用不仅仅是关闭文件,还会刷新缓冲区,确保所有数据都真正写到了磁盘上。
你可能会想:程序退出时,系统会自动关闭所有打开的文件。没错,但依赖这个习惯很危险。我见过一个同事,在长时间运行的数据采集程序里,每次循环都fopen但不fclose,结果文件描述符耗尽,程序崩溃。嗯,这种bug很难复现,但一旦出现就是大问题。
最佳实践:fopen和fclose要成对出现。我写代码时,总是在fopen之后立即写下对应的fclose,然后再填充中间的读写逻辑。这样就不会忘了。
3.5 知识体系:文件打开与关闭的核心逻辑
下面这张图,是我梳理的文件打开与关闭的完整流程。你可以把它当作一个检查清单。
这张图的核心逻辑很简单:打开 -> 检查 -> 读写 -> 关闭。少一步都不行。尤其是错误检查那一步,我见过太多人跳过了。
3.6 完整示例:打开、写入、关闭
最后,给你看一个完整的例子。这是我写日志模块时常用的模式:
#include <stdio.h>
#include <stdlib.h>
int write_log(const char *msg) {
// 以追加模式打开日志文件
FILE *fp = fopen("system.log", "a");
if (fp == NULL) {
perror("fopen failed");
return -1;
}
// 写入日志
fprintf(fp, "[INFO] %s\n", msg);
// 关闭文件
if (fclose(fp) != 0) {
perror("fclose failed");
return -1;
}
return 0;
}
int main() {
if (write_log("系统启动成功") == 0) {
printf("日志写入成功\n");
}
return 0;
}
注意看,我用了"a"模式而不是"w"。为什么?因为日志文件要保留历史记录,用"w"会清空之前的内容。这就是模式选择的重要性。
一个小技巧:在嵌入式系统里,如果文件系统是只读的(比如某些ROMFS),fopen用"r"或"rb"就好。别试图用"w",它会直接返回NULL。我习惯在代码里加一个断言或者日志,把fopen失败的原因打印出来,调试时能省不少时间。
好了,关于文件的打开与关闭,核心就是这些。记住:fopen是入口,fclose是出口,中间的模式选择决定了你能做什么。下次写文件操作时,多花10秒钟想想模式选对了没有——这10秒钟,可能会帮你省下后面几个小时的调试时间。