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 知识体系:文件打开与关闭的核心逻辑

下面这张图,是我梳理的文件打开与关闭的完整流程。你可以把它当作一个检查清单。

开始文件操作 fopen(filename, mode) 选择正确的模式 (r/w/a/rb/wb等) 返回值 == NULL ? 错误处理 (perror / return) 执行读写操作 fread / fwrite / fprintf / fscanf fclose(fp) 刷新缓冲区,释放资源 操作完成

这张图的核心逻辑很简单:打开 -> 检查 -> 读写 -> 关闭。少一步都不行。尤其是错误检查那一步,我见过太多人跳过了。

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秒钟,可能会帮你省下后面几个小时的调试时间。


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