7、标准输入输出重定向:freopen()函数的使用

说实话,很多C语言教材讲文件操作时,都会把freopen()放在角落里。我刚开始学的时候也觉得这函数没啥用——不就是换个文件流嘛?直到有一次做嵌入式日志系统,我才发现这玩意儿简直是救星。

今天我们就来聊聊freopen()。它能把标准输入、标准输出、标准错误流,统统重定向到文件里。说白了,就是让printf()打印到文件,让scanf()从文件读数据。

7.1 为什么需要重定向?

你想想看,在嵌入式开发中,很多时候没有显示器、没有键盘。程序跑在开发板上,printf()打印的东西你看不到。怎么办?

我个人的习惯是:把标准输出重定向到一个日志文件。这样程序运行结束后,打开文件就能看到所有输出。调试起来特别方便。

再比如,你要批量测试一组输入数据。手动敲键盘?太慢了。把数据写进文件,重定向标准输入,程序自动读取——效率翻倍。

核心思想: 重定向不改变代码逻辑,只改变数据流向。你的printf()scanf()fgets()等函数完全不用改。

7.2 freopen()函数原型

先看函数原型,很简单:

FILE *freopen(const char *filename, const char *mode, FILE *stream);

三个参数:

  • filename:要打开的文件名
  • mode:打开模式,和fopen()一样("r", "w", "a"等)
  • stream:要重定向的流,通常是stdinstdoutstderr

返回值:成功返回文件指针,失败返回NULL。

小技巧: 调用freopen()后,原来的流会被关闭。你不需要手动fclose()它。

7.3 标准输出重定向到文件

这是最常用的场景。把printf()的输出写到文件里。

#include <stdio.h>

int main() {
    // 将标准输出重定向到 output.txt
    if (freopen("output.txt", "w", stdout) == NULL) {
        perror("freopen failed");
        return 1;
    }

    // 这些 printf 会写入文件,而不是显示在屏幕上
    printf("这是第一行日志\n");
    printf("这是第二行日志\n");
    printf("当前时间:2024年\n");

    // 恢复标准输出到屏幕(可选)
    // 注意:freopen 没有直接的"恢复"函数
    // 可以用 dup() 系统调用保存原始 fd,但那是另一回事了

    return 0;
}

运行后,打开output.txt,你会看到三行文本。屏幕上反而什么都没有。

注意: 重定向后,所有printf()输出都去了文件。如果你还想在屏幕上显示一些信息,得用fprintf(stderr, ...)。标准错误流默认还是屏幕。

7.4 标准输入重定向到文件

这个场景我经常用在自动化测试上。把测试数据写进文件,程序从文件读取。

#include <stdio.h>

int main() {
    int a, b;

    // 将标准输入重定向到 input.txt
    if (freopen("input.txt", "r", stdin) == NULL) {
        perror("freopen failed");
        return 1;
    }

    // 这些 scanf 会从文件读取数据
    printf("请输入两个整数:\n");  // 这行也会被重定向到文件(如果 stdout 没改)
    scanf("%d %d", &a, &b);
    printf("a + b = %d\n", a + b);

    return 0;
}

假设input.txt内容为:

10 20

程序运行后,scanf()直接从文件读取10和20,不需要你手动输入。

我曾经踩过的坑: 有一次我把stdin和stdout都重定向到了同一个文件。结果程序卡死了——因为写入和读取互相干扰。记住,不要同时重定向stdin和stdout到同一个文件,除非你明确知道自己在做什么。

7.5 标准错误流重定向

标准错误流stderr通常用于输出错误信息。重定向它,可以把错误日志单独存到一个文件。

#include <stdio.h>

int main() {
    // 将标准错误重定向到 error.log
    if (freopen("error.log", "a", stderr) == NULL) {
        perror("freopen failed");
        return 1;
    }

    fprintf(stderr, "警告:内存不足\n");
    fprintf(stderr, "错误:文件打开失败\n");

    // printf 仍然输出到屏幕(如果 stdout 没被重定向)
    printf("程序正常运行\n");

    return 0;
}

这样,错误信息追加到error.log,正常输出显示在屏幕。调试时一目了然。

7.6 恢复标准流

freopen()没有提供直接的"恢复"函数。但有一个常用技巧:在重定向前,先保存原始文件描述符。

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main() {
    int saved_stdout_fd;

    // 保存原始标准输出的文件描述符
    saved_stdout_fd = dup(STDOUT_FILENO);

    // 重定向到文件
    if (freopen("output.txt", "w", stdout) == NULL) {
        perror("freopen failed");
        return 1;
    }

    printf("这行写入文件\n");

    // 恢复标准输出
    dup2(saved_stdout_fd, STDOUT_FILENO);
    close(saved_stdout_fd);

    printf("这行显示在屏幕\n");

    return 0;
}
说明: dup()dup2()是系统调用,用于复制文件描述符。这里只是提一下,后面章节会详细讲。

7.7 知识体系图

下面这张图帮你理清freopen()的核心逻辑:

freopen() 重定向核心逻辑 stdin stdout stderr input.txt (读模式) output.txt (写模式) error.log (追加模式) 关键点 • 调用 freopen() 后,原流自动关闭 • 代码中的 printf/scanf/fprintf 无需修改 • 恢复原始流需使用 dup/dup2 系统调用

7.8 实际项目中的用法

我在做嵌入式设备日志系统时,经常这样用:

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

void init_log(const char *logfile) {
    // 重定向 stdout 到日志文件
    if (freopen(logfile, "a", stdout) == NULL) {
        fprintf(stderr, "无法打开日志文件: %s\n", logfile);
        return;
    }
    // 同时重定向 stderr
    if (freopen(logfile, "a", stderr) == NULL) {
        fprintf(stderr, "无法重定向 stderr\n");
        return;
    }
    // 设置无缓冲,确保日志实时写入
    setbuf(stdout, NULL);
    setbuf(stderr, NULL);
}

int main() {
    init_log("system.log");

    printf("[INFO] 系统启动\n");
    fprintf(stderr, "[ERROR] 传感器异常\n");
    printf("[INFO] 系统关闭\n");

    return 0;
}

这样,所有输出都追加到同一个日志文件,带时间戳的话更好追踪。

个人建议: 在调试阶段,可以用freopen()把输出重定向到文件,避免刷屏。正式发布时,再改回屏幕输出。代码逻辑完全不用动。

7.9 常见错误与避坑

  • 文件打开失败: 检查路径权限。嵌入式环境下,文件系统可能只读。
  • 忘记恢复流: 重定向后,所有输出都去了文件。如果后续代码需要屏幕交互,记得用dup2()恢复。
  • 缓冲区问题: 默认stdout是行缓冲的。重定向到文件后,可能变成全缓冲。用setbuf(stdout, NULL)设为无缓冲。
  • 多线程环境: freopen()不是线程安全的。多线程程序中要加锁。
我曾经犯过的错: 有一次在循环里反复调用freopen(),每次打开同一个文件。结果文件描述符泄漏,程序跑着跑着就崩溃了。记住,每次freopen()都会关闭旧流,但如果你频繁调用,最好检查返回值。

7.10 总结

freopen()是个小巧但实用的函数。它让你在不修改代码的前提下,改变数据流向。无论是日志记录、自动化测试,还是嵌入式调试,它都能派上大用场。

嗯,掌握它,你的C语言工具箱里又多了一件利器。


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