一、标准IO的缓冲区
1.数据流动流程图
程序
↓
FILE 指针
↓
FILE 结构体 ──→ struct _IO_FILE {
int _flags; // 缓冲区类型标志(如全缓冲、行缓冲)
...其他字段
char* _IO_buf_base; // 缓冲区起始地址
char* _IO_buf_end; // 缓冲区结束地址
};
↓
缓冲区
↓ (满足刷新条件:缓冲区满/程序正常结束等等)
硬件
2.缓冲区的概念
定义:标准 I/O 中的缓冲区是C语言标准库为优化文件读写效率设计的内存机制,本质上就是一段由FILE 结构体管理的内存空间;
• 作用:通过临时存储数据、批量处理,可以减少硬件IO(如磁盘、终端)的交互次数,提升程序效率;
• 缓冲策略:标准 I/O 中设计了行缓冲、全缓冲、无缓冲三种策略,来适配不同的场景,也支持手动修改;
• 行缓冲:默认用于终端设备(如stdout、stdin),Linux系统中行缓冲的大小为1024(1KB);
• 全缓冲:默认用于文件(如 .txt),Linux系统中全缓冲大小通常是4096(4KB);
• 无缓冲:不使用临时内存暂存,数据直接写入硬件(如stderr);无缓冲区的大小为0;
二、缓冲区的刷新机制
1.行缓冲刷新时机
• 遇到换行符"\n"会刷新缓冲区
bash
#include <stdio.h>
#include <unistd.h> // sleep()
int main() {
printf("Hello, ");
sleep(3); // 等3秒,你会发现这句话没立刻打印出来
printf("World!\n"); // 遇到\n,缓冲区被刷新,内容一起输出
sleep(3);
return 0;
}
• 程序正常结束会刷新缓冲区
cs
#include <stdio.h>
#include <unistd.h>
int main() {
printf("程序结束前,这句话会被刷新出来");
sleep(5); // 睡5秒,屏幕上不会立刻看到这句话
return 0; // main函数return,程序正常结束,缓冲区被强制刷新
}
• 当文件关闭时会刷新缓冲区
cs
#include <stdio.h>
int main() {
FILE* fp = fopen("test.txt", "w");
if (fp == NULL) {
perror("fopen");
return 1;
}
fputs("这行数据先写到缓冲区,还没真正进文件", fp);
// 此时如果用编辑器打开 test.txt,内容是空的
fclose(fp); // 文件关闭,缓冲区被刷新,数据写入磁盘
// 现在再打开 test.txt,就能看到内容了
return 0;
}
• 在输入和输出发生切换时会刷新缓冲区
cs
#include <stdio.h>
int main() {
printf("请输入一个数字:"); // 先输出,没加\n
int n;
scanf("%d", &n); // 切换到输入时,会自动刷新输出缓冲区
printf("你输入的数字是:%d\n", n);
return 0;
}
• 缓冲区写满(未到换行符)会刷新缓冲区
cs
#include <stdio.h>
int main() {
// 对文件流默认是全缓冲,缓冲区一般是4KB或8KB
FILE* fp = fopen("big_test.txt", "w");
if (fp == NULL) {
perror("fopen");
return 1;
}
// 写一个很大的字符串,超过缓冲区大小
char big_buf[1024*8];
for (int i = 0; i < sizeof(big_buf); i++) {
big_buf[i] = 'A';
}
big_buf[sizeof(big_buf)-1] = '\0';
fputs(big_buf, fp);
// 此时缓冲区已满,会自动刷新到文件,不用等fclose
getchar(); // 暂停程序,此时打开文件已经能看到内容了
fclose(fp);
return 0;
}
• 主动调用fflush函数会强制刷新缓冲区
| 项目 | 内容说明 |
|---|---|
| 所需头文件 | #include <stdio.h> |
| 函数原型 | int fflush(FILE *stream); |
| 功能 | 将指定流 (stream) 缓冲区中尚未写入的内容,强制写入到对应的输出设备(如文件、终端等)中 |
| 参数 | stream:用于指定需要刷新缓冲区的流 |
| 返回值 - 成功 | 返回 0,表示成功刷新了流缓冲区 |
| 返回值 - 失败 | 返回 EOF,并将 errno 设置为具体的错误类型 |
cs
#include <stdio.h>
#include <unistd.h>
int main() {
printf("我要立刻被刷新!");
fflush(stdout); // 强制刷新标准输出缓冲区
sleep(3); // 此时你会立刻看到文字,然后等3秒
printf(" 结束啦\n");
return 0;
}
2.全缓冲刷新机制
• 程序正常结束会刷新缓冲区
cs
#include <stdio.h>
int main() {
FILE* fp = fopen("full_buffer_test.txt", "w");
if (!fp) {
perror("fopen");
return 1;
}
// 写入数据,此时数据只在缓冲区,没写入文件
fputs("这行数据在缓冲区,未写入磁盘\n", fp);
printf("数据已写入缓冲区,但文件还没真正写入\n");
// 暂停,你可以此时打开文件查看,内容是空的
getchar();
// 调用fclose,会自动刷新缓冲区,数据写入文件
fclose(fp);
printf("文件已关闭,缓冲区数据已刷新到磁盘\n");
return 0;
}
• 当文件关闭时会刷新缓冲区
上同
• 在输入和输出发生切换时会刷新缓冲区
cs
#include <stdio.h>
int main() {
FILE* fp = fopen("io_switch_test.txt", "w+");
if (!fp) {
perror("fopen");
return 1;
}
fputs("先写入缓冲区的数据", fp);
// 此时数据在缓冲区,未写入文件
// 切换到输入操作,会触发缓冲区刷新
rewind(fp); // 移动文件指针到开头
char buf[100];
fgets(buf, sizeof(buf), fp); // 输入操作,强制刷新之前的输出缓冲区
printf("从文件读取到的内容:%s\n", buf);
fclose(fp);
return 0;
}
• 缓冲区写满(未到换行符)会刷新缓冲区
cs
#include <stdio.h>
#include <string.h>
int main() {
FILE* fp = fopen("full_write_test.txt", "w");
if (!fp) {
perror("fopen");
return 1;
}
// 构造一个超过默认缓冲区大小的数据(比如8KB)
char big_data[1024 * 8];
memset(big_data, 'A', sizeof(big_data) - 1);
big_data[sizeof(big_data) - 1] = '\0';
printf("开始写入大数据,超过缓冲区大小会自动刷新\n");
fputs(big_data, fp);
// 此时数据已经因为缓冲区写满被刷新到文件了
getchar(); // 暂停,此时打开文件能看到内容
fclose(fp);
return 0;
}
• 主动调用fflush函数会强制刷新缓冲区;
cs
#include <stdio.h>
int main() {
FILE* fp = fopen("fflush_test.txt", "w");
if (!fp) {
perror("fopen");
return 1;
}
fputs("还在缓冲区的数据", fp);
printf("数据在缓冲区,未写入文件\n");
// 主动调用fflush强制刷新
fflush(fp);
printf("fflush调用后,数据已写入文件\n");
getchar(); // 暂停,此时打开文件能看到内容
fclose(fp);
return 0;
}
三、手动设置缓冲区
1.为什么需要手动设置缓冲?
• 覆盖默认缓冲策略:不同系统/语言对IO流的默认缓冲设置不同(例如C语言中stdout默认行缓 冲,stderr默认无缓冲),手动设置可按需调整;
• 性能与实时性平衡:
• 行缓冲:适合交互式输出(如命令行程序),每行输出后自动刷新;
• 全缓冲:适合大量数据批量写入(如日志文件),减少IO次数;
• 无缓冲:适合需要立即显示的关键信息(如错误日志);
标准 I/O(C 标准库提供的 stdio.h 系列函数,如 printf/fread/fwrite)通过用户态缓冲区,减少直接调用系统调用(如 read/write)的次数。
优点:
1.大幅减少系统调用次数,提升性能
系统调用会陷入内核态,开销较大。缓冲机制将多次小 I/O 合并为一次批量操作,减少上下文切换次数,显著提升效率。
2.适配不同设备的访问特性
例如磁盘设备适合批量读写,终端设备适合按行交互,缓冲模式可以根据设备特性优化行为。
3.简化应用层逻辑
开发者无需手动管理 I/O 批次,标准库自动处理缓冲刷新,降低编程复杂度。
缺点:
1.数据不是实时写入设备,存在延迟风险
数据暂存在用户态缓冲区,若程序意外崩溃(如 SIGSEGV),缓冲区数据未刷新,会导致数据丢失。
2.调试和异常处理复杂度提升
例如 printf 未加 \n 时,程序崩溃前不会输出内容,给调试带来干扰;需额外调用 fflush 或设置无缓冲模式。
占用额外内存空间
3.每个流都需要维护缓冲区,增加了进程的内存开销。
可能造成数据不一致
若多个进程 / 线程同时操作同一文件,缓冲数据未刷新时,会出现视图不一致的问题。
• 特殊场景需求:例如网络编程中避免数据堆积,或调试时强制输出中间结果; • 手动设置缓冲典型应用场景:
• 日志系统:根据日志级别设置缓冲(错误日志无缓冲,普通日志全缓冲);
• 交互式工具:命令行程序中确保用户输入后立即响应;
• 嵌入式系统:受限内存环境下自定义缓冲区大小;
2.setbuf函数
| 项目 | 内容说明 |
|---|---|
| 所需头文件 | #include <stdio.h> |
| 函数原型 | void setbuf(FILE *stream, char *buf); |
| 功能 | 设置指定文件流的缓冲区 |
参数 -stream |
指向FILE对象的指针,标识要设置缓冲区的文件流 |
参数 -buf |
指向用户提供的缓冲区的指针;若提供缓冲区,其大小必须至少为BUFSIZ字节;若为NULL,则设置为无缓冲模式 |
| 返回值 | 无 |
- 为文件流设置自定义缓冲区
cs
#include <stdio.h>
#include <unistd.h>
int main() {
// 定义一个大小为 BUFSIZ 的缓冲区(stdio.h 中定义的默认缓冲区大小)
char my_buf[BUFSIZ];
FILE* fp = fopen("setbuf_test.txt", "w");
if (fp == NULL) {
perror("fopen");
return 1;
}
// 为文件流 fp 设置自定义缓冲区
setbuf(fp, my_buf);
// 写入数据,此时数据会先写入我们定义的 my_buf 中
fputs("这行数据会先进入我们自定义的缓冲区\n", fp);
printf("数据已写入自定义缓冲区,未写入文件\n");
// 暂停 3 秒,你可以此时打开文件查看,内容是空的
sleep(3);
// 关闭文件,会自动刷新缓冲区,数据写入文件
fclose(fp);
printf("文件已关闭,缓冲区数据已刷新到磁盘\n");
return 0;
}
- 设置无缓冲模式(数据直接写入设备)
cs
#include <stdio.h>
#include <unistd.h>
int main() {
// 为标准输出设置无缓冲模式,数据直接输出到终端
setbuf(stdout, NULL);
printf("无缓冲模式:");
sleep(3); // 即使没有 \n,也会立刻打印文字,不会等待缓冲区刷新
printf("数据直接输出,没有缓冲延迟!\n");
return 0;
}
3.结合 setbuf 与 fflush 强制刷新
cs
#include <stdio.h>
#include <unistd.h>
int main() {
char my_buf[BUFSIZ];
FILE* fp = fopen("setbuf_fflush.txt", "w");
if (fp == NULL) {
perror("fopen");
return 1;
}
setbuf(fp, my_buf);
fputs("数据在自定义缓冲区,未写入文件", fp);
printf("数据在缓冲区,等待刷新...\n");
sleep(3);
// 主动调用 fflush 强制刷新自定义缓冲区
fflush(fp);
printf("已调用 fflush,数据写入文件\n");
fclose(fp);
return 0;
}
💡 关键说明:
setbuf 必须在流打开后、任何读写操作前调用,否则行为未定义。
buf 参数必须是全局变量或静态变量,不能是局部栈变量,否则函数结束时缓冲区被释放会导致崩溃。
setbuf(fp, NULL) 等价于关闭该流的所有缓冲,每次写入都会直接刷新到设备。
• setbuf函数允许两种方式的缓冲:
• 全缓冲:如果buf指向一个大小大于0的缓冲区,那么IO操作将使用全缓冲模式;
• 无缓冲:如果buf为NULL,或者是大小为0,那么IO操作将变为无缓冲模式;
• setbuf函数注意事项:
• 调用setbuf函数应该在打开文件流或者是任何IO操作之前,否则可能不会生效;
• 如果要使用自定义的缓冲区,确保缓冲区足够大,以避免缓冲区溢出;
• 当使用自定义缓冲区时,不要在buf指向的缓冲区被释放后再进行IO操作,以免导致未定义的行为;
2.sleep函数
| 项目 | 内容说明 |
|---|---|
| 所需头文件 | #include <unistd.h> |
| 函数原型 | unsigned int sleep(unsigned int seconds); |
| 功能 | 用于让程序暂停执行指定的时间 |
| 参数 | seconds:指定程序暂停执行的秒数 |
| 返回值 - 成功 | 返回 0 |
| 返回值 - 失败 | 若程序被信号中断,返回未休眠完成的剩余秒数 |
1. 基础用法:程序暂停指定秒数
cs
#include <stdio.h>
#include <unistd.h>
int main() {
printf("程序开始执行...\n");
// 让程序暂停 3 秒
printf("暂停 3 秒...\n");
sleep(3);
printf("3 秒已过,程序继续执行!\n");
return 0;
}
运行效果:先打印前两行,等待 3 秒后再打印最后一行。
2. 处理信号中断的情况
cs
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
// 信号处理函数:当收到 SIGINT (Ctrl+C) 时触发
void sig_handler(int sig) {
printf("\n收到信号 %d,程序被中断!\n", sig);
}
int main() {
unsigned int remain;
signal(SIGINT, sig_handler); // 注册信号处理函数
printf("程序将暂停 10 秒,按 Ctrl+C 可中断\n");
remain = sleep(10);
if (remain > 0) {
printf("sleep 被中断,剩余未休眠的秒数:%u\n", remain);
} else {
printf("sleep 完整执行完毕\n");
}
return 0;
}
运行效果:
不中断:等待 10 秒后,打印 sleep 完整执行完毕
中途按 Ctrl+C:程序会立即打印剩余秒数(比如休眠了 3 秒被打断,返回值是 7)
3. 配合 printf 缓冲区的使用(经典场景)
cs
#include <stdio.h>
#include <unistd.h>
int main() {
printf("加载中");
// 不刷新缓冲区,先休眠3秒
sleep(3);
printf("...完成!\n"); // 遇到换行符,才会把前面的内容一起输出
return 0;
}
运行效果:先等待 3 秒,再一次性打印 加载中...完成!。
如果想让文字实时显示,需要主动刷新缓冲区:
cs
#include <stdio.h>
#include <unistd.h>
int main() {
printf("加载中");
fflush(stdout); // 强制刷新标准输出缓冲区
sleep(3);
printf("...完成!\n");
return 0;
}
此时会立刻显示 加载中,等待 3 秒后再补充 ...完成!。
💡 补充说明:
sleep 是秒级休眠,精度较低。如果需要毫秒级 / 微秒级控制,可以使用 usleep()(微秒)或 nanosleep()(纳秒)。
sleep 会让进程进入阻塞状态,不占用 CPU 资源,适合做延时等待场景。
3.setvbuf函数
| 项目 | 内容说明 |
|---|---|
| 所需头文件 | #include <stdio.h> |
| 函数原型 | int setvbuf(FILE *stream, char *buf, int mode, size_t size); |
| 功能 | 灵活控制文件流的缓冲行为,可指定缓冲模式和缓冲区大小;必须在流执行任何读写操作前调用 |
参数 -stream |
指向FILE对象的指针,标识要设置缓冲区的文件流 |
参数 -buf |
用户提供的缓冲区指针;若为NULL,函数会自动分配一个大小为size的缓冲区 |
参数 -mode |
缓冲模式:- _IOFBF:全缓冲- _IOLBF:行缓冲- _IONBF:无缓冲(此时size参数被忽略) |
参数 -size |
缓冲区的大小(字节) |
| 返回值 - 成功 | 返回 0 |
| 返回值 - 失败 | 返回非 0 值 |
1. 为文件流设置自定义全缓冲
cs
#include <stdio.h>
#include <unistd.h>
int main() {
// 自定义一个 1024 字节的缓冲区
char my_buf[1024];
FILE* fp = fopen("setvbuf_full.txt", "w");
if (!fp) {
perror("fopen");
return 1;
}
// 设置为全缓冲模式,使用自定义缓冲区
if (setvbuf(fp, my_buf, _IOFBF, sizeof(my_buf)) != 0) {
perror("setvbuf");
fclose(fp);
return 1;
}
fputs("数据写入自定义全缓冲中,未写入文件\n", fp);
printf("数据在缓冲区,等待刷新...\n");
sleep(3); // 暂停3秒,此时文件内容为空
fclose(fp); // 关闭文件,缓冲区刷新到文件
printf("文件已关闭,数据写入完成\n");
return 0;
}
2. 为标准输出设置行缓冲
cs
#include <stdio.h>
#include <unistd.h>
int main() {
// 为 stdout 设置行缓冲模式,缓冲区大小 1024 字节
char line_buf[1024];
if (setvbuf(stdout, line_buf, _IOLBF, sizeof(line_buf)) != 0) {
perror("setvbuf");
return 1;
}
printf("这行文字遇到换行符");
sleep(3);
printf("才会被刷新出来\n"); // 遇到\n,行缓冲刷新
sleep(3);
return 0;
}
3. 设置无缓冲模式(数据直接输出)
cs
#include <stdio.h>
#include <unistd.h>
int main() {
// 为 stdout 设置无缓冲模式,size 会被忽略
if (setvbuf(stdout, NULL, _IONBF, 0) != 0) {
perror("setvbuf");
return 1;
}
printf("无缓冲模式:");
sleep(3); // 即使没有换行,也会立刻打印文字
printf("文字直接输出,无缓冲延迟!\n");
return 0;
}
💡 关键注意事项:
调用时机:必须在fopen之后、任何printf/fputs等读写操作之前调用setvbuf,否则行为未定义。
缓冲区生命周期:如果使用自定义缓冲区,它的生命周期必须长于文件流的生命周期,不能是局部栈变量(否则函数结束时缓冲区被释放,会导致程序崩溃)。
无缓冲模式:设置_IONBF时,size参数会被忽略,数据每次都会直接写入设备。
4.setbuf 与 setvbuf 的异同点对比
两者都是 C 标准库中用于设置文件流缓冲区的函数,核心目标都是控制流的缓冲行为,但在功能灵活性、参数设计上有明显差异。
✅ 相同点
核心作用一致:都用于修改文件流(FILE*)的缓冲模式,包括设置自定义缓冲区、关闭缓冲、修改缓冲大小。
调用时机要求相同:必须在流打开后、任何读写操作(如printf/fread)之前调用,否则行为未定义。
头文件依赖一致:都需要包含 <stdio.h>。
都支持无缓冲模式:都可以将流设置为无缓冲模式,数据直接写入设备。
❌ 不同点
| 对比维度 | setbuf |
setvbuf |
|---|---|---|
| 函数原型 | void setbuf(FILE *stream, char *buf); |
int setvbuf(FILE *stream, char *buf, int mode, size_t size); |
| 功能灵活性 | 仅支持两种模式:1. 全缓冲(传入buf,大小固定为BUFSIZ)2. 无缓冲(传入NULL) |
支持完整的缓冲控制:1. 可指定缓冲模式(全缓冲 / 行缓冲 / 无缓冲)2. 可自定义缓冲区大小3. 可让系统自动分配缓冲区 |
| 缓冲模式选择 | 无法指定行缓冲模式,只能用默认模式 | 可通过mode参数选择:_IOFBF:全缓冲_IOLBF:行缓冲_IONBF:无缓冲 |
| 缓冲区大小 | 必须使用BUFSIZ(stdio.h 定义的固定大小) |
可通过size参数自定义任意大小 |
| 返回值 | 无返回值,无法直接判断调用是否成功 | 返回0表示成功,非 0 表示失败,可判断错误 |
| 可移植性 | 是 C 标准的一部分,但功能有限 | 也是 C 标准的一部分,是setbuf的增强版,更推荐使用 |