目录
[1. 回车换行](#1. 回车换行)
[2. 缓冲区现象](#2. 缓冲区现象)
[3. 强制刷新缓冲区](#3. 强制刷新缓冲区)
[1. 实现过程](#1. 实现过程)
[2. 最终代码](#2. 最终代码)
[1. 设计一个进度条](#1. 设计一个进度条)
[2. 实现一个基础的进度条](#2. 实现一个基础的进度条)
[3. 优化与改进](#3. 优化与改进)
[4. 封装版本--实际中是什么用的?](#4. 封装版本--实际中是什么用的?)
一、理解缓冲区现象
1. 回车换行
回车换行其实是两个概念。
- 回车:指的是将光标水平移动 到当前行的行首 。字符表示为**\r****。**
- 换行:将光标垂直移动 到下一行 ,但水平位置不变,字符表示为**\n**。
而C语言中,我们使用 \n 往往看到的效果都是回车+换行两个动作,这是因为**操作系统或输出设备(如终端)**自动做了适配:它们将单独的 \n 解释为 回车+换行 的组合效果。
但是我们要知道,本质上\n代表的就只有换行,回车则是由 \r 实现的。
我们日常按下的 Enter 键,在大多数情况下是同时执行了"回车"和"换行"两个动作,让光标跳转到下一行的行首。
2. 缓冲区现象
先看现象
现象1:
我们先来看来下面这个代码:
这段代码的执行结果就是输出字符串"hello world",然后再然后让这个程序等待两秒才结束(sleep函数在头文件<unistd.h>中,传递函数参数seconds,表示让程序睡眠seconds秒(睡眠意味着程序暂停运行,等待一段时间后再继续))。最后呈现出的效果为:
其中processbar表示的就是main.c生成的可执行程序。
现象2:
将上面的代码的 \n 去掉,得以下代码:
呈现出的效果为:
现象:在这段代码中,由于程序一定是从上到下执行的,所以一定是先执行printf,再执行sleep的,那么应该是先回打印出hello world ,但是这里并没有打印,而是得到程序结束的时候才打印出来的,那么这是为什么呢?
理解为什么?
为什么第二段代码回先等待,在打印呢?首先我们要知道由于程序一定是从上到下执行的,所以一定是先执行printf,再执行sleep的,那么一定会打印出hello world,但是这里并没有打印,而是等待sleep后才打印的,那么在这2秒时间中 hello world一定是被保存起来了,要保存,就一定会有一段内存空间来保存,而这段空间就是缓冲区。而当程序结束时,会自动刷新缓冲区,从而将他保存的信息打印出来。
在第一段代码中,由于加了 \n ,而 \n 是可以刷新缓冲区的,所以执行结果就是先打印,在等待的。
那么当前阶段我们对缓冲区的理解就是:C语言维护的一段内存空间。
所以,上述为什么会先等待2秒,再打印,就是因为在实现printf中没有 \n 来刷新缓冲区,所以hello world 就会先保存到缓冲区,当程序退出时,由于程序会自动刷新缓冲区,才会将缓冲区中的数据都打印出来。
3. 强制刷新缓冲区
所以,为了让上面第二段代码能够在执行 printf 后就打印出信息,我们就需要一种方法来刷新缓冲区,将信息后刷出来。而实现这种情况,我们就需要使用到一个叫 fflush 函数。它就是来刷新缓冲区的。
C语言程序默认会打开三个标准输入输出流:
- 标准输入流(stdin)
- 标准输出流(stdout)
- 标准错误流(stderr)
在Linux中输入 man 3 stdin 就可以查到它们,它们的类型都是FILE的。如图所示:
而函数 flush 函数,我们也可以通过 man 3 fflush 来查到:
而print打印的数据就是打印到标准输出流上(stdout)的,准确的说,就是显示器上。
所以,我们要刷新打印到显示器上的缓冲区,只要使用**fflush(stdout)**就可以了。
修改上面的代码,得:
这样,我们执行程序后,看到的效果就是先打印出 hello world ,然后等待2秒后,程序才结束了。
二、理解倒计时的实现
1. 实现过程
有了上面,我们对缓冲区的理解和刷新缓冲区的方法,我们就可以来实现一个与其有关的倒计时程序了。
我们知道一个倒计时,它会在同一个位置,数字会逐渐减小。
如果实现?
(1)怎么实现每隔一秒就输出一个数字?
在我们打印数字的时候,我们每隔一秒就打印一个数字。那么我们就可以使用 sleep(1) 来实现。即当我们打印完一个数字,我们就将执行一个sleep(1)。代码实现(C语言):
执行结果:
可以发现,这样是一行一行打印的,并不能达到我们实现的效果。
(2)如何实现在同一行上打印?
因为我们的数字应该是在同一个位置显示的,我们值需要回车,而不需要换行。这里就不能使用 \n,而应该使用 \r 。从而实现覆盖的意思,所以实现代码为:
我们再来看执行结果:
可以发现在程序运行的前10秒内,这里什么都没有打印。这是因为,在这段代码在没有刷新缓冲区,在0~10秒中,打印的数据都在缓冲区中,程序结束缓冲区刷新,数据都会被刷新出来,但是由于每次打印都会回车,所以9~1的数字都看不到,只有在第10秒的时候,才会看到一个0。
(3)如何实现每个秒就显示一个数据?
上面的代码不能实现没隔一秒就显示一个数字,是因为,我们没有在打印完一个数字后就将缓冲区中的数字刷新出来。
所以我们就需要在代码的printf后面加一个 fflush 函数调用来刷新显示器的输出流。实现代码为:
实现结果:
(4)考虑不同位数的情况
最后,需要注意:通过printf打印出来的默认都是右对齐,因此如果我们的计数是从2位数或者3位数开始的话,显示的结果救护已出现多余的数字。比如如果从10开始倒计时,实现代码及结果如下:
这里就堕落一个0,因此我们还需要对输出的结果设置宽度,比如如果是从2位数开始倒计时,则快读看设为2,同时还需要设为左对齐(因为printf设置宽度默认为有对齐),实现代码为:
2. 最终代码
C语言实现的倒计时代码为:
cpp
#include <stdio.h>
#include <unistd.h>
int main()
{
int cnt = 10;
while(cnt >= 0)
{
printf("%-2d\r", cnt); // 使用 \r 让光标移动到行首
cnt--;
fflush(stdout); // 刷新标准输出流的缓冲区
sleep(1); // 每隔一秒就打一个数字
}
printf("\n"); // 让bash命令行和打印结果分开,方便观察
return 0;
}
注意:如果倒计时开始的数字比较大的时候,其中printf中的%d输出的宽度可以设置宽一点,但必须是左对齐。
三、进度条
1. 设计一个进度条
进度条其实就是在一段空间中,我们会根据一种规律,然后逐渐填充其中的空间(通过字符来填充),来让我们看到它的完成情况。
怎么设计?
可以先确定一块空间比如通过 [ ] 来表示出来,然后再其中填充一些字符来表示进度。如图所示:
有些进度条,有的还会显示进度比例,以及旋转光标,如图所示:
2. 实现一个基础的进度条
(1)准备工作
首先我们得有以下几个文件,来方便我们实现代码:
然后,我们就开始实现进度条代码了,这里我们只需要在 processBar.c 中写代码就可以了。
(2)如何实现?
我们可以先实现一个如图所示的简易进度条: 
要实现这样的效果,我们可以这样设计:
假如这里每一个 # 就表示1%的进度,而每秒增长进度,我们只会增加1%的进度。
- 如果进度条打满,就只会有100个#字符,那么我们就可以使用一个字符数组 bar[101] 来存储(初始化为'\0',因为C的字符串是以'\0'结尾的)。
- 这里我们会循环100次(如果从0开始就是101次),那么每一次循环,我们都会将这个字符数组以100的宽度,并且左对齐输出,同时在字符数组中添加一个字符 # 。这样就可以实现进度条递增的效果了.
- 要保证在同一行输出,则循环中,我们每一次输出完一个进度后,我们就需要进行回车 \r 。
- 要保证每秒进度都增加一个,则需要在循环中每次都使用sleep(1)来暂停1秒再输出。
- 最后为了保证,数据实时更新,则每次循环,我们还需要刷新输出流的缓冲区,即使用 fflush(stdout)。
所以我们的代码实现如下所示:
cpp
#include "processBar.h"
#include <string.h>
#include <unistd.h>
void processbar()
{
int cnt = 0;
char bar[101];
memset(bar, '\0', sizeof(bar));
// 逐渐覆盖的填充字符
while(cnt <= 100)
{
printf("[%-100s]\r", bar); //
fflush(stdout); // 刷新缓冲区
bar[cnt++] = '#';
sleep(1); // 每隔一秒就增加一个字符
}
printf("\n"); // 只是为了方便观察
}
运行后,我们就可以看到一段正在进度的 [########### ] 了。
如果觉得这样太慢,则可以使用 usleep 函数,usleep(100) 表示的就是每100微秒就睡眠一次。
下面,我们来增加显示进度比例,以及旋转光标。
即:
因为cnt的范围就是0~100,所以cnt的值就是进度比例。而旋转光标则可以定义一个全局的字符串,然后通过递增的 cnt 来映射对应下标来访问这个字符串。
C语言实现的代码如下:
cpp
#include "processBar.h"
#include <string.h>
#include <unistd.h>
const char* lable = "|/-\\";
void processbar()
{
int cnt = 0;
char bar[101];
memset(bar, '\0', sizeof(bar));
// 逐渐覆盖的填充字符
while(cnt <= 100)
{
printf("[%-100s][%d%%][%c]\r", bar, cnt, lable[cnt % 4]);
fflush(stdout); // 刷新缓冲区
bar[cnt++] = '#';
usleep(100000); // 每隔100000微秒就增加一个字符
//sleep(1); // 每隔一秒就增加一个字符
}
printf("\n"); // 只是为了方便观察
}
为了方便观察,我将上面的sleep(1),改成了usleep(100000)。这样我们就实现了一个简单的进度条了。执行结果如下所示:
(3)效果演示
进度条v1
3. 优化与改进
(1)代码改进
上述代码中,我们实现的代码中使用的常量比较多,一般我们对于这种有特殊意义的变量,可以通过宏定义来实现代码,提高可读性。C代码改进后如下所示:
cpp
#include "processBar.h"
#include <string.h>
#include <unistd.h>
#define NUM 101 // 字符数组大小
#define TOP 100 // 总比列
#define STYLE '#' // 填充进度条的字符样式
const char* lable = "|/-\\"; // 旋转光标
void processbar()
{
int cnt = 0;
char bar[NUM];
memset(bar, '\0', sizeof(bar));
// 逐渐覆盖的填充字符
while(cnt <= TOP)
{
printf("[%-100s][%d%%][%c]\r", bar, cnt, lable[cnt % 4]);
fflush(stdout); // 刷新缓冲区
bar[cnt++] = STYLE;
usleep(100000); // 每隔100000微秒就增加一个字符
//sleep(1); // 每隔一秒就增加一个字符
}
printf("\n"); // 只是为了方便观察
}
(2)样式改进
我们进度条中逐渐增加的字符也可以设为其他字符。从而实现不同的效果,比如:
代码如下所示:
cpp
#include "processBar.h"
#include <string.h>
#include <unistd.h>
#define NUM 101 // 字符数组大小
#define TOP 100 // 总比列
#define STYLE '-' // 填充进度条的字符样式
#define RIGHT '>' // 进度条最右边的字符
const char* lable = "|/-\\"; // 旋转光标
void processbar()
{
int cnt = 0;
char bar[NUM];
memset(bar, '\0', sizeof(bar));
// 逐渐覆盖的填充字符
while(cnt <= TOP)
{
printf("[%-100s][%d%%][%c]\r", bar, cnt, lable[cnt % 4]);
fflush(stdout); // 刷新缓冲区
bar[cnt++] = STYLE;
if(cnt < 100) bar[cnt] = RIGHT; // 在进度条最右边添加一个字符 >
usleep(100000); // 每隔100000微秒就增加一个字符
//sleep(1); // 每隔一秒就增加一个字符
}
printf("\n"); // 只是为了方便观察
}
(3)颜色显示
我们的进度条也是可以设置颜色的,比如,实现以下效果:
实现代码为:
cpp
#include "processBar.h"
#include <string.h>
#include <unistd.h>
#define NUM 101 // 字符数组大小
#define TOP 100 // 总比列
#define STYLE '-' // 填充进度条的字符样式
#define RIGHT '>' // 进度条最右边的字符
// 设置颜色
#define GREEN "\033[0;32;32m"
#define NONE "\033[m"
const char* lable = "|/-\\"; // 旋转光标
void processbar()
{
int cnt = 0;
char bar[NUM];
memset(bar, '\0', sizeof(bar));
// 逐渐覆盖的填充字符
while(cnt <= TOP)
{
//printf("[%-100s][%d%%][%c]\r", bar, cnt, lable[cnt % 4]);
printf(GREEN"[%-100s]"NONE"[%d%%][%c]\r", bar, cnt, lable[cnt % 4]);
fflush(stdout); // 刷新缓冲区
bar[cnt++] = STYLE;
if(cnt < 100) bar[cnt] = RIGHT; // 在进度条最右边添加一个字符 >
usleep(100000); // 每隔100000微秒就增加一个字符
//sleep(1); // 每隔一秒就增加一个字符
}
printf("\n"); // 只是为了方便观察
}
注意:从开头到该位置以上所有代码均是仅仅只对 processBar.c 的代码进行了修改。
4. 封装版本--实际中是什么用的?
进度条一般都是用来显示我们下载任务的进度的,这里我们可以来模拟实际上我们一般下载任务中进度条的使用。使用代码如下所示:
processBar.h
cpp
#include <stdio.h>
extern void processbar(int rate);
extern void initbar();
processBar.c
cpp
#include "processBar.h"
#include <string.h>
#include <unistd.h>
#define NUM 101 // 字符数组大小
#define STYLE '#' // 填充进度条的字符样式
const char* lable = "|/-\\"; // 旋转光标
char bar[NUM];
void initbar()
{
memset(bar, '\0', sizeof(bar));
}
// processbar被调用,传递了一个百分比
void processbar(int rate)
{
if(rate < 0 || rate > 100) return;
printf("[%-100s][%d%%][%c]\r", bar, rate, lable[rate % 4]);
fflush(stdout); // 刷新缓冲区
bar[rate++] = STYLE;
}
main.c
cpp
#include "processBar.h"
#include <unistd.h>
typedef void (*callback_t)(int); // 函数指针类型,这里指的就是void procebar(int rate)函数指针的类型
void downLoad(callback_t cb)
{
int total = 1000; // 1000MB 要下载的文件大小
int cur = 0; // 0MB 当前已下载的大小
while(cur <= total)
{
// 正在进行的下载过程
// ...
usleep(100000); // 模拟下载花费的时间
int rate = cur * 100 / total; // 更新进度
cb(rate); // 回调函数,显示进度
cur += 10; // 模拟当前下载了一部分
}
initbar(); // 完成下载后,就初始化一下字符数组,方便下一次下载的显示
printf("\n"); // 只是方便观察
}
int main()
{
// 进行一系列的下载任务
printf("download 1: \n");
downLoad(processbar);
printf("download 2: \n");
downLoad(processbar);
printf("download 3: \n");
downLoad(processbar);
printf("download 4: \n");
downLoad(processbar);
printf("download 5: \n");
downLoad(processbar);
printf("download 6: \n");
downLoad(processbar);
return 0;
}
代码的执行结果如图所示:
简单解释下上面的整个过程:
在main函数中,会执行一系列的下载任务,在下载这些任务时,下载过程都是通过一个循环函数模拟的,每一次循环,都会下载一点点内容,同时就会更新出一个下载进度百分比,将这个百分比数字传给processbar函数,就可以实时看到它的下载进度了。
感谢各位观看!希望大家多多支持!

其中processbar表示的就是main.c生成的可执行程序。
呈现出的效果为:



