3、stdio.h 核心函数(下):fopen与fclose、fread与fwrite、fprintf与fscanf、文件打开模式详解

好,咱们接着聊 stdio.h 里的重头戏。上一章讲了 printf 和 scanf 那套标准 I/O,说白了那是跟控制台打交道。但真正的工程里,数据总不能每次都敲进去吧?你得读写文件。

这一章,我把文件操作里最核心的几个函数掰开揉碎了讲。fopen/fclose、fread/fwrite、fprintf/fscanf,还有那六个打开模式——r、w、a、r+、w+、a+。嗯,这些名字看着简单,但坑不少。我当年刚入行时,就因为搞混了 w+ 和 r+,把同事的配置文件给清空了……那场面,至今难忘。

3.1 文件打开与关闭:fopen 与 fclose

先看 fopen。它的原型长这样:

FILE *fopen(const char *filename, const char *mode);

它返回一个 FILE 指针。如果打开失败,返回 NULL。我个人习惯,每次 fopen 之后立刻检查返回值,绝不偷懒。为什么?因为文件可能不存在、权限不够、磁盘满了——任何意外都可能发生。

警告:永远不要假设 fopen 一定成功。不检查返回值就直接读写,程序崩溃是迟早的事。

有开就有关。fclose 负责释放资源:

int fclose(FILE *stream);

成功返回 0,失败返回 EOF。我记得有一次线上排查问题,发现程序跑着跑着文件描述符耗尽了。查了半天,原来是某个分支路径下忘了 fclose。嗯,这种泄漏在嵌入式或高并发环境里是致命的。

个人经验:我习惯在 fopen 之后立刻写对应的 fclose,就像写大括号一样,先配对再填中间逻辑。这样不容易漏。

3.2 文件打开模式详解:r/w/a/r+/w+/a+

这六个模式,是文件操作的基石。我画了一张图帮你理清关系:

文件打开模式分类 只读模式 r — 文件必须存在 从文件开头读 不能写,不能创建 只写模式 w — 创建或截断 文件存在则清空 不存在则新建 追加模式 a — 文件必须存在 写入追加到末尾 不存在则新建 读写模式 r+ — 文件必须存在 可读可写,不截断 写从当前位置开始 写读模式 w+ — 创建或截断 可读可写 先清空再读写 追加读模式 a+ — 追加并读取 读从开头开始 写始终追加到末尾

你想想看,这六个模式其实就三组:只读、只写、读写。每组又分两种变体。我重点说说容易搞混的地方。

r 与 r+

r 是只读,文件必须存在。r+ 是读写,文件也必须存在,但不会清空内容。写操作从当前位置开始覆盖。我曾经用 r+ 修改一个配置文件,结果因为没调 fseek,写到了错误的位置……嗯,从那以后我每次用 r+ 都会先确认文件指针的位置。

w 与 w+

w 是只写,文件存在就清空,不存在就新建。w+ 是读写,同样会清空文件。注意:w+ 虽然能读,但文件已经被清空了,你读到的只能是空内容。所以 w+ 的实际用途是:先写再读,验证写入是否正确。

关键区别:r+ 不会破坏原有内容,w+ 会。如果你只是想修改文件中的某一部分,用 r+。如果你想重写整个文件,用 w+。

a 与 a+

a 是追加,写入永远在文件末尾。a+ 是追加加读取,读可以从任意位置开始,但写永远在末尾。这个模式在日志系统里特别常用。我做过一个日志收集程序,就用 a+ 模式,一边读已有日志做分析,一边追加新日志。

3.3 二进制读写:fread 与 fwrite

这两个函数是二进制读写的核心。原型如下:

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);

参数看着多,其实逻辑很简单:从 stream 中读取 nmemb 个元素,每个元素 size 字节,存到 ptr 指向的内存里。返回值是实际读取/写入的元素个数。

举个例子,读写一个结构体数组:

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

// 写入
Student students[100];
// ... 填充数据 ...
size_t written = fwrite(students, sizeof(Student), 100, fp);
if (written != 100) {
    // 写入不完整,处理错误
}

// 读取
Student buffer[100];
size_t read = fread(buffer, sizeof(Student), 100, fp);
if (read != 100) {
    if (feof(fp)) {
        // 文件提前结束
    } else {
        // 读取出错
    }
}
避坑指南:我曾经犯过一个错误——用 fwrite 写结构体时,结构体里有指针成员。结果写进去的是指针地址,不是实际数据。下次读回来,指针指向的内存早就变了。记住:fwrite/fread 只适合写「扁平」数据,不要包含指针。

3.4 格式化读写:fprintf 与 fscanf

这两个函数是 printf/scanf 的文件版本。原型:

int fprintf(FILE *stream, const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);

用法和 printf/scanf 几乎一样,只是多了一个 FILE 参数。我个人更倾向于用 fprintf 写文本配置文件,因为可读性好,方便调试。

// 写入配置文件
fprintf(fp, "host=%s\nport=%d\ntimeout=%d\n", host, port, timeout);

// 读取配置文件
char host[64];
int port, timeout;
fscanf(fp, "host=%s\nport=%d\ntimeout=%d\n", host, &port, &timeout);
小技巧:fscanf 遇到格式不匹配时会停止读取,并且不会跳过错误数据。我建议用 fgets 先读一行,再用 sscanf 解析,这样更容易控制错误处理。

3.5 综合示例:一个简单的日志记录器

把上面这些函数串起来,写一个实用的日志记录器。你想想看,实际项目中日志有多重要?

#include <stdio.h>
#include <time.h>

int log_message(const char *filename, const char *level, const char *msg) {
    FILE *fp = fopen(filename, "a+");
    if (!fp) {
        perror("fopen");
        return -1;
    }

    time_t now = time(NULL);
    char time_str[64];
    strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", localtime(&now));

    // 使用 fprintf 写入格式化日志
    fprintf(fp, "[%s] [%s] %s\n", time_str, level, msg);

    fclose(fp);
    return 0;
}

int main() {
    log_message("app.log", "INFO", "程序启动");
    log_message("app.log", "WARN", "内存使用率超过80%");
    log_message("app.log", "ERROR", "数据库连接失败");

    // 读取并显示所有日志
    FILE *fp = fopen("app.log", "r");
    if (fp) {
        char line[256];
        while (fgets(line, sizeof(line), fp)) {
            printf("%s", line);
        }
        fclose(fp);
    }
    return 0;
}

这个例子用了 a+ 模式,每次写入都追加到末尾。读取时用 r 模式从头开始。嗯,代码虽短,但涵盖了 fopen、fclose、fprintf、fgets 的典型用法。

3.6 常见错误与避坑

  • 忘记 fclose:文件描述符泄漏。轻则程序运行一段时间后无法打开新文件,重则导致系统资源耗尽。
  • 模式选错:用 w 模式打开已有文件,内容瞬间消失。我有个同事曾经用 w 打开了配置文件……嗯,版本管理的重要性就在这时候体现出来了。
  • fread 返回值检查不完整:返回值小于请求数,不一定是错误,也可能是文件结束。要用 feof 和 ferror 区分。
  • fprintf 和 fscanf 的格式不匹配:写入时用 "%d",读取时用 "%s",数据就全乱了。
总结一下:文件操作的核心就三件事——打开、读写、关闭。打开时要选对模式,读写时要检查返回值,关闭时不要忘记。做到这三点,大部分文件操作的问题都能避免。

好了,这一章的内容就到这儿。记住我强调的那几个坑,尤其是 w 和 w+ 的清空行为。下一章咱们聊聊文件定位和缓冲机制,那又是另一片天地。


专注资料整理