进度条---命令行版本
回车换行
其实本质上回车和换行是不同概念,我们用一张图来简单的理解一下:
在计算机语言当中:
换行符:\n
回车符:\r
\r\n:回车换行
这时候有人可能会有疑问:我在学习C/C++语言的时候,单纯的\n就起到了回车加换行的行为呀?!
答:因为在语言层面\n给我们编译成了\r\n
缓冲区问题
在我们目前的阶段,我们将缓冲区看作是一段内存块,我们可以先实现一个测试代码:
#touch test.c
#可以直接
vim test.c
测试代码
:!man 3 sleep
#include<stdio.h>
#include<unistd.h>
int main()
{
//printf("hello Linux!\n");
printf("hello Linux!");
sleep(3);
return 0;
}
我们在Linux编译后./test执行时,当只有上被注释的代码被执行,和只有第二句printf的代码,我们发现前者内容打印出来,程序3秒后结束,后者是3秒后程序结束才打印内容,我们可以明显感受到程序是先执行sleep(3)的,但是我们之前学习C/C++知道,我们定义的程序逻辑都是从上往下执行的,可是我们为什么没有看到字符串呢?
我们应该知道,对于后者,printf在sleep之前一定执行完了,但是显示器上没有显示,那么在我们休眠3秒期间,字符串" hello Linux! "在哪里?其实该字符串在缓冲区里面,可以理解为内存有一小内存块,将该字符串临时保存在该处了。我们带有\n的字符串可以在显示屏上打印,我们称为行刷新,也就是遇到\n,printf执行完就默认将其字符串直接显示到显示器了(行刷新),可以理解为没有\n就不给行刷新;
那后者为什么最后也可以刷新出来呢?
是因为程序退出了,会自动刷新缓冲区!
我们如何可以让不带\n的字符串能够立马刷新呢?
我们如果想让不带\n的字符串立马刷新,可以使用fflush:
fflush
是 C 语言标准库函数之一,用于刷新流的缓冲区。其原型定义在 <stdio.h>
头文件中
我们可以进行man手册查看。
printf("hello Linux!");
fflush(stdout);
其实我们的程序会默认打开三个输入输出流,分别是:(描述符分别为1,2,3)
-
标准输入流(
stdin
):用于从标准输入设备(通常是键盘)读取数据。 -
标准输出流(
stdout
):用于向标准输出设备(通常是屏幕)输出数据。 -
标准错误流(
stderr
):用于输出错误信息和其他诊断信息,通常也是输出到屏幕。
都是一个指向 FILE
结构体的指针,定义在 <stdio.h>
中。
我们的printf默认是从标准输出流里面打,我们可以看到fprintf其实就是比printf多了一个参数:
在Linux下,一切皆文件,当一个程序在 Linux 下运行时,操作系统会自动为该程序打开三个标准流文件描述符,也就是下面两条代码是等价的,fprintf只是显示的表示出往哪里输出:
fprintf(stdout,"hello Linux!\n");
printf("hello Linux!\n");
我们就可以利用缓冲区来实现一个倒计时:光标显示后回退(回车)
int main()
{
int i=9;
while(i>=0)
{
printf("%d\r",i);
i--;
sleep(1);
}
return 0;
}
但是我们发现在显示器上没有显示任何数据,这是为什么呢?
因为我们行刷新是需要\n的,%d后是\r,\r不支持我们行刷新,所以对应的信息并未有显示出来,还在缓冲区里面存着呢,所以需要用到fflush(stdout);
int main()
{
int i=9;
while(i>=0)
{
printf("%d\r",i);
fflush(stdout);
i--;
sleep(1);
}
printf("\n");//跑完之后想要保留0(命令行),不让会覆盖
return 0;
}
为了使我们对下面进度条程序更好的实现,我们来看看当i取10的时候:
命令行变化:10-90-80-70-60-50-40-30-20-10-00----00
这时候,我们应该好好理解显示:
举个例子,我们往显示器上输入:12345,是输出的是12345数字,还是'1' '2' '3' '4' '5'字符?
显示器只认识字符!显示器是字符设备,所以输出的是后者,这也是为什么我们需要像%d的格式化输出,所以我们可以将上述代码中的%d改成%2d,还可以在前面加一个-即"%-2d"来整体靠左显示,代码我就不写出来了,在原代码上做改进就🆗。
以上我们进度条的预备工作就基本完成了(回车换行,缓冲区问题,格式问题,字符设备的理解问题,输出设备的刷新问题)
我们想写一个怎么样的进度条
框架的搭建
创建文件
首先,在我们自己的工作目录中创建四个文件:
-
main.c
-
Makefile
-
process.c
-
process.h
编写 Makefile
使用 vim
编辑器打开 Makefile
,并写入以下内容:
SRC:=$(wildcard *.c)
OBJ:=$(SRC:.c=.o)
BIN=processbar
$(BIN): $(OBJ)
gcc -o $@ $^
%.o: %.c
gcc -c $<
.PHONY: clean
clean:
rm -f $(OBJ) $(BIN)
这个 Makefile
定义了如何编译和链接你的程序。它使用 wildcard
自动查找所有 .c
文件,并生成相应的 .o
文件。最终生成的可执行文件名为 processbar
。
编写 process.h
使用 vim
编辑器打开 process.h
,并写入以下内容:
#pragma once
#include <stdio.h>
void process_v1();
这个头文件声明了一个函数 process_v1
,该函数将在 process.c
中实现。
编写 process.c
使用 vim
编辑器打开 process.c
,并写入以下内容:
#include "process.h"
void process_v1() {
printf("hello rose!\n");
}
这个文件实现了 process_v1
函数,目前只是简单地打印一条消息。
编写 main.c
使用 vim
编辑器打开 main.c
,并写入以下内容:
#include "process.h"
int main() {
process_v1();
return 0;
}
这个文件是程序的入口点,它调用了 process_v1
函数。
编译和运行
完成上述文件编写后,你可以使用以下命令进行编译:
make
这将根据 Makefile
的规则编译并生成 processbar
可执行文件。然后,你可以运行该程序:
./processbar
如果一切顺利,你应该会看到输出 "hello rose!"。
清理
最后,使用以下命令清理生成的文件:
make clean
这将删除所有 .o
文件和 processbar
可执行文件。
接下来,我们就可以在 process.c
中实现进度条的功能。可以根据具体需求设计进度条的更新逻辑和显示方式。
version1原理版本
#include"process.h"
#include<string.h>
#include<unistd.h>
#define NUM 101//因为会有一个\0的存在
#define STYLE '='
void process_v1()
{
char buffer[NUM];
memset(buffer,0,sizeof(buffer));
const char *lable="|/-\\";
size_t len=strlen(lable);
//计数器
int cnt=0;
while(cnt<=100)//这个循环会循环101次
{
printf("[%-100s][%d%%][%c]\r",buffer,cnt,lable[cnt%len]);
fflush(stdout);
buffer[cnt]=STYLE;
cnt++;
//sleep(1);
usleep(10000);
}
printf("\n");
return;
}
-
定义常量和变量
NUM
定义为 101,因为进度条的长度为 100 个字符,加上一个字符串的终止符\0
。STYLE
定义为=
,用于表示进度条的已填充部分。buffer
是一个字符数组,用于存储进度条的当前状态。lable
是一个字符串,包含旋转的符号|/-\
,用于在进度条旁边显示一个旋转的动画效果。len
是lable
字符串的长度。cnt
是一个计数器,用于控制进度条的更新和旋转符号的切换。
-
初始化缓冲区
- 使用
memset
函数将buffer
初始化为全 0,即空字符串。
- 使用
-
进度条更新循环
while
循环会执行 101 次,因为cnt
从 0 开始,直到 100 结束。- 在每次循环中,使用
printf
函数输出当前的进度条状态:[%-100s]
表示一个宽度为 100 的左对齐字符串,buffer
作为参数传入,表示进度条的当前填充状态。[%d%%]
表示当前的百分比进度,cnt
作为参数传入。[%c]
表示旋转符号,lable[cnt%len]
计算当前应该显示的旋转符号。
- 使用
\r
作为printf
的结尾,表示返回到行首,这样下一次输出会覆盖当前行的内容。 - 调用
fflush(stdout)
确保输出立即显示在屏幕上,而不是被缓冲。 - 将
buffer[cnt]
设置为STYLE
,即=
,表示进度条的已填充部分向右扩展一个字符。 cnt++
更新计数器。usleep(10000)
暂停 10 毫秒,使进度条的更新速度适中,便于观察。注释掉的sleep(1)
是暂停 1 秒的另一种方式,但会使进度条更新过慢。
-
结束输出
- 循环结束后,调用
printf("\n")
输出一个换行符,使光标移动到下一行,避免后续输出覆盖进度条。
- 循环结束后,调用
整体功能:
这个 process_v1
函数实现了一个动态更新的文本进度条,进度条的长度为 100 个字符,旁边有一个旋转的符号动画。通过在循环中逐步填充 buffer
并输出,模拟了进度条的动态增长过程。每次更新后,使用 \r
返回行首并刷新输出,实现了进度条的原地更新效果。
version2真实版本
main.c
#include "process.h"
#include <stdio.h>
#include <unistd.h>
double total = 1024.0; // 总下载量,单位为MB
double speed = 1.0; // 下载速度,单位为MB/s
void DownLoad() {
double current = 0; // 当前已下载量
while (current <= total) {
// 刷新进度条
FlushProcess(total, current);
// 模拟下载数据,每次循环下载speed大小的数据
usleep(300000); // 暂停300ms,模拟下载延时
current += speed;
}
printf("\ndownload %.2lfMB Done\n", current);
}
int main() {
DownLoad();
return 0;
}
version2
#include "process.h"
#include <string.h>
#include <unistd.h>
#define NUM 101 // 因为会有一个\0的存在
#define STYLE '='
// version2
void FlushProcess(double total, double current) {
char buffer[NUM];
memset(buffer, 0, sizeof(buffer));
const char *lable = "|/-\\";
size_t len = strlen(lable);
static int cnt = 0; // 静态变量,用于记录旋转符号的位置
// 计算当前进度条的填充数量
int num = (int)(current * 100 / total);
for (int i = 0; i < num; i++) {
buffer[i] = STYLE;
}
double rate = current / total; // 计算当前下载进度的百分比
cnt %= len; // 计算旋转符号的索引
printf("[%-100s][%.1f%%][%c]\r", buffer, rate * 100, lable[cnt]);
cnt++;
fflush(stdout); // 确保进度条立即显示
}
进度条与下载模拟功能梳理:
-
下载模拟:
DownLoad
函数模拟了一个下载过程,其中total
表示总下载量,speed
表示下载速度。- 在
while
循环中,每次循环模拟下载speed
大小的数据,并通过usleep
函数暂停一段时间来模拟下载延时。 - 循环过程中,不断调用
FlushProcess
函数来刷新进度条,显示当前的下载进度。
-
进度条刷新:
FlushProcess
函数负责根据当前下载量current
和总下载量total
计算进度条的填充状态,并输出到屏幕。buffer
数组用于存储进度条的当前状态,num
变量计算出需要填充的=
字符的数量。- 使用
printf
函数输出进度条,其中[%-100s]
表示一个宽度为 100 的左对齐字符串,用于显示进度条的填充部分;[%.1f%%]
显示当前的下载百分比;[%c]
显示一个旋转的符号,用于增加动画效果。 - 使用
\r
作为printf
的结尾,表示返回到行首,使下一次输出覆盖当前行的内容,实现进度条的原地更新效果。 fflush(stdout)
确保进度条的输出立即显示在屏幕上,而不是被缓冲。
整体功能
这个程序通过模拟下载过程,并在每次下载数据后刷新进度条,实现了动态显示下载进度的效果。进度条的长度为 100 个字符,旁边有一个旋转的符号动画,使用户可以直观地看到下载进度的变化。当下载完成后,程序会输出一条完成信息,告知用户下载已成功完成。
我们不仅仅可以在下载方面使用进度条,我们也可以在上传方面使用进度条,因此,在main.c源文件当中,我们typedef一个函数指针来使用回调函数来优化代码:
#include"process.h"
#include<stdio.h>
#include<unistd.h>
typedef void (*callback_t) (double total,double current);//函数指针
double total=1024.0;
double speed=1.0;
//回调函数
void DownLoad(callback_t cb)
{
double current=0;
while(current<=total)
{
//刷新进度
cb(total,current);
//下载代码
usleep(3000);//充当下载数据
current+=speed;
}
printf("\ndownload %.2lfMB Done\n",current);
}
void UpLoad(callback_t cb)
{
double current=0;
while(current<=total)
{
//刷新进度
cb(total,current);
//下载代码
usleep(3000);//充当下载数据
current+=speed;
}
printf("\nUpload %.2lfMB Done\n",current);
}
int main()
{
DownLoad(FlushProcess);
UpLoad(FlushProcess);
return 0;
}