【Linux我做主】进度条小程序深度解析
Linux下C语言进度条程序深度解析
- 进度条小程序
- GitHub地址
- 前言
- 前置知识
- 回车换行(CR/LF)的深度解析
- 历史渊源与技术规范
- 在进度条/倒计时中的应用
- 缓冲区机制的全面剖析
- 缓冲区引入
- 缓冲类型对比
- 进度条开发中的关键控制
- 进度条实现
- 以小见大——倒计时
- 倒计时最终效果演示
- 错误演示
- 位宽不够带来的影响
- 设置位宽后不反转带来的影响
- 不使用\r回车带来的影响
- 总结回顾倒计时
- 进度条架构设计
- 组件关系图
- 核心数据结构
- 版本迭代解析
- v1版本悟原理
- progressBar.h头文件
- progressBar.c源文件
- main.c调用
- V2版本求拓展
- progressBar.h头文件
- progressBar.c源文件
- main.c模拟多任务调度实现
- 最终效果演示
- Makefile配置要点
- 结语
进度条小程序
GitHub地址
有梦想的电信狗
前言
在Linux
系统编程中,控制台交互的视觉反馈是提升用户体验的重要环节。进度条作为经典的人机交互组件,在软件安装、文件传输、数据处理等场景中具有广泛应用价值。本文将以Linux
环境下C语言实现的进度条程序为切入点,深入探讨控制台输出控制、缓冲区机制、函数指针应用等核心技术。通过三个版本迭代的代码解析(基础版/V1、模拟多任务版/V2),读者将掌握从原理到实践的完整知识链路。
前置知识
回车换行(CR/LF)的深度解析
在C语言中,我们使用\n
来表示换行,这其实是C语言帮我们做了处理。实际上,回车和换行其实是两个动作。
C语言中用
\n
来表示回车和换行。
以上图片深入阐述了回车和换行概念以及和区别。
\r
:回车,光标回到当前行的最开始。C语言中用\r
来表示仅回车。\n
:换行,光标垂直向下移动一行,叫做换行。
历史渊源与技术规范
- ASCII规范定义:
CR
(Carriage Return,\r
,ASCII 13
)将光标移动到行首 LF
(Line Feed,\n
,ASCII 10
)使光标下移一行Windows
系统采用CRLF
组合实现新行操作Linux/Unix
系统使用LF
单独完成换行
在进度条/倒计时中的应用
printf("%-3d\r", cnt); // 关键代码示例
此代码实现:
- 使用
%-3d
保证3字符宽度左对齐- 3表示该值位宽为3,C语言默认为右对齐,用
-
来表示左对齐。
- 3表示该值位宽为3,C语言默认为右对齐,用
\r
使每次输出回到行首- 配合
fflush(stdout)
强制刷新缓冲区 - 实现原地更新的数字倒计时效果
缓冲区机制的全面剖析
缓冲区引入
先看如下两个例子:
- 有换行符
\n
时显示器直接刷新
- 无换行符时,像是先执行了
sleep
再执行printf
经分析得知: C语言
中一定是按顺序执行代码的,因此一定是printf
先执行,再执行sleep
。- 那么,在
sleep
期间,printf
函数一定已经执行完了。 - 那么,
sleep
期间,hello wolrd
在哪里?
综上,hello wolrd
一定是被保存起来了!!!
保存hello wolrd
,必然 需要一块内存空间,这块内存空间被称为缓冲区。
- 缓冲区就是由
C语言
维护的一段内存。
C程序运行时,默认会帮助我们打开三个输入输出流
stdin
:标准输入stdout
:标准输出(默认是显示器)stderr
:标准错误
C语言的默认行为是在程序退出时,再刷新缓冲区。
printf
打印消息,是向stdout
输入,消息暂存在了stdout
中,当我们不想让消息暂存在缓冲区中,而是想直接刷新stdout
的内容到显示器时,可以使用fflush
刷新,默认stdout
在程序结束时刷新,使用fflush
可以强制进行刷新输入输出流。
- 以下:
- 此时
printf("hello world")
没有\n
- 使用
fflush(stdout)
强制将缓冲区中的数据刷新到显示器上
通过以上两个例子,我们已经对缓冲区有了一个大概的理解了。
缓冲类型对比
缓冲类型 | 特征 | 典型应用场景 |
---|---|---|
全缓冲 | 缓冲区满时刷新 | 文件操作 |
行缓冲 | 遇到换行符或缓冲区满时刷新 | 终端输出(默认) |
无缓冲 | 立即输出 | 标准错误流stderr |
进度条开发中的关键控制
- 手动刷新机制:
fflush(stdout); // 强制立即输出缓冲区内容
进度条实现
以小见大——倒计时
倒计时最终效果演示
- 代码如下:
void test() {//实现一个倒计时int cnt = 100;while (cnt >= 0) {// printf("%-2d\r", cnt);//使用\r回车会导致三位数只刷新了两位数printf("%-3d\r", cnt); // %3d\r 可以实现在行的开头更新数字 -相当于反转 确保是左对齐fflush(stdout);--cnt;sleep(1);}printf("\n");
}
int main() {test();return 0;
}
- 当前设置倒计时从
100
开始 - 关于
printf
中的参数%-3d\r
的解释%d的作用
:表示输出数字cnt
,用于显示倒计时3的作用
:用于控制输出显示位宽为3
,倒计时的数字是几位,位宽就设置为几。-的作用
:设置位宽后,C程序默认为右对齐,我们想让数字在当前行的最左侧显示,要用-
实现左对齐。\r的作用
:用于实现仅回车,回到当前行的开始,覆盖打印实现倒计时的效果。
- 使用
fflush(stdout)
将缓冲区内的数据立即刷新出来 sleep(1)
每隔一秒循环一次
错误演示
位宽不够带来的影响
- 位宽小于数字的位数时,会出现数字残余的情况。
- 由于显示器是字符设备,只会一个一个打印字符,123实际上是1 2 3三个字符连在一起表示的
- 以下是从10开始计数的错误例子
- 正确设置位宽即可解决。
设置位宽后不反转带来的影响
- 不用
-
进行反转会导致数字不靠左显示- 对于倒计时来说影响甚微,但靠右显示的话,会导致进度条从右向左加载!
不使用\r回车带来的影响
- 不使用
\r
会导致数字接连不断的出现,不符合倒计时的效果。
总结回顾倒计时
printf
中的格式化控制参数缺一不可
- 关于
printf
中的参数%-3d\r
的理解%d
表示输出数字cnt
,用于显示倒计时3
用于控制输出显示位宽为3
,倒计时的数字是几位,位宽就设置为几。- 设置位宽后,C程序默认为右对齐,我们想让数字在当前行的最左侧显示,要用
-
实现左对齐。 \r
用于实现仅回车,回到当前行的开始,覆盖打印实现倒计时的效果。
- 使用
fflush(stdout)
将缓冲区内的数据立即刷新出来
进度条架构设计
组件关系图
核心数据结构
#define NUM 102 // 缓冲区长度(含终止符)
#define BODY '=' // 进度条主体字符
#define HEAD '>' // 进度头部指示符
#define TOP 100 // 进度最大值typedef void (*callback_t)(int); // 标准化回调接口
- 利用函数指针实现回调。
版本迭代解析
v1版本悟原理
progressBar.h头文件
#pragma once#include <stdio.h>//缓冲区长度(含终止符)
#define NUM 102 // 102 表示字符数组的长度 0-100 101个字符 末尾是\0, 因此大小是 102
#define BODY '=' // 进度条主体字符
#define HEAD '>' // 进度头部指示符
#define TOP 100 // 进度最大值extern void progressbar(int speed); // extern 声明外部变量时必须加上 函数声明可加可不加
#pragma once
:防止头文件重复包含#define NUM 102
:102
表示字符数组的长度,0-100
,共101
个字符 ,字符串末尾是\0
, 因此数组长度是102
#define BODY '='
:定义进度条的形体为=
#define HEAD '>'
:定义进度条的头部为>
#define TOP 100
:定义进度条的区间长度,暂定为100
通过宏的方式定义,可以方便的实现进度条样式的修改!
progressBar.c源文件
#include "progressBar.h"
#include <string.h>
#include <unistd.h>char bar[NUM] = {0};
const char* label = "|/-\\";void progressbar(int speed) {memset(bar, '\0', sizeof(bar)); //整体将字符串设为\0,可以方便的输出int len = strlen(label);int cnt = 0;while (cnt <= TOP) {//没有\n 就没有立即刷新,因为显示器默认是行刷新printf("[%-100s][%d%%][%c]\r", bar, cnt, label[cnt % len]);//预留出100空间,100s 默认是右对齐,进度条是反的, 用-100s解决//用变长的字符串,循环覆盖输出,实现进度条光标移动的效果//给进度条跑 %%显示百分号fflush(stdout);bar[cnt++] = BODY; //更改进度条的风格if (cnt < TOP)bar[cnt] = '>';// sleep(1);usleep(speed); // 100000微秒,用usleep实现更快的跑完}printf("\n"); //防止命令行提示符影响效果
}
char bar[NUM] = {0}
:进度条主体使用长度不断改变的字符串来实现- 初始化为{0},这样就不用手动设置
\0
终止符了,用%s
输出变化的字符串 - 从
0-100
,恰好是char bar[NUM] = {0}
字符数组中每个字符的下标 - 每次循环内
cnt
会++- 利用
cnt
将0-100
每个位置的字符都设置为进度条主体=
。 - 随着
cnt++
,数组内字符串的长度也在增长,再通过%s\r
回车数组字符串,从而实现进度条的动态增长
- 初始化为{0},这样就不用手动设置
const char* label = "|/-\\"
- 通过
0-4
五个字符的顺序循环输出,实现光标闪动的效果。
- 通过
printf("[%-100s][%d%%][%c]\r", bar, cnt, label[cnt % len])
[%-100s]
-
:确保进度条不反方向增长100s
:预留出100
长度,供进度条字符串填充
[%d%%]
:%d
:输出进度数字cnt
%%
:控制输出字符%
[%c]
:控制循环顺序输出|/-\\
中的每个字符,实现光标闪动的效果- 用
cnt % len
实现0-4
的循环
- 用
\r
:实现每次从行首开始输出,实现进度条的动态增长!
fflush(stdout)
:每次printf
过后,刷新缓冲区usleep(speed)
:usleep
结合函数参数speed
,实现进度条时长的控制
main.c调用
#include <unistd.h>
#include "progressBar.h"int main(){progressbar(20000);return 0;
}
效果如下:
V2版本求拓展
- 进度条最常见的适用场景就是在下载任务中,具体的运行方式应该是:
- 下载任务向进度条函数传递下载任务已完成的进度百分比,进度条函数根据比率动态显示
- 因此下载任务内一定要反复调用进度条程序。
- 我们可以传入进度条函数的地址进行实现,也就是函数指针的回调函数。
progressBar.h头文件
#pragma once#include <stdio.h>//缓冲区长度(含终止符)
#define NUM 102 // 102 表示字符数组的长度 0-100 101个字符 末尾是\0, 因此大小是 102
#define BODY '=' // 进度条主体字符
#define HEAD '>' // 进度头部指示符
#define TOP 100 // 进度最大值typedef void (*callback_t)(int); //函数指针类型extern void progressbar(int rate); // extern 声明外部变量时必须加上 函数声明可加可不加
extern void downLoad(callback_t cb);
extern void initBar();
typedef void (*callback_t)(int)
:callback_t
是函数指针类型,我们可以拆解帮助理解:void (*)(int)
:其中:*
:表示这是一个指针,且必须用括号包裹。否则会被解析为函数返回指针(如void *func(int)
是返回void*
的函数)(*)
:表示这是一个函数指针void
:表示该函数的返回值为void,(int)
:表示该函数的参数类型为一个int
typedef void (*callback_t)(int)
可以理解为typedef void (*)(int) callback_t
typedef
:在typedef
中,参数名会被省略,只保留类型。因此可以拆解为:typedef void (*)(int) callback_t
:将void (*)(int)
类型的指针定义为类型别名callback_t
progressBar.c源文件
#include "progressBar.h"
#include <string.h>
#include <unistd.h>
// v2 应用
char bar[NUM] = {0};
const char* label = "|/-\\";void initBar() {memset(bar, '\0', sizeof(bar));
}
void progressbar(int rate) {if (rate < 0 || rate > 100)return;int len = strlen(label);//用单个字符循环覆盖输出实现光标闪动printf("[%-100s][%d%%][%c]\r", bar, rate, label[rate % len]);fflush(stdout);bar[rate++] = BODY; //更改进度条的风格if (rate < TOP)bar[rate] = HEAD;
}
优化亮点:
-
全局状态保存实现多任务支持
-
initBar()
提供重置进度条的能力 -
动态头部指示符(>)增强视觉效果
-
增加非法进度的判断
if (rate < 0 || rate > 100) return
main.c模拟多任务调度实现
#include <unistd.h>
#include "progressBar.h"
// 模拟下载任务调用进度条
void downLoad(callback_t cb) { int total = 1000;int curr = 0; //目前curr需要从0开始while (curr <= total) {//进行某种下载任务,模拟时使用手动控制速度usleep(50000);int rate = curr * 100 / total;cb(rate); // 传入参数,回调展示进度curr += 10;}printf("\n"); //防止命令行提示符影响效果
}int main() {printf("downLoan 1:\n");downLoad(progressbar);initBar();printf("downLoan 2:\n");downLoad(progressbar);initBar();printf("downLoan 3:\n");downLoad(progressbar);initBar();return 0;
}
- 通过任务调用进度条,外部程序通过回调,调用进度条
void (callback_t)(int)
,callback_t
是函数指针类型。- 相当于
typedef void (*)(int) callback_t
,把callback_t
变成函数指针类型的别名
- 相当于
关键技术:
- 函数指针实现回调机制
- 速率换算算法(
curr * 100 / total
) - 时间控制(
usleep
微秒级延时) char bar[NUM]
为全局数组,每次模拟download
后,需initbar()
函数重置保证独立性- 模块化设计,确保了可维护性。
最终效果演示
Makefile配置要点
# 依赖关系# 依赖方法
progressBar:*.c @gcc $^ -o $@# clean和上面是独立的
.PHONY:clean
clean:@rm -f progressBar
结语
关键知识点回顾
- 控制台输出控制:回车符与换行符的灵活运用
- 缓冲区机制:行缓冲特性与强制刷新策略
- 可视化设计:进度条元素(主体、头部、百分比)的协同
- 软件工程实践:模块化设计、回调机制、多任务支持
扩展应用场景
- 大数据处理进度监控
- 嵌入式系统固件更新
- 自动化测试进度反馈
- 游戏加载界面优化
通过本文对Linux下C语言进度条程序的深度解析,我们系统性地掌握了控制台交互的核心技术。从回车换行符的底层原理到缓冲区刷新机制,从进度条动态显示到模块化设计,每一步都揭示了控制台可视化反馈的实现精髓。通过函数指针与回调机制的精妙配合,我们实现了多任务场景下的独立进度管理,展现了C语言在系统编程中的强大灵活性。
本项目的核心价值在于:不仅实现了基础的进度展示功能,更通过版本迭代演进,示范了软件开发的渐进式优化思路。在性能层面,未来可结合纳秒级延时控制与多线程安全机制提升精度;在交互层面,可考虑引入ANSI色彩代码或动态图标可进一步增强用户体验。
分享到此结束啦
一键三连,好运连连!