【Linux】进度条实践教程:使用Makefile构建项目

欢迎来到 CILMY23 的博客

🏆本篇主题为:进度条实践教程:使用Makefile构建项目

🏆个人主页:CILMY23-CSDN博客

🏆系列专栏:C++ | C语言 | Linux | Python | 数据结构和算法 | 算法专题

🏆感谢观看,支持的可以给个一键三连,点赞收藏+评论。如果你觉得有帮助,还可以点点关注


文章目录


前言

上期我们介绍了 makeMakefile 的基本用法,本期我们将通过编写一个进度条程序来实际应用这些知识。本文将在 VS Code 与 Ubuntu 22.04 环境下实现一个简易进度条,你也可以在自己的开发环境中进行实践。

进度条准备工作

进度条样式设计

我们计划实现的进度条样式如下:

text 复制代码
[***********                     ][29.0%][|]

其中:

  • * 表示已完成的进度部分;
  • 空格表示未完成部分;
  • 右侧显示当前进度百分比;
  • 末尾有一个旋转的图标,用于表示程序正在运行。

设计方案

项目文件结构

本项目包含以下文件:

text 复制代码
项目根目录/
├── main.c           # 程序入口
├── Makefile         # 构建配置
├── Processbar.c     # 进度条核心实现
├── Processbar.h     # 进度条头文件
└── Processbar.exe   # 编译生成的可执行文件(自动生成)

进度条构成分析

所以在有以上文件,我们就需要清楚我们应该如何设计我们的进度条

我们将进度条拆分成三个部分:

  1. 进度图示 :使用星号(*)或其它字符表示已完成的部分;
  2. 百分比数值 :实时显示当前进度,如 29.0%
  3. 动态图标 :一个旋转的符号序列,如 [|][/][-][\]

Makefile配置

我们使用 make 进行编译,支持 makemake clean 命令。为了简化编译过程,我们可以使用 $@$^ 等自动变量:

  • $@:表示目标文件
  • $^:表示所有依赖文件

相应的 Makefile 内容如下:

makefile

复制代码
Processbar.exe: Processbar.c main.c
	gcc -o $@ $^

.PHONY: clean
clean:
	rm -rf Processbar.exe

进度条

实现一个简易的倒计时

在编写进度条之前,我们先理解如何在终端实现"原地更新"效果。

换行和回车

在实现一个简易的进度条之前,我们先来了解一个简单的知识点。如何实现一个倒计时?

这是我用豆包AI生成的一个简易的倒计时,当然这是一帧的图片,如果我们让每帧30个图片的效果,就能动起来了。

想象一下,当你在阅读一本书时,每次看到新的一行,你都是从左边缘开始阅读。这就是"换行"的概念。但如果你正在填写一份表格,需要回到当前行的开头填写另一项内容,这就像是"回车"的操作。在计算机世界里,这两个概念同样存在,但它们有着微妙的区别:

在早期的打字机上,回车和换行是两个独立的操作:回车是将打印头移回行首,换行是将纸张上移一行。计算机文本处理沿用了这些概念。

打字机时代

  1. 回车 (Carriage Return, \r):就像老式打字机将打印头移回左侧起点
  2. 换行 (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后执行,所以在执行完printfsleep休眠的一秒中,有个空窗期,那在这一个空窗期中,这一串字符串在哪里呢?

它被我们的系统存在了一个缓冲区中,缓冲区实际上就是一个内存区域。

我们可以使用在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 函数中,我们根据当前下载大小和文件总大小计算下载比例,然后显示相应长度的进度条。

有两个坑一定要注意:

  1. usleep
  2. 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次

最后大家就可以根据自己的想法添加创意啦,如果你感觉有收获的话可以给个三连点点关注,谢谢各位,我们下篇博客见!

相关推荐
沉在嵌入式的鱼2 小时前
linux串口对0X0D、0X0A等特殊字符的处理
linux·stm32·单片机·特殊字符·串口配置
Better Bench3 小时前
Ubuntu aarch64\arm64系统安装vscode
linux·vscode·ubuntu
暴风游侠3 小时前
linux知识点-服务相关
linux·服务器·笔记
阿海5744 小时前
卸载nginx的shell脚本
linux·nginx
JANG10244 小时前
【Linux】常用指令
linux·服务器·javascript
DeeplyMind4 小时前
使用parted工具扩展QCOW2磁盘大小完整方案
linux·qemu·virtialization
蓝天~白云4 小时前
ESXI虚拟机启动卡住在0%,无法关闭
linux·运维·服务器
明月心9524 小时前
IP 中 0/24 和 0/16 的区别
linux·服务器·网络·ip
没有名字的鬼4 小时前
1 Linux 系统简介
linux