【Linux】第一个小程序:进度条

目录

一、理解缓冲区现象

[1. 回车换行](#1. 回车换行)

[2. 缓冲区现象](#2. 缓冲区现象)

[3. 强制刷新缓冲区](#3. 强制刷新缓冲区)

二、理解倒计时的实现

[1. 实现过程](#1. 实现过程)

[2. 最终代码](#2. 最终代码)

三、进度条

[1. 设计一个进度条](#1. 设计一个进度条)

[2. 实现一个基础的进度条](#2. 实现一个基础的进度条)

(1)准备工作

(2)如何实现?

(3)效果演示

[​3. 优化与改进](#3. 优化与改进)

​(1)代码改进

(2)样式改进

(3)颜色显示

[4. 封装版本--实际中是什么用的?](#4. 封装版本--实际中是什么用的?)



一、理解缓冲区现象

1. 回车换行

回车换行其实是两个概念。

  • 回车:指的是将光标水平移动 到当前行的行首 。字符表示为**\r****。**
  • 换行:将光标垂直移动下一行 ,但水平位置不变,字符表示为**\n**。

而C语言中,我们使用 \n 往往看到的效果都是回车+换行两个动作,这是因为**操作系统或输出设备(如终端)**自动做了适配:它们将单独的 \n 解释为 回车+换行 的组合效果。

但是我们要知道,本质上\n代表的就只有换行,回车则是由 \r 实现的。

我们日常按下的 Enter 键,在大多数情况下是同时执行了"回车"和"换行"两个动作,让光标跳转到下一行的行首。

2. 缓冲区现象

先看现象

现象1:

我们先来看来下面这个代码:

这段代码的执行结果就是输出字符串"hello world",然后再然后让这个程序等待两秒才结束(sleep函数在头文件<unistd.h>中,传递函数参数seconds,表示让程序睡眠seconds秒(睡眠意味着程序暂停运行,等待一段时间后再继续))。最后呈现出的效果为:其中processbar表示的就是main.c生成的可执行程序。

现象2:

将上面的代码的 \n 去掉,得以下代码:呈现出的效果为:

现象:在这段代码中,由于程序一定是从上到下执行的,所以一定是先执行printf,再执行sleep的,那么应该是先回打印出hello world ,但是这里并没有打印,而是得到程序结束的时候才打印出来的,那么这是为什么呢?

理解为什么?

为什么第二段代码回先等待,在打印呢?首先我们要知道由于程序一定是从上到下执行的,所以一定是先执行printf,再执行sleep的,那么一定会打印出hello world,但是这里并没有打印,而是等待sleep后才打印的,那么在这2秒时间中 hello world一定是被保存起来了,要保存,就一定会有一段内存空间来保存,而这段空间就是缓冲区。而当程序结束时,会自动刷新缓冲区,从而将他保存的信息打印出来。

在第一段代码中,由于加了 \n ,而 \n 是可以刷新缓冲区的,所以执行结果就是先打印,在等待的。

那么当前阶段我们对缓冲区的理解就是:C语言维护的一段内存空间

所以,上述为什么会先等待2秒,再打印,就是因为在实现printf中没有 \n 来刷新缓冲区,所以hello world 就会先保存到缓冲区,当程序退出时,由于程序会自动刷新缓冲区,才会将缓冲区中的数据都打印出来。

3. 强制刷新缓冲区

所以,为了让上面第二段代码能够在执行 printf 后就打印出信息,我们就需要一种方法来刷新缓冲区,将信息后刷出来。而实现这种情况,我们就需要使用到一个叫 fflush 函数。它就是来刷新缓冲区的。

C语言程序默认会打开三个标准输入输出流:

  1. 标准输入流(stdin)
  2. 标准输出流(stdout)
  3. 标准错误流(stderr)

在Linux中输入 man 3 stdin 就可以查到它们,它们的类型都是FILE的。如图所示:

而函数 flush 函数,我们也可以通过 man 3 fflush 来查到:

而print打印的数据就是打印到标准输出流上(stdout)的,准确的说,就是显示器上。

所以,我们要刷新打印到显示器上的缓冲区,只要使用**fflush(stdout)**就可以了。

修改上面的代码,得:

这样,我们执行程序后,看到的效果就是先打印出 hello world ,然后等待2秒后,程序才结束了。


二、理解倒计时的实现

1. 实现过程

有了上面,我们对缓冲区的理解和刷新缓冲区的方法,我们就可以来实现一个与其有关的倒计时程序了。

我们知道一个倒计时,它会在同一个位置,数字会逐渐减小。

如果实现?

(1)怎么实现每隔一秒就输出一个数字?

在我们打印数字的时候,我们每隔一秒就打印一个数字。那么我们就可以使用 sleep(1) 来实现。即当我们打印完一个数字,我们就将执行一个sleep(1)。代码实现(C语言):

执行结果:

可以发现,这样是一行一行打印的,并不能达到我们实现的效果。

(2)如何实现在同一行上打印?

因为我们的数字应该是在同一个位置显示的,我们值需要回车,而不需要换行。这里就不能使用 \n,而应该使用 \r 。从而实现覆盖的意思,所以实现代码为:

我们再来看执行结果:可以发现在程序运行的前10秒内,这里什么都没有打印。这是因为,在这段代码在没有刷新缓冲区,在0~10秒中,打印的数据都在缓冲区中,程序结束缓冲区刷新,数据都会被刷新出来,但是由于每次打印都会回车,所以9~1的数字都看不到,只有在第10秒的时候,才会看到一个0。

(3)如何实现每个秒就显示一个数据?

上面的代码不能实现没隔一秒就显示一个数字,是因为,我们没有在打印完一个数字后就将缓冲区中的数字刷新出来。

所以我们就需要在代码的printf后面加一个 fflush 函数调用来刷新显示器的输出流。实现代码为:

实现结果:

(4)考虑不同位数的情况

最后,需要注意:通过printf打印出来的默认都是右对齐,因此如果我们的计数是从2位数或者3位数开始的话,显示的结果救护已出现多余的数字。比如如果从10开始倒计时,实现代码及结果如下:

这里就堕落一个0,因此我们还需要对输出的结果设置宽度,比如如果是从2位数开始倒计时,则快读看设为2,同时还需要设为左对齐(因为printf设置宽度默认为有对齐),实现代码为:

2. 最终代码

C语言实现的倒计时代码为:

cpp 复制代码
#include <stdio.h>
#include <unistd.h>

int main()
{
    int cnt = 10;
    while(cnt >= 0)
    {
        printf("%-2d\r", cnt); // 使用 \r 让光标移动到行首
        cnt--;

        fflush(stdout); // 刷新标准输出流的缓冲区

        sleep(1); // 每隔一秒就打一个数字
    }

    printf("\n"); // 让bash命令行和打印结果分开,方便观察

    return 0;
}

注意:如果倒计时开始的数字比较大的时候,其中printf中的%d输出的宽度可以设置宽一点,但必须是左对齐。


三、进度条

1. 设计一个进度条

进度条其实就是在一段空间中,我们会根据一种规律,然后逐渐填充其中的空间(通过字符来填充),来让我们看到它的完成情况。

怎么设计?

可以先确定一块空间比如通过 [ ] 来表示出来,然后再其中填充一些字符来表示进度。如图所示:

有些进度条,有的还会显示进度比例,以及旋转光标,如图所示:

2. 实现一个基础的进度条

(1)准备工作

首先我们得有以下几个文件,来方便我们实现代码:

然后,我们就开始实现进度条代码了,这里我们只需要在 processBar.c 中写代码就可以了。

(2)如何实现?

我们可以先实现一个如图所示的简易进度条:

要实现这样的效果,我们可以这样设计:

假如这里每一个 # 就表示1%的进度,而每秒增长进度,我们只会增加1%的进度。

  • 如果进度条打满,就只会有100个#字符,那么我们就可以使用一个字符数组 bar[101] 来存储(初始化为'\0',因为C的字符串是以'\0'结尾的)。
  • 这里我们会循环100次(如果从0开始就是101次),那么每一次循环,我们都会将这个字符数组以100的宽度,并且左对齐输出,同时在字符数组中添加一个字符 # 。这样就可以实现进度条递增的效果了.
  • 要保证在同一行输出,则循环中,我们每一次输出完一个进度后,我们就需要进行回车 \r 。
  • 要保证每秒进度都增加一个,则需要在循环中每次都使用sleep(1)来暂停1秒再输出。
  • 最后为了保证,数据实时更新,则每次循环,我们还需要刷新输出流的缓冲区,即使用 fflush(stdout)。

所以我们的代码实现如下所示:

cpp 复制代码
#include "processBar.h"
#include <string.h>
#include <unistd.h>

void processbar()
{
    int cnt = 0;
    char bar[101]; 
    memset(bar, '\0', sizeof(bar));

    // 逐渐覆盖的填充字符
    while(cnt <= 100)
    {
        printf("[%-100s]\r", bar); // 
        
        fflush(stdout); // 刷新缓冲区

        bar[cnt++] = '#';

        sleep(1); // 每隔一秒就增加一个字符
    }

    printf("\n"); // 只是为了方便观察
}

运行后,我们就可以看到一段正在进度的 [########### ] 了。

如果觉得这样太慢,则可以使用 usleep 函数,usleep(100) 表示的就是每100微秒就睡眠一次

下面,我们来增加显示进度比例,以及旋转光标。

即:

因为cnt的范围就是0~100,所以cnt的值就是进度比例。而旋转光标则可以定义一个全局的字符串,然后通过递增的 cnt 来映射对应下标来访问这个字符串。

C语言实现的代码如下:

cpp 复制代码
#include "processBar.h"
#include <string.h>
#include <unistd.h>

const char* lable = "|/-\\";

void processbar()
{
    int cnt = 0;
    char bar[101]; 
    memset(bar, '\0', sizeof(bar));

    // 逐渐覆盖的填充字符
    while(cnt <= 100)
    {
        printf("[%-100s][%d%%][%c]\r", bar, cnt, lable[cnt % 4]); 
        
        fflush(stdout); // 刷新缓冲区

        bar[cnt++] = '#';

        usleep(100000); // 每隔100000微秒就增加一个字符
        //sleep(1); // 每隔一秒就增加一个字符
    }

    printf("\n"); // 只是为了方便观察
}

为了方便观察,我将上面的sleep(1),改成了usleep(100000)。这样我们就实现了一个简单的进度条了。执行结果如下所示:

(3)效果演示

进度条v1

3. 优化与改进

(1)代码改进

上述代码中,我们实现的代码中使用的常量比较多,一般我们对于这种有特殊意义的变量,可以通过宏定义来实现代码,提高可读性。C代码改进后如下所示:

cpp 复制代码
#include "processBar.h"
#include <string.h>
#include <unistd.h>

#define NUM 101 // 字符数组大小
#define TOP 100  // 总比列
#define STYLE '#' // 填充进度条的字符样式

const char* lable = "|/-\\"; // 旋转光标


void processbar()
{
    int cnt = 0;
    char bar[NUM]; 
    memset(bar, '\0', sizeof(bar));

    // 逐渐覆盖的填充字符
    while(cnt <= TOP)
    {
        printf("[%-100s][%d%%][%c]\r", bar, cnt, lable[cnt % 4]); 
        
        fflush(stdout); // 刷新缓冲区

        bar[cnt++] = STYLE;

        usleep(100000); // 每隔100000微秒就增加一个字符
        //sleep(1); // 每隔一秒就增加一个字符
    }

    printf("\n"); // 只是为了方便观察
}

(2)样式改进

我们进度条中逐渐增加的字符也可以设为其他字符。从而实现不同的效果,比如:代码如下所示:

cpp 复制代码
#include "processBar.h"
#include <string.h>
#include <unistd.h>

#define NUM 101 // 字符数组大小
#define TOP 100  // 总比列
#define STYLE '-' // 填充进度条的字符样式
#define RIGHT '>' // 进度条最右边的字符

const char* lable = "|/-\\"; // 旋转光标

void processbar()
{
    int cnt = 0;
    char bar[NUM]; 
    memset(bar, '\0', sizeof(bar));

    // 逐渐覆盖的填充字符
    while(cnt <= TOP)
    {
        printf("[%-100s][%d%%][%c]\r", bar, cnt, lable[cnt % 4]); 
        
        fflush(stdout); // 刷新缓冲区

        bar[cnt++] = STYLE;
        if(cnt < 100) bar[cnt] = RIGHT; // 在进度条最右边添加一个字符 > 

        usleep(100000); // 每隔100000微秒就增加一个字符
        //sleep(1); // 每隔一秒就增加一个字符
    }

    printf("\n"); // 只是为了方便观察
}

(3)颜色显示

我们的进度条也是可以设置颜色的,比如,实现以下效果:实现代码为:

cpp 复制代码
#include "processBar.h"
#include <string.h>
#include <unistd.h>

#define NUM 101 // 字符数组大小
#define TOP 100  // 总比列
#define STYLE '-' // 填充进度条的字符样式
#define RIGHT '>' // 进度条最右边的字符

// 设置颜色
#define GREEN "\033[0;32;32m"
#define NONE "\033[m"


const char* lable = "|/-\\"; // 旋转光标

void processbar()
{
    int cnt = 0;
    char bar[NUM]; 
    memset(bar, '\0', sizeof(bar));

    // 逐渐覆盖的填充字符
    while(cnt <= TOP)
    {
        //printf("[%-100s][%d%%][%c]\r", bar, cnt, lable[cnt % 4]); 
        printf(GREEN"[%-100s]"NONE"[%d%%][%c]\r", bar, cnt, lable[cnt % 4]); 
        
        fflush(stdout); // 刷新缓冲区

        bar[cnt++] = STYLE;
        if(cnt < 100) bar[cnt] = RIGHT; // 在进度条最右边添加一个字符 > 

        usleep(100000); // 每隔100000微秒就增加一个字符
        //sleep(1); // 每隔一秒就增加一个字符
    }

    printf("\n"); // 只是为了方便观察
}

注意:从开头到该位置以上所有代码均是仅仅只对 processBar.c 的代码进行了修改。

4. 封装版本--实际中是什么用的?

进度条一般都是用来显示我们下载任务的进度的,这里我们可以来模拟实际上我们一般下载任务中进度条的使用。使用代码如下所示:

processBar.h

cpp 复制代码
#include <stdio.h>

extern void processbar(int rate);

extern void initbar();

processBar.c

cpp 复制代码
#include "processBar.h"
#include <string.h>
#include <unistd.h>

#define NUM 101 // 字符数组大小
#define STYLE '#' // 填充进度条的字符样式

const char* lable = "|/-\\"; // 旋转光标

char bar[NUM];
void initbar()
{
    memset(bar, '\0', sizeof(bar));
}

// processbar被调用,传递了一个百分比
void processbar(int rate)
{
    if(rate < 0 || rate > 100) return;

    printf("[%-100s][%d%%][%c]\r", bar, rate, lable[rate % 4]); 
        
    fflush(stdout); // 刷新缓冲区

    bar[rate++] = STYLE;
}

main.c

cpp 复制代码
#include "processBar.h"
#include <unistd.h>
typedef void (*callback_t)(int); // 函数指针类型,这里指的就是void procebar(int rate)函数指针的类型

void downLoad(callback_t cb)
{
    int total = 1000; // 1000MB 要下载的文件大小
    int cur = 0; // 0MB 当前已下载的大小

    while(cur <= total)
    {
        // 正在进行的下载过程
        // ...
        
        usleep(100000); // 模拟下载花费的时间
        int rate = cur * 100 / total; // 更新进度

        cb(rate); // 回调函数,显示进度

        cur += 10; // 模拟当前下载了一部分
    }
    initbar(); // 完成下载后,就初始化一下字符数组,方便下一次下载的显示
    
    printf("\n"); // 只是方便观察
}

int main()
{
    // 进行一系列的下载任务
    printf("download 1: \n");
    downLoad(processbar);

    printf("download 2: \n");
    downLoad(processbar);

    printf("download 3: \n");
    downLoad(processbar);
    
    printf("download 4: \n");
    downLoad(processbar);
    
    printf("download 5: \n");
    downLoad(processbar);
    
    printf("download 6: \n");
    downLoad(processbar);
    return 0;
}

代码的执行结果如图所示:

简单解释下上面的整个过程:

在main函数中,会执行一系列的下载任务,在下载这些任务时,下载过程都是通过一个循环函数模拟的,每一次循环,都会下载一点点内容,同时就会更新出一个下载进度百分比,将这个百分比数字传给processbar函数,就可以实时看到它的下载进度了。


感谢各位观看!希望大家多多支持!

相关推荐
扬帆破浪1 小时前
免费开源AI软件.桌面单机版,可移动的AI知识库,察元 AI桌面版:本地离线知识库的最小依赖 Linux下不联外网装包跑通
linux·运维·人工智能
kyle~1 小时前
Linux---挂载系统
linux·运维·服务器
wqdian_com1 小时前
华为手机浏览器的一个bug
服务器·华为·bug
qinyia1 小时前
服务器异常流量排查:发现并清除kswpad挖矿后门及持久化守护进程
运维·服务器·人工智能
Bechamz1 小时前
大数据开发学习Day30
大数据·学习
颂love1 小时前
Git的简单学习
git·学习
凡梦千华1 小时前
CentOS系统安装Elasticsearch,RPM包方式
linux·elasticsearch·centos
倔强的石头1061 小时前
【Linux 指南】文件系统系列(二):核心抽象层 —— 块 、分区 、inode 从原理到实操
linux·服务器·数据库
谷雨不太卷1 小时前
TCP外壳
linux·网络·tcp/ip