16、缓冲机制:setbuf函数、setvbuf函数、全缓冲/行缓冲/无缓冲、fflush刷新缓冲区

说到文件操作,有个东西你绕不开——缓冲区

很多初学者写代码,fwrite 完数据,断电重启,发现文件里啥也没有。嗯,这就是缓冲区在“捣鬼”。

我个人习惯把缓冲区理解成一个“蓄水池”。你往文件里写数据,就像往水池里倒水。如果每次倒一杯水就立刻灌进管道,效率太低。所以系统先攒着,攒到一定量再一次性冲出去。这个“攒”的过程,就是缓冲。

16.1 三种缓冲模式

C 标准库定义了三种缓冲模式,说白了就是三种“攒水策略”:

模式 宏定义 行为
全缓冲 _IOFBF 缓冲区填满后才进行 I/O 操作
行缓冲 _IOLBF 遇到换行符 '\n' 时刷新缓冲区
无缓冲 _IONBF 每次 I/O 都直接操作,不缓存

默认情况下:

  • 标准输出 stdout 是行缓冲(终端交互场景)
  • 普通磁盘文件是全缓冲
  • 标准错误 stderr 是无缓冲(为了及时输出错误信息)

为什么会这样设计?你想想看,如果 stderr 也用全缓冲,程序崩溃时错误信息可能还憋在缓冲区里没输出,调试起来得多痛苦。

16.2 setbuf 函数——简单粗暴

setbuf 是最简单的设置缓冲区的函数。原型长这样:

void setbuf(FILE *stream, char *buf);

用法就两种:

  • 传一个自定义缓冲区:setbuf(fp, mybuf); —— 启用缓冲
  • 传 NULL:setbuf(fp, NULL); —— 关闭缓冲

注意,setbuf 的缓冲区大小是固定的 BUFSIZ(通常 512 字节或 4096 字节,看平台)。你不能指定大小,也不能指定模式。

⚠ 我曾经踩过的坑: setbuf 必须在文件打开后、任何 I/O 操作之前调用。否则行为是未定义的。而且 buf 指向的内存必须保证在文件关闭前一直有效,不能是局部变量。

16.3 setvbuf 函数——精细控制

如果你想要更灵活的控制,用 setvbuf。我个人更推荐这个,因为它参数更清晰:

int setvbuf(FILE *stream, char *buf, int mode, size_t size);

参数说明:

  • stream:文件指针
  • buf:自定义缓冲区,传 NULL 则系统自动分配
  • mode:_IOFBF / _IOLBF / _IONBF 之一
  • size:缓冲区大小(字节)

来看个实际例子:

#include <stdio.h>

int main() {
    FILE *fp = fopen("data.txt", "w");
    if (!fp) {
        perror("fopen");
        return 1;
    }

    // 设置 4KB 的全缓冲
    char buf[4096];
    if (setvbuf(fp, buf, _IOFBF, sizeof(buf)) != 0) {
        fprintf(stderr, "setvbuf failed\n");
    }

    fprintf(fp, "Hello, buffer!\n");
    // 此时数据还在缓冲区里,没写到磁盘

    fclose(fp);  // 关闭时会自动刷新
    return 0;
}
💡 我的建议: 如果你不确定缓冲区该设多大,传 NULL 让系统自己分配。系统会根据文件类型和平台选择最优大小。我一般只在嵌入式场景下才手动指定缓冲区,因为内存太金贵了。

16.4 fflush——手动“冲水”

有时候你不想等缓冲区满了再写,想立刻把数据刷到磁盘。这时候就用 fflush:

int fflush(FILE *stream);

传 NULL 表示刷新所有打开的输出流。

我在项目中遇到过这样一个场景:写日志系统时,程序可能随时崩溃。如果日志全攒在缓冲区里,崩溃后啥也看不到。所以我每写一条日志就调一次 fflush:

fprintf(log_fp, "[ERROR] %s\n", error_msg);
fflush(log_fp);  // 立即写入磁盘

代价是性能会下降,但换来了可靠性。嗯,这里要注意:fflush 对输入流的行为是未定义的,别乱用。

⚠ 注意: fflush 只保证数据从用户态缓冲区刷到内核态缓冲区,不保证数据已经落盘。如果你需要确保数据真正写到磁盘上(比如写数据库日志),还得用 fsync 或 fdatasync。这是很多新手容易忽略的点。

16.5 缓冲机制流程图

下面这张图展示了数据从应用程序到磁盘的完整路径:

C 语言缓冲机制数据流向图 应用程序 fwrite / fprintf 用户态缓冲区(setbuf / setvbuf) 全缓冲 / 行缓冲 / 无缓冲 _IOFBF 缓冲区满才刷 _IOLBF 遇到 '\n' 就刷 _IONBF 每次直接写 内核缓冲区(系统层) fsync / fdatasync 磁盘 / 文件 fflush 强制刷出

从图中可以看到,fflush 只是把数据从用户态缓冲区推到内核缓冲区。要真正落盘,还得靠 fsync。这个区别我在做嵌入式存储时吃过亏,数据丢了才意识到问题。

16.6 实际应用建议

总结几条我自己的经验:

  • 普通文件读写:用默认的全缓冲就行,性能最好
  • 日志系统:行缓冲 + 关键位置 fflush,平衡性能和可靠性
  • 标准错误输出:保持无缓冲,别改它
  • 嵌入式低内存环境:手动指定小缓冲区,或者用无缓冲
  • 网络通信:一般用行缓冲或全缓冲,配合 fflush 控制发送时机

核心要点:

  • setbuf 简单但功能有限,setvbuf 更灵活
  • 三种模式:全缓冲(攒满再写)、行缓冲(遇到换行写)、无缓冲(立即写)
  • fflush 强制刷新用户态缓冲区,但不保证落盘
  • 缓冲区设置必须在第一次 I/O 之前完成

缓冲机制说白了就是“空间换时间”的经典应用。理解它,你写出来的代码才能既快又稳。


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