欢迎来到 CILMY23 的博客
🏆本篇主题为:进度条实践教程:使用Makefile构建项目
🏆个人主页:CILMY23-CSDN博客
🏆系列专栏:C++ | C语言 | Linux | Python | 数据结构和算法 | 算法专题
🏆感谢观看,支持的可以给个一键三连,点赞收藏+评论。如果你觉得有帮助,还可以点点关注
文章目录
前言
上期我们介绍了 make 和 Makefile 的基本用法,本期我们将通过编写一个进度条程序来实际应用这些知识。本文将在 VS Code 与 Ubuntu 22.04 环境下实现一个简易进度条,你也可以在自己的开发环境中进行实践。
进度条准备工作
进度条样式设计
我们计划实现的进度条样式如下:
text
[*********** ][29.0%][|]
其中:
*表示已完成的进度部分;- 空格表示未完成部分;
- 右侧显示当前进度百分比;
- 末尾有一个旋转的图标,用于表示程序正在运行。
设计方案
项目文件结构

本项目包含以下文件:
text
项目根目录/
├── main.c # 程序入口
├── Makefile # 构建配置
├── Processbar.c # 进度条核心实现
├── Processbar.h # 进度条头文件
└── Processbar.exe # 编译生成的可执行文件(自动生成)
进度条构成分析
所以在有以上文件,我们就需要清楚我们应该如何设计我们的进度条

我们将进度条拆分成三个部分:
- 进度图示 :使用星号(
*)或其它字符表示已完成的部分; - 百分比数值 :实时显示当前进度,如
29.0%; - 动态图标 :一个旋转的符号序列,如
[|]、[/]、[-]、[\]。
Makefile配置
我们使用 make 进行编译,支持 make 和 make clean 命令。为了简化编译过程,我们可以使用 $@、$^ 等自动变量:
$@:表示目标文件$^:表示所有依赖文件
相应的 Makefile 内容如下:
makefile
Processbar.exe: Processbar.c main.c
gcc -o $@ $^
.PHONY: clean
clean:
rm -rf Processbar.exe
进度条
实现一个简易的倒计时
在编写进度条之前,我们先理解如何在终端实现"原地更新"效果。
换行和回车
在实现一个简易的进度条之前,我们先来了解一个简单的知识点。如何实现一个倒计时?
这是我用豆包AI生成的一个简易的倒计时,当然这是一帧的图片,如果我们让每帧30个图片的效果,就能动起来了。

想象一下,当你在阅读一本书时,每次看到新的一行,你都是从左边缘开始阅读。这就是"换行"的概念。但如果你正在填写一份表格,需要回到当前行的开头填写另一项内容,这就像是"回车"的操作。在计算机世界里,这两个概念同样存在,但它们有着微妙的区别:
在早期的打字机上,回车和换行是两个独立的操作:回车是将打印头移回行首,换行是将纸张上移一行。计算机文本处理沿用了这些概念。
打字机时代:
- 回车 (Carriage Return, \r):就像老式打字机将打印头移回左侧起点
- 换行 (Line Feed, \n):像卷动纸张,让光标移动到下一行
在Unix/Linux系统中,换行('\n')即表示新的一行,也就是回车+换行。而在Windows系统中,换行由两个字符表示:回车+换行("\r\n")。
回车(\r)与换行(\n)
- 换行(
\n) :光标移动到下一行行首(在 Linux 中相当于\r\n的效果)。 - 回车(
\r):光标返回当前行的行首,不换行。
在C语言中,当我们使用printf函数时,通常使用'\n'来换行,它会将光标移到下一行并回到行首(相当于回车+换行)。但是,如果我们只想让光标回到行首而不换行,就可以使用'\r'。
利用 \r 可以在同一行覆盖输出,实现动态效果,例如一个简单的倒计时:
c
int main()
{
int count = 10;
while(count)
{
printf("倒计时:%d\r",count);
count--;
sleep(1);
}
return 0;
}

当你运行这段代码你就会惊奇的发现,屏幕上这怎么什么都不打印。所以这就不得不提我们的一个机制,缓冲区
缓冲区
C语言执行代码都是从上往下执行的,所以都是printf先执行然后sleep后执行,所以在执行完printf和sleep休眠的一秒中,有个空窗期,那在这一个空窗期中,这一串字符串在哪里呢?
它被我们的系统存在了一个缓冲区中,缓冲区实际上就是一个内存区域。
我们可以使用在printf末尾添加'\n'字符来观察现象(读者可自行测试),其实加不加\n的差别就是,printf直接显示,不加就没有显示,程序退出才显示。
那我写了这么多还是有个疑问吗,我不知道什么是缓冲区,但缓冲区说到底就是一块内存空间。
所以printf并不是写,只是拷贝到缓冲区里,当我们带\n的时候直接输出到屏幕上了,所以在休眠期间,就放在缓冲区里,当程序结束的时候就会强制冲刷缓冲区。
如果不想程序结束的自动缓冲区,我们有两种办法。
一种是加
\n.缓冲区满了也会进行刷新。
所以我们可以画一张图出来

这里我画的比较抽象,不过不影响上述的理解惹,但是在Linux下一切皆文件,所以显示器(也就是屏幕)可以看作一个文件,我们C语言当中,程序在启动会默认执行三个输入输出流。

所以我们如果想直接刷新到屏幕上就可以用一个函数fflush,把屏幕这个文件传进去就可以实时刷新了。

所以我们的代码就可以写成
c
int main()
{
int count = 10;
while(count)
{
printf("倒计时:%-2d\r",count);
count--;
fflush(stdout);
sleep(1);
}
return 0;
}
但是在倒计时从10到1的过程中,数字的位数在变化,可能会导致覆盖不干净。例如,当从10变成9时,10是两位数,9是一位数,所以9后面会多出一个0。为了解决这个问题,我们可以在打印时固定宽度,比如用%-2d来左对齐并固定两位宽度。
实现一个简易的进度条
理解了动态效果,也能知道计算机屏幕显示刷新也是如此。
当我们使用\n换行时,就像翻到了新的一页;而使用\r回车时,就像在同一页上擦除重写。这就是进度条和倒计时的魔法所在------它们通过\r在原地更新内容,创造出动态效果。现在,让我们把倒计时的原地更新技巧,应用到更复杂的进度条显示中...(我们为了文章简洁具体的声明和main.c文件中的内容不再过多阐述,只展示Processbar.c的代码)
我们想实时刷新进度条,就可以先创建一个字符数组,然后设定一个常量字符,假设我们有一百个字符区域,空间就定义成101个,末尾用于存放'\0',然后每次写入一个*,这样我们就有一个动态的进度条了。
c
void Processbar()
{
char bar[barlength];
memset(bar,'\0',sizeof(bar));
int count = 0;
for(count=0;count<101;count++)
{
printf("[%-100s]\r",bar);
fflush(stdout);
bar[count] = '*';
usleep(100000);
}
printf("\n");
}
运行后就可以执行进度条啦
注意这里窗口长度一定要够,否则,就会变成下面这个样子
写完一个简易进度条之后,我们就可以把数值和图标加上了,图标我们用一个字符串来表示,数值就用count就好啦。
c
#include"Processbar.h"
#define signal '*'
#define barlength 101
const char* Icon = "\\|/-";
void Processbar()
{
char bar[barlength];
memset(bar,'\0',sizeof(bar));
int count = 0;
int Iconlength = strlen(Icon);
for(count=0;count<101;count++)
{
printf("[%-100s][%3d%%][%c]\r",bar,count,Icon[count%Iconlength]);
fflush(stdout);
bar[count] = '*';
usleep(100000);
}
printf("\n");
}
模拟网络下载的进度条
如果仅仅只会一个固定长度的进度条肯定是不够的,所以我们可以模拟一下网络下载。
c
// 进度条_V2
// 模拟网络下载场景
void download()
{
double filesize = 100.0 * 1024 * 1024; // 文件总大小,假设为100MB
// 换算成字节B -> 1MB = 1 * 1024 KB = 1 * 1024 * 1024 B
double Cursize = 0.0; // 当前下载
double bandwidth = 1.0 * 1024 * 1024; // 带宽(10MB/s)
printf("download begin: \n");
// 下载
while (Cursize <= filesize)
{
Processbar(filesize, Cursize);
Cursize += bandwidth;
usleep(100000);
}
}
我们要下载一个文件,然后用进度条来表示进度。
c
#include "Processbar.h"
#define signal '*'
#define barlength 101
const char *Icon = "\\|/-";
void Processbar(double FileSize, double CurSize)
{
char bar[barlength];
memset(bar, '\0', sizeof(bar));
int count = 0;
int Iconlength = strlen(Icon);
// 进度条的循环次数
double rate = (CurSize * 100.0) / FileSize;
int loopcount = (int)rate;
while (count <= loopcount)
{
printf("[%-100s][%.1lf%%][%c]\r", bar, rate, Icon[count % Iconlength]);
fflush(stdout);
bar[count] = '*';
count++;
//usleep(100000);
}
// printf("\n");
}
我们有两个函数:download 和 Processbar。
在 download 函数中,我们模拟下载过程,不断更新当前下载大小(Cursize),然后调用 Processbar 显示进度。
在 Processbar 函数中,我们根据当前下载大小和文件总大小计算下载比例,然后显示相应长度的进度条。
有两个坑一定要注意:
- usleep
- rate
在第一个代码中,usleep放在Processbar内部的while循环中,每次输出一个星号后都会等待100毫秒,然后继续循环,直到达到当前的loopcount。
而在第二个代码中,usleep放在download函数的循环中,每次调用Processbar后等待100毫秒,然后增加Cursize,再次调用Processbar。
在第一个代码中,Processbar函数内部有一个while循环,这个循环会连续打印多个字符(每次增加一个星号),并且每次打印后都等待100毫秒。
这样,在同一个函数调用中,我们会看到进度条逐渐增加,但是因为每次打印后都等待,所以看起来是动画效果。
但是,如果这个函数被频繁调用(比如在download循环中)那么每次调用Processbar时,它都会从头开始打印(因为bar数组被重新初始化),并且快速(在while循环中)打印出多个字符。
每次调用Processbar都会看到进度条从0开始增加到当前进度,然后下一次调用又从头开始。这样就会导致频闪。把usleep放在download中就不会了。
其次是进度条的循环次数
text
示例1:完成50%
假设 FileSize = 100, CurSize = 50
rate = (50 * 100.0) / 100 = 50.0
loopcount = (int)50.0 = 50
循环条件:count <= 50
循环次数 = 51次 (count从0到50)
示例2:完成100%
假设 FileSize = 100, CurSize = 100
rate = (100 * 100.0) / 100 = 100.0
loopcount = (int)100.0 = 100
循环条件:count <= 100
循环次数 = 101次
最后大家就可以根据自己的想法添加创意啦,如果你感觉有收获的话可以给个三连点点关注,谢谢各位,我们下篇博客见!

