自制第一个Linux小程序——进度条

进度条

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 ,是10 两个字符;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()的彩色输出,就留给我们自行探索吧!

相关推荐
zbyyd1 小时前
深入理解 Linux 文件 IO 与目录 IO:系统调用与库函数的本质区别
linux·c语言
qetfw1 小时前
Debian OpenLDAP 目录服务配置:DN、LDIF、ldapsearch 与认证验证
linux·运维·debian
词却1 小时前
Linux基础:Linux环境搭建与远程连接
linux
不悔哥1 小时前
Linux 小电视:稚晖君的开源桌面终端,从 6 层板到 Quark
linux
qq_349447952 小时前
在ollama下部署DeepSeek-r1模型(linux机器)
linux·运维·服务器
hehelm2 小时前
仿muduo库实现高并发服务器—Channel类
linux·服务器·开发语言·网络·c++
小此方2 小时前
Linux加餐(二):藏在Linux中的设计模式(二)单例模式与线程池
linux·单例模式·设计模式
猫吃了源码2 小时前
Linux系统制作 Ubuntu Server 24.04 启动U盘详细教程
linux·服务器·ubuntu
hehelm2 小时前
仿muduo库实现高并发服务器—EventLoop
linux·服务器·网络·c++