当前位置: 首页 > news >正文

C/C++缓冲区的概念及应用

C/C++缓冲区的概念及应用

  • 缓冲区概念
    • 观察现象
    • 缓冲区的概念
    • 缓冲区刷新策略
    • 回车\r和回车换行\n
    • C语言的缓冲区
    • 文件缓冲区
  • 缓冲区特性的应用:进度条的实现
    • 简易进度条的实现
    • 工程实践版本
    • \r被解释成回车加换行的情况

缓冲区概念

观察现象

首先是执行以下 3 个 C 程序带来的现象:

sleep函数的程序 ( Linux 的需要展开unistd.h),C 程序的printf输出的字符串带\n,消息会立马刷新,且命令提示符([用户名@主机名 目录名]$)会出现在下一行。

例如这个测试程序:

#include<stdio.h>#include<unistd.h>intmain(){printf("Can you see me?\n");sleep(2);return0;}

交互界面:

[Bjarne@VM-8-8-centos cppTest]$makegcc a.c-oa.exe[Bjarne@VM-8-8-centos cppTest]$ ./a.exe Can you see me?# 这里无法体现现象,需要自行测试[Bjarne@VM-8-8-centos cppTest]$

这里会先输出语句 “Can you see me?” ,再暂停 2 秒,之后再输出下一条命令提示符[Bjarne@VM-8-8-centos cppTest],文本和图片均无法展示过程(还可以使用 gif,但这里没做),可自行测试。

而不带\n则会先暂停一下,再刷新消息,且命令提示符会紧随其后。这里再次修改程序:

intmain(){printf("Can you see me?");sleep(2);return0;}

交互界面:

[Bjarne@VM-8-8-centos cppTest]$makegcc a.c-oa.exe[Bjarne@VM-8-8-centos cppTest]$ ./a.exe Can you see me?[Bjarne@VM-8-8-centos cppTest]$# 这里会先暂停2秒,再输出下一条命令提示符,文本无法展示过程

可以看到,不带\n和带\n有明显差别。

若带fflush函数对stdout进行强制刷新,则会先显示printf的字符串,再睡眠。

参考man 3 fflush

#include<stdio.h>intfflush(FILE*stream);

对于输出流stdiofflush通过该流的底层写函数,强制将给定输出或更新流的所有用户空间缓冲数据执行一次写入,即刷新流。

对于输入流stdinfflush丢弃所有已从底层文件获取但尚未被应用程序消费的缓冲数据。流的打开状态不受影响。

如果参数streamNULLfflush将刷新所有已打开的输出流。

\n也是一种刷新流的策略:行刷新。

这里再次修改程序:

#include<stdio.h>#include<unistd.h>intmain(){printf("Can you see me?");fflush(stdout);sleep(2);return0;}

交互界面

[Bjarne@VM-8-8-centos cppTest]$makegcc a.c-oa.exe[Bjarne@VM-8-8-centos cppTest]$ ./a.exe Can you see me?[Bjarne@VM-8-8-centos cppTest]$# 这里会先输出语句,再暂停2秒,再输出下一条命令提示符,文本无法展示过程

对 3 个现象进行分析:

C 语言的函数内的语句是从上往下执行。所以不存在先执行sleep,再执行printf的情况。但不带\n时,会看到sleep先运行的假象,这种情况只能是printf早已经运行,但字符串没有被显示出来。

所以到这里只会 C 语言已经没法解释,需要站在操作系统的视角理解。

缓冲区的概念

根据之前的学习可知,缓冲区的本质就是进程管理的一块内存空间,用于临时存储数据。

之前的现象就是printf数据先拷贝到某个缓冲区里,然后根据是否有\n来决定算是先清空缓冲区还是先sleep,或者使用fflush提前刷新缓冲区。

理解为什么要有缓冲区,这里举一个生活中常见的案例。

假设甲在江苏读大学,乙在浙江读大学,甲和乙是一对好哥们。某天甲在自己城市的电脑商城买了一个仅在自己城市的电脑城出售的键盘(数据),觉得很好用,于是想让好友乙也使用,于是带着键盘骑自行车从江苏一路跑到浙江,将键盘送给乙。

这种甲亲自行动将键盘(数据)带给乙,对甲来说效率是非常低下的。所以甲可以讲键盘委托菜鸟驿站,让菜鸟驿站的工作人员将键盘给乙送去。尽管可能这个菜鸟驿站不靠谱,同样是要骑自行车送键盘,甚至很可能还要等仓库堆满了再送快递(刷新策略),但对甲来说甲只耗费了委托菜鸟驿站的时间,极大提高了甲的效率。

除此之外,甲可能会觉得菜鸟驿站送的太慢,于是催促菜鸟驿站的工作人员(fflush)让他们送快点。于是工作人员也不骑自行车摸鱼了,直接改乘高铁跑过去。

所以缓冲区扮演的就是菜鸟驿站的角色。进程想要通过printf将字符串数据输出到显示屏,可以先将字符串数据保存到缓冲区,缓冲区满了之后(这里没有填满,而是sleep加进程阻塞)再统一将数据给显示屏传输过去。

对 C/C++ 语言,会针对标准输出stdout,Linux 会给程序提供默认的缓冲区。在sleep期间,字符串在输出缓冲区中。

C/C++ 程序在运行时会默认打开三个文件流:stdinstdoutstderr。其中stdout就是输出缓冲区(标准输出流)。同理stdin也有输入缓冲区(标准输入流),输入的数据中也包括\n,在算法竞赛中需要对这个\n进行处理才能正确读取数据。

缓冲区刷新策略

通俗地讲,数据在缓冲区堆积了多少,进程才将数据拷贝出去并清理缓冲区,这个模式叫刷新策略

C/C++ 程序常见的刷新策略有 3 种:

  • 全缓冲
    • 即等缓冲区满了再输出(菜鸟驿站货物堆满了再送快递);
    • 检测到fflush的使用(用户催发货);
    • 程序正常结束(例如菜鸟驿站倒闭了,为防止用户上门找茬,将仅剩的快递送完再散伙)。
  • 行缓冲检测到\n时再输出。其他情况例如缓冲区满、强制刷新、程序结束时可能也会触发行缓冲。
  • 无缓冲立即输出,无需缓冲区。

因为内存和外部设备显示屏的交流速度很慢(指几毫秒,和 CPU 的纳秒级别的数据处理无法比较),所以全缓冲、行缓冲一定程度上提高了整体的效率,否则每输出一个字符都要访问显示屏,传输效率会特别低下。

回车\r和回车换行\n

C 语言的转义运算符\n,即回车换行其实是做了 2 件事:换到下一行和将光标放在下一行的最开头。

老式回车键是想通过形式来告诉用户回车的原理。

C 语言的这 2 个转义字符\r是只回车,\n是 ”回车 + 换行“。

验证只回车\r而设计的倒计时程序:

#include<stdio.h>#include<unistd.h>intmain(){intcnt=9;while(cnt){printf("%d\r",cnt);//输出完成后,光标会回到开头fflush(stdout);//没有fflush不会输出cnt--;sleep(1);}return0;}

第7行的printf,数据暂时被缓冲起来,直到最后一直没显示,且光标回到开头将数据清理掉,或干脆就不输出。解决方法是加fflush刷新stdout。如此便完成了一个倒计时程序。

这些 C 程序向显示器打印的并不是数字,而是字符。将各种数据转换成字符并输出是printf的工作,所以printf格式化控制函数

这就意味着,若之前的程序的cnt初始化为10,则会输出{90,80,...,10}。这是倒计时程序不希望出现的 bug,所以可以用%-2d进行格式控制,-会让输出的字符进行左对齐,不加-则是右对齐。

能显示 2 位数倒计时的程序如下:

#include<stdio.h>#include<unistd.h>intmain(){intcnt=10;while(cnt){printf("%-2d\r",cnt);//输出倒计时fflush(stdout);cnt--;sleep(1);}return0;}

所以将显示器称为字符设备,因为它显示的只是字符,不是整型,也不是浮点数。

从键盘中得到的数据同理也是字符,哪怕是数字,这种读取方式是格式化输入,键盘也被称之为字符设备。

c程序在进行打印时是要将数据打印到显示器上,直接刷新到显示器上时,每次刷新本质都是访问外设的过程,效率太低,所以c语言先暂时将字符串存储在缓冲区。

类比的话是快递小哥,一次将所有的快递给小哥,小哥一次将所有快递递交给用户。强制刷新输出流相当于是给小哥压力,让小哥及时反馈信息。

C语言的缓冲区

这里运行一个程序,来进行一个测试。

#include<stdio.h>#include<string.h>#include<unistd.h>intmain(){printf("C: hello printf\n");fprintf(stdout,"C: hello fprintf\n");fputs("C: hello fputs\n",stdout);fwrite("C: hello fwrite\n",16,1,stdout);constchar*str="system call: hello write\n";write(1,str,strlen(str));fork();return0;}

交互界面:

[Bjarne@VM-8-8-centos cTest]$makegcc a.c-oa.exe-std=c99[Bjarne@VM-8-8-centos cTest]$ ./a.exe C: helloprintfC: hello fprintf C: hello fputs C: hello fwrite system call: hellowrite[Bjarne@VM-8-8-centos cTest]$ ./a.exe>t.txt[Bjarne@VM-8-8-centos cTest]$catt.txt system call: hellowrite# 系统调用最先输出C: helloprintfC: hello fprintf C: hello fputs C: hello fwrite C: helloprintf# 从这里开始,凡是C语言封装的函数都输出了第二次C: hello fprintf C: hello fputs C: hello fwrite[Bjarne@VM-8-8-centos cTest]$

现象解释

一般C库函数写入文件时是全缓冲的,而使用系统调用write写入显示器是行缓冲

printffprintffputsfwrite这些库函数会自带缓冲区,当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲,因为对磁盘上的文件写入时默认是全缓冲。此时放在缓冲区中的数据,就不会被立即刷新(指将缓冲区数据立马送走并将缓冲区清空)。

fork创造进程,待进程退出之后,会统一刷新写入文件当中。但是fork调用时,父子进程的数据会发生写时拷贝(指两个进程指向同一缓冲区,哪怕是拷贝一份完全一样的缓冲区也不影响测试结果),所以当两个进程结束时,两个进程各自将自己的缓冲区刷新,父、子进程谁先结束没有影响。

write没有变化,说明没有所谓的缓冲,而是直接输出到文件(在Linux,显示屏也被看成文件)。

综上

printffwrite等 C 语言的库函数会自带缓冲区,而write系统调用没有带缓冲区。这里所说的缓冲区,都是用户级缓冲区,即提供给用户使用的缓冲区。

为了提升整机性能,OS也会提供相关内核级缓冲区,这里暂时不讨论。

printffwrite是库函数,write是系统调用,库函数在系统调用的 “上层” ,是对系统调用的封装”。但是write没有缓冲区,而printffwrite有,所以缓冲区是二次加上的,又因为是 C 语言,所以这个 “用户级缓冲区” 由 C 标准库提供。

例如printf的大致内容(为了处理可变参数、错误处理和线程安全,肯定会更复杂):

intprintf(constchar*str,...){//声明一段缓冲区,一般是 char buffer[SIZE]; SIZE为某个整数的标识符//因为缓冲区本质是暂时存放一段数据,然后根据刷新策略将数据弄走,//所以只要能在进程地址空间上表示的连续空间都可以作为缓冲区charbuffer[SIZE];//这里处理各种%c、%d等等进行格式化while(*str!='\0'){if(*str=='%'){//格式化处理,例如将int型数据转换成char型//将识别出来的数据拷贝到buffer}}//将处理好的buffer通过系统调用打印到显示屏,Linux是write,Windows和其他操作系统可能是别的intlen=strlen(buffer);write(1,buffer,len);returnlen;//打印输出成功的字符数}

日常用的最多啊的是 C/C++ 提供的用户级别的缓冲区,也可以叫做语言级别的缓冲区,后者是前者的具体实现方式。

文件缓冲区

FILE结构体:typedef struct _IO_FILE FILE;,在/usr/include/stdio.h可看到。

[Bjarne@VM-8-8-centos cTest]$grep-n'typedef struct _IO_FILE FILE'/usr/include/stdio.h48:typedef struct _IO_FILE FILE;# -n指定出现在第几行[Bjarne@VM-8-8-centos cTest]$grep-Rn'struct _IO_FILE {'/usr/include/*# -R表示递归查找/usr/include/libio.h:246:struct _IO_FILE{[Bjarne@VM-8-8-centos cTest]$head-287/usr/include/libio.h|tail-42# 通过hean、tail和管道查看指定部分

通过head -287 /usr/include/libio.h | tail -42,在/usr/include/libio.h可以看到的信息大概是这样:

struct_IO_FILE{int_flags;/* High-order word is _IO_MAGIC; rest is flags. */#define_IO_file_flags_flags//缓冲区相关的管理指针/* The following pointers correspond to the C++ streambuf protocol. *//* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */char*_IO_read_ptr;/* Current read pointer */char*_IO_read_end;/* End of get area. */char*_IO_read_base;/* Start of putback+get area. */char*_IO_write_base;/* Start of put area. */char*_IO_write_ptr;/* Current put pointer. */char*_IO_write_end;/* End of put area. */char*_IO_buf_base;/* Start of reserve area. */char*_IO_buf_end;/* End of reserve area. *//* The following fields are used to support backing up and undo. */char*_IO_save_base;/* Pointer to start of non-current get area. */char*_IO_backup_base;/* Pointer to first valid character of backup area */char*_IO_save_end;/* Pointer to end of non-current get area. */struct_IO_marker*_markers;struct_IO_FILE*_chain;//管理方式还是通过链表进行管理int_fileno;//封装的文件描述符// 省略部分信息};

因此进程打开的文件除了具体的文件 inode文件的打开方法集,还有文件指定的缓冲区

缓冲区特性的应用:进度条的实现

进度条的应用通常是在下载、登录时使用。

进度条样式效果示例:

[### ][xx%][\][==>][xx%][\]

这里通过在第一个[]内填充符号的方式实现进度条,通过修改第 2 个[]内的数字和第 3 个[]内的字符,让用户 ”感受到进度条确实是在运行“ 。

所以进度条理论上只会降低程序运行的效率,一般只有在特别耗时的情况才会通过进度条随时监视进度。但很多情况为了 “安抚用户的情绪,不得不设计。

简易进度条的实现

这里的简易进度条要求:

  1. 限制一段范围(一般是100 个字符,可自定义)。
  2. 在这段范围中推进自定义的符号(可以是#,也可以是==>,还可以个性化例如^v^)。
  3. 在末尾携带进度百分比。
  4. 给出旋转光标给用户反馈进度条一直在推进。

进度条样式效果:

[###### ][xx%][\][=====>][xx%][\][^v^^v^][xx%][\]

实现这个样式,可以用printf("[%s][%d\%][%c]\r",...);输出,因为是\r,所以printf的下一句需要是fflush(stdout),否则进程不会进行行刷新,输出的数据一直在缓冲区不会打印到显示屏。之后就是对进度条的进度操作,一般是下载的数据占整体的比例,若是图形化界面会有更直观的表示。

sleep在 Linux 中是以秒为单位,这对进度条来说太慢,需要使用函数usleep

参考man 3 usleep

#include<unistd.h>intusleep(useconds_tusec);

usleep以微秒为单位让程序挂起(或者说睡眠)一段时间,1000微秒等于1毫秒。

这里提供三个版本的进度条:

  1. 简单原理版本。
  2. 实际工程实践版本。
  3. C 语言扩展–带颜色。

简单原理版本(不进行封装和声明、定义分离,仅供参考):

#include<stdio.h>#include<unistd.h>#include<string.h>constchar*rotat="|/-\\";//进度条旋转表示程序正常推进intmain(){intrate=0;//进度百分比charbar[101]={'\0'};//进度条本体intnum=strlen(rotat);while(rate<=100){//%-100s 是为了打印长度为100的进度条,-是左对齐。printf("[%-100s][%d%%][%c]\r",bar,rate,rotat[rate%num]);fflush(stdout);usleep(1000*50);//50毫秒为单位,可修改bar[rate++]='#';}printf("\n");return0;}

工程实践版本

任何进度条一定是和某种任务关联。例如下载、压缩、解压、删除文件等。

所以整理进度条函数的逻辑:

  1. 给进度rate,进度条函数会根据进度rate刷新进度条,之后在调用进度条函数的地方进行任务记录、任务进行。
  2. 先执行任务时,再打印进度条时,循环可设置进度为100 % 100\%100%时停止。
  3. 可能会出现任务暂停在某个进度的情况(例如网络卡顿),这种情况可以通过操控进度的值让最后的光标进行旋转,“迷惑用户
  4. 这里采用 101 个长度的char数组模拟进度条,floor(rate*100)作为进度条的填充,则有效进度条范围是[0,99],当进度达到99 % 99\%99%时理应填满整个进度条。
  5. 进度有可能从 0.99 跳到 1,或者 1.1 等,所以当进度大于100 % 100\%100%时直接输出100 % 100\%100%。因此需要特殊处理。
  6. 可选择自定义的进度条,例如这里的模拟实现采用==>

工程实践版本的大致参考程序如下:

#include<stdio.h>#include<string.h>#include<unistd.h>typedefvoid(*callshow_v)(double);// 进度条展示版本voidprocess(doublerate){constchar*rotat="|/-\\";// 进度条旋转表示程序正常推进staticcharbar[101]={'\0'};// 进度条本体staticsize_t cnt=0;// 控制进度条的旋转,无符号数无需担心溢出intrlen=strlen(rotat);if(rate<100.0){// 决定采用浮点数时,要考虑不同下载速度的情况if(rate<99.0){bar[(int)rate]='=';bar[(int)rate+1]='>';inttmp=(int)rate-1;while(tmp>=0&&bar[tmp]!='='){// 一次完成几个百分比时向前填充bar[tmp]='=';--tmp;}}else{bar[99]='=';bar[100]='\0';// 第101处填充`\0`}printf("[%-100s][%5.1lf%%][%c]\r",bar,rate,rotat[cnt%rlen]);++cnt;fflush(stdout);}else{// 任务完成的情况rate=100.0;bar[99]='=';bar[100]='\0';inttmp=98;// 假设用的是更先进的通信技术例如4G网,1秒钟流量7MBwhile(tmp>=0&&bar[tmp]!='='){// 向前填充bar[tmp]='=';--tmp;}printf("[%-100s][%5.1lf%%][%c]\n",bar,rate,rotat[0]);memset(bar,'\0',sizeof(bar));// 手动清空迎接下个任务}}voiddownload(callshow_v cs){intaim=1024*1024*10;// 10MB 待下载数据intv=1024;// 下载速度为1KB(原始人的网络)intret=1024*1024*9;// 下载进度记录,初始90%,可设为0// int cnt = 500;while(ret<aim){// 达到目标时应该停止,因为这里是先下载再记录ret+=v;// 正在下载usleep(1000*10);doublerate=ret*100.0/aim;// while(cnt&&rate>=66.0){//模拟网络卡顿的情况// ret-=v;// rate = ret*100.0/aim;// --cnt;// }cs(rate);// 刷新进度条}}intmain(){download(process);//上传函数名作为参数// download(process); //模拟多次下载// download(process);return0;}

C语言可以使用格式化语句输出简单的彩色代码片段。例如,蓝底红字:

printf("[\033[31;44m%-100s\033[0m][%5.1lf%%][%c]\r",bar,rate,rotat[cnt%rlen]);

具体参考【C语言】C语言基础——printf带颜格式化输出(Linux)_printf格式化输出文本颜色-CSDN博客。

\r被解释成回车加换行的情况

部分拓展屏的终端驱动程序或 xshell 在不同的拓展屏可能会将\r解释成回车加换行,这时可通过循环输出\b即一直输出退格直到回退到行首。这个方案适用所有终端显示屏。

voidprocess(doublerate){//保留原代码不变if(rate<100.0){//决定采用浮点数时,要考虑不同下载速度的情况//保留原代码不变intoutlen=printf("[%-100s][%5.1lf%%][%c]",bar,rate,rotat[cnt%rlen]);for(inti=0;i<outlen;i++)//循环输出\b退格printf("\b");++cnt;fflush(stdout);}else{//任务完成的情况//保留原代码不变}}

也可以使用 ANSI 专业序列\033[2K\r。这个序列的功能是检测到这个序列时,终端设备会将光标移到开头。这个方案相对于输出等量的\b能显著提升性能,但要求目标终端支持 ANSI 。

voidprocess(doublerate){//保留原代码不变if(rate<100.0){//保留原代码不变//检测到\033[2K\r,将光标移动到开头,再输出后续字符printf("\033[2K\r[%-100s][%5.1lf%%][%c]",bar,rate,rotat[cnt%rlen]);++cnt;fflush(stdout);}else{//保留原代码不变printf("\033[2K\r[%-100s][%5.1lf%%][%c]\n",bar,rate,rotat[0]);memset(bar,'\0',sizeof(bar));//手动清空迎接下个任务}}

除了进度条,标准库stdio.h的部分库函数例如printffputsfopen等可以自己模拟实现。其中结构体FILE内部存在缓冲区,这个缓冲区可以是栈上开辟,也可以用malloc库函数申请,根据自己的需求还可以添加文件描述符fd接收系统调用open的返回值、flag接收文件的打开模式。

例如简易版:

#pragmaonce#defineSIZE4096#defineFLUSH_NONE1#defineFLUSH_LINE(1<<1)#defineFLUSH_ALL(1<<2)typedefstruct_myFILE{intfileno;intflag;charbuffer[SIZE];intend;}myFILE;

其他的fopenfwritefflushfclose等都可以模拟实现。这里暂时省略。

http://www.jsqmd.com/news/1217412/

相关文章:

  • Ubuntu 22.04安装NVIDIA驱动与CUDA全攻略
  • #大学生毕业设计
  • Java Handler设计模式解析与实战应用
  • Flask消息闪现机制详解与应用实践
  • Azure分布式数据库磁盘选型:YugabyteDB存储性能优化指南
  • MyBatisPlus与SpringBoot整合开发实战指南
  • TI OMAP SCM模块实战:寄存器配置、低功耗与调试指南
  • 救命!2026年AI写作辅助网站排行榜,选题卡壳的毕业生直接抄作业
  • Python循环结构详解:从基础到高级应用
  • 如何让城通网盘下载速度提升10倍?这可能是最实用的免费解决方案
  • JSF企业级登录系统实战:安全认证与性能优化
  • 2026年企业AI办公工具深度横评:飞书 aily 等5款主流产品选型指南
  • 相关性分析实战:协方差、Pearson与Spearman的工程化选择
  • I2C OLED模块开发指南:从硬件连接到高级应用
  • Unity自行车物理插件:简化模型实现真实骑行手感与参数调校指南
  • Cookie技术解析:工作原理、安全攻防与最佳实践
  • Python爬虫入门:从HTTP请求到数据存储的完整指南
  • 从Notebook到生产环境的ML系统性交付实战
  • Visual Studio AppBuilder跨平台开发实战指南
  • 2026陕西妇科洗液OEM厂家盘点:4家主流企业信息汇总
  • Android Handler机制详解:原理、优化与线程通信
  • Kimi-K3前端代码竞技场:性能优化与框架实战指南
  • AM62L EMIF时序寄存器配置实战:从DDR原理到嵌入式内存优化
  • 耐震时程分析:从基本原理到优化代码—基于GM_Tools的完整解决方案
  • Android多线程编程:Thread与Handler的协同机制
  • AIGC人找Logo灵感,试试这几招
  • FastAPI+Celery+pg-vector构建LLM SaaS生产级文件处理架构
  • Java 只有引用,Go 的指针 * 到底是什么?内存模型一次讲透
  • Android应用签名机制详解:从原理到实践
  • 书桌台灯什么牌子好?居家实用护眼台灯分享,日常陪伴更舒心