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 字节,看平台)。你不能指定大小,也不能指定模式。
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;
}
16.4 fflush——手动“冲水”
有时候你不想等缓冲区满了再写,想立刻把数据刷到磁盘。这时候就用 fflush:
int fflush(FILE *stream);
传 NULL 表示刷新所有打开的输出流。
我在项目中遇到过这样一个场景:写日志系统时,程序可能随时崩溃。如果日志全攒在缓冲区里,崩溃后啥也看不到。所以我每写一条日志就调一次 fflush:
fprintf(log_fp, "[ERROR] %s\n", error_msg);
fflush(log_fp); // 立即写入磁盘
代价是性能会下降,但换来了可靠性。嗯,这里要注意:fflush 对输入流的行为是未定义的,别乱用。
16.5 缓冲机制流程图
下面这张图展示了数据从应用程序到磁盘的完整路径:
从图中可以看到,fflush 只是把数据从用户态缓冲区推到内核缓冲区。要真正落盘,还得靠 fsync。这个区别我在做嵌入式存储时吃过亏,数据丢了才意识到问题。
16.6 实际应用建议
总结几条我自己的经验:
- 普通文件读写:用默认的全缓冲就行,性能最好
- 日志系统:行缓冲 + 关键位置 fflush,平衡性能和可靠性
- 标准错误输出:保持无缓冲,别改它
- 嵌入式低内存环境:手动指定小缓冲区,或者用无缓冲
- 网络通信:一般用行缓冲或全缓冲,配合 fflush 控制发送时机
核心要点:
- setbuf 简单但功能有限,setvbuf 更灵活
- 三种模式:全缓冲(攒满再写)、行缓冲(遇到换行写)、无缓冲(立即写)
- fflush 强制刷新用户态缓冲区,但不保证落盘
- 缓冲区设置必须在第一次 I/O 之前完成
缓冲机制说白了就是“空间换时间”的经典应用。理解它,你写出来的代码才能既快又稳。