进度条
1、两个储备知识,以及编写一个demo
1.1、回车与换行,是一回事吗?
回车与换行不是一回事:
- 回车:光标回到当前行的开头
- 换行:光标新起一行
我们之前在C/C++代码中使用的\n std::endl,其实是回车 + 换行。
回车的转义字符是\r,回车加换行的转义字符是\r\n。
1.2、缓冲区的简单认识
给出一段C语言代码:
cpp
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("Hello world.\n"); // \n与\r\n的效果一样
sleep(1);
return 0;
}
没有循环语句和分支语句的情况下,C语言在每一个函数域中执行代码,都是从上往下执行的。也就是说,上面代码中,printf函数先于sleep函数执行。
我们在Linux上运行一下,确实是先有字符串,再停顿了一会儿。
对于这段代码,我们把\n去除:
cpp
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("Hello world.");
sleep(1);
return 0;
}
上面代码中,printf函数也应该是先于sleep函数执行。
但如果我们把上面程序放在Linux环境下跑的时候,我们会发现,我们等了一秒空白,Hello world.才出现。这似乎与C语言从上往下执行的规则不符。
printf函数一定是先于sleep函数执行的,只是Hello world.字符串没有显示到屏幕上。那么字符串去哪了呢?
答案是进入了缓冲区,被缓存起来了;之后程序结束,字符串才被刷新到了屏幕上。
所以,\n与\r\n除了有回车换行的功能,还可以立即将缓冲区里的内容刷新到屏幕上。
其实,还有一个函数具有立即刷新的功能:fflush。
cpp
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("Hello world.");
fflush(stdout); //
sleep(1);
return 0;
}
这么一修改,程序也能达到先显示字符串,后等待的效果。
1.3、demo
我们尝试编写一个倒计时小程序:
c
#include<stdio.h>
#include<unistd.h>
int main()
{
for (int cnt = 9; cnt >= 0; cnt--)
{
printf("%d\r", cnt);
sleep(1);
}
printf("\n");
return 0;
}
为了能够达到数字在屏幕上跳动切换的效果,我们期望打印完一个数字时光标回到最开始,继续打印下一个数字以覆盖前一个数字;同时加上sleep函数。
可是,我们在Linux上运行程序,等了老半天还没出现一个数字,最后程序结束前才出现一个0。这是怎么回事?
原来,数字全部先写入了缓冲区,而\r没有刷新缓冲区的功能,所以数字没有刷新出来,我们也就看不到。
所以我们需要手动刷新,加上一个fflush函数:
c
#include<stdio.h>
#include<unistd.h>
int main()
{
for (int cnt = 9; cnt >= 0; cnt--)
{
printf("%d\r", cnt);
fflush(stdout);
sleep(1);
}
printf("\n");
return 0;
}
数字不显示的问题是解决了,我们却遇到了另外一个问题:当我们把cnt修改成10,运行程序时我们会发现,跳动的数字后面诡异地跟着一个0 ,然后程序倒计时结束后回显一个诡异的00。这又是怎么回事?
要知道,程序向显示器中写入的10 ,是1 和0 两个字符;0字符未被覆盖,所以直到程序结束之前,0会一直被保留。
还记得吗?
printf:格式化输出,将数据转化成字符
scanf:格式化输入,将字符转化成数据
键盘是一种字符设备。
修改的方法,是将%d改为%-2d,其中,
- 2表示占两个字符的位置,这样就可以刷新掉0。
- **-**表示左对齐。
c
#include<stdio.h>
#include<unistd.h>
int main()
{
for (int cnt = 9; cnt >= 0; cnt--)
{
printf("%-2d\r", cnt); //
fflush(stdout);
sleep(1);
}
printf("\n");
return 0;
}
2、制作进度条
有了上面的知识,我们就可以制作第一个版本的进度条。
2.1、第一个版本的进度条
c
// main.c
#include"process.h"
int main()
{
Process();
return 0;
}
c
// process.h
#pragma once
#include<stdio.h>
#include<unistd.h>
#include<string.h>
void Process();
c
// poocess.c
#include"process.h"
void Process()
{
// 写一个进度条,包含:
// =表示进度条,百分比,旋转图标
int cnt = 0;
char str[102]; // 表示进度的字符串,将会依次放入=
memset(str, '\0', sizeof(str));
const char* flag = "|/-\\"; // 旋转图标
int len = strlen(flag);
while (cnt <= 100)
{
printf("[%-100s][%3d%%][%c]\r", str, cnt, flag[cnt%len]); // 打印
fflush(stdout); // 刷新
str[cnt] = '=';
usleep(100000);
cnt++;
}
printf("\n");
}
当前制作的进度条有三个要素:
- "="的增长表示进度的增长
- 百分比记录进度
- 旋转图标表明进度正在进行,即使速度很慢

但是,当前的进度条,能否真正满足实际场景的需求吗?
答案是不能的。我们要根据实际任务的进度,来实时更新进度条。所以我们将制作第二个版本的进度条。
2.2、第二个版本的进度条
我们可以虚构出一个场景:
有一个下载任务,软件包的大小是1024,下载速度固定是1。那么下载每完成1,进度条都要更新一次。
我们就可以先调用下载函数,进行下载;每一次current进度推进时,我们就调用进度条函数更新进度条。
c
// main.c
#include"process.h"
double gtotal = 1024.0; // 总任务
double gspeed = 1.0; // 速度
void DownLoad(double gtotal)
{
int current = 0; // 已经完成了多少
if (current > gtotal) return; // 保护措施
while (current <= gtotal)
{
Process(gtotal, current); // 每推进一个单位,更新一次进度条
current += gspeed;
usleep(90000);
}
if (current >= gtotal) printf("\n"); // 换行
}
int main()
{
DownLoad(gtotal);
return 0;
}
c
// process.h
#pragma once
#include<stdio.h>
#include<unistd.h>
#include<string.h>
void Process(double total, double cnt);
c
// process.c
#include"process.h"
#define Flag '='
void Process(double total, double cnt)
{
// 1. 计算比率
double rate = cnt*100 / total;
// 2. 填充进度条
char str[102];
memset(str, '\0', sizeof(str));
int i = 0;
for (; i < (int)rate; ++i)
{
str[i] = Flag;
}
// 3. 制作动态图标
const char* flag[] = {
"",
".",
"..",
"...",
"..",
".",
""
};
int len = sizeof(flag) / sizeof(flag[0]);
static int begin = 0;
// 4. 打印进度条
printf("[%-100s][%5.1lf%%][%-3s]\r", str, rate, flag[begin%len]);
++begin;
fflush(stdout);
}
现在,一个与下载任务绑定的基本进度条就已经初步创建成了。但是还有一些地方可以完善:
2.2.1、完善进度条内容
进度条不仅可以有进度的直观显示、记录百分比、动态图标,还可以有目标文件总大小 、已经下载了的大小 、网速及网速的单位。
c
// process.c
#include"process.h"
#define Flag '='
void Process(double total, double cnt, double speed, const char* speedinfo) // 函数参数加上速度,与速度单位
{
// 1. 计算比率
double rate = cnt*100 / total;
// 2. 填充进度条
char str[102];
memset(str, '\0', sizeof(str));
int i = 0;
for (; i < (int)rate; ++i)
{
str[i] = Flag;
}
// 3. 制作动态图标
const char* flag[] = {
"",
".",
"..",
"...",
"..",
".",
""
};
int len = sizeof(flag) / sizeof(flag[0]);
static int begin = 0;
// 4. 制作进度条
printf("[%-100s][%5.1lf%%][%-3s] | %.1lf/%.1lf, %.1lf%s\r",\
str, rate, flag[begin%len], total, cnt, speed, speedinfo); // 总大小/已经下载的大小, 速度
++begin;
fflush(stdout);
}
2.2.2、网速的不稳定性
文件下载时,网速其实是不稳定的,时好时坏。
我们可以创建一个网速的数组,然后利用随机数随机选取一个网速,以达到模拟真实情况下网速不稳定的效果。
我们还需注意一点的是,由于网速是随机的,下载的量可能超过文件总量,所以我们要做一步判断:
- 如果已下载的量小于文件总量,正常下载
- 如果已下载的量大于等于文件总量,那么文件已经下载完成了,所有的进度都应调整成100%
c
// main.c
#include"process.h"
double gtotal = 1024.0;
//double gspeed = 1.0;
void DownLoad(double gtotal)
{
int current = 0;
if (current > gtotal) return;
double speed[] = {0.01, 0.05, 0.1, 0.2, 0.5, 1.1, 1.8, 2.12, 5.8, 10.5, 20.7, 35.4}; // 随机时间的数组
while (1) // 设置成死循环
{
double cnt_speed = speed[rand() % (sizeof(speed) / sizeof(speed[0]))];
if (current < gtotal) // 没下完,正常下载
{
Process(gtotal, current, cnt_speed, "MB/s");
current += cnt_speed;
usleep(90000);
}
else // 已经下完了,100%,结束循环
{
Process(gtotal, gtotal, cnt_speed, "MB/s");
break;
}
}
if (current >= gtotal) printf("\n");
}
int main()
{
srand((unsigned int)time(NULL));
DownLoad(gtotal);
return 0;
}
2.2.3、多文件下载
在某些场景下我们需要下载多个文件,那么我们就可以调用多个下载函数。
c
// main.c
// ...
int main()
{
srand((unsigned int)time(NULL));
DownLoad(gtotal);
DownLoad(66.7);
DownLoad(2048.0);
DownLoad(999.0);
DownLoad(120.4);
return 0;
}

2.3、两种补充设计
函数指针
我们在下载函数DownLoad()中显示调用了进度条函数Process(),这显然是增加了代码的耦合度的,因为如果我们修改了Process(),DownLoad()也需要跟着修改。
C++中我们可以用仿函数和lambda表达式,但是C语言中我们只能用函数指针。
c
// process.h
typedef void (*flush)(...) // 重命名函数指针类型
c
// main.c
void DownLoad(..., flush cb)
{
cb(...); // 回调函数
}
printf()可以进行彩色输出
printf()可以进行彩色输出,包括但不限于字体彩色、背景彩色。
printf()的彩色输出,就留给我们自行探索吧!