跳到主要内容 Linux C 语言实现进度条详解 | 极客日志
C
Linux C 语言实现进度条详解 在 Linux 环境下使用 C 语言实现进度条的方法。首先讲解了回车符(\r)与换行符(\n)的区别及缓冲区刷新机制(fflush)。接着通过倒计时小程序演示了如何控制输出流。随后分两个版本实现了进度条:v1 版本为匀速推进;v2 版本支持动态速度模拟真实场景,并引入回调函数解耦业务逻辑与显示逻辑。代码包含 main.c、process.c 和 process.h 文件结构,展示了 Makefile 构建过程。最终实现了可配置总数据量、当前进度及操作类型的通用进度条组件。
云间漫步 发布于 2026/3/30 更新于 2026/4/13 0 浏览1.补充知识回车与换行
在实现进度条的项目之前,我们需要了解关于回车和换行的基础补充知识。
首先我们要了解的是回车和换行有什么区别。在现代计算机中基本将这两个概念不进行区分了,但在老式打字机上换行是将纸向下移动,而回车需要将指针移动回起始位置。
在现代的计算机当中,回车其实是换行 + 回车 的组合。在 C 语言当中使用的**\n本质上实现了换行 + 回车的功能, \r**实现的就是回车的功能。
2.练手小程序——倒计时
以上我们了解了回车的本质,接下来通过一个倒计时的小程序来作为实现进度条项目之前的练手。
首先来看以下的代码:
#include <stdio.h>
#include <unistd.h>
int main () {
printf ("hello world\n" );
sleep(5 );
return 0 ;
}
运行会出现什么现象?程序会在显示器当中打印出 hello world,之后休眠 5 秒再让程序结束。
再看以下的代码会有什么现象:
#include <stdio.h>
#include <unistd.h>
int main () {
printf ("hello world" );
sleep(5 );
return 0 ;
}
以上的代码相比原来的代码只是少了一个换行符\n,此时该程序运行的结果会和之前的一样吗?
通过将该程序形成可执行程序运行之后就可以看出和之前不同,该程序一开始没有将 hello world 输出到显示器上,而是到程序结束的时候才打印到显示器上。要解答以上的问题就需要了解 C 语言当中进行输出的流程是怎么样的。其实在将对应的信息输出到显示器之前是会将数据先存储到缓冲区,之后再统一的将缓冲区内的数据刷新到显示器上。
这其实和显示器行刷新 的策略有关,当使用\n之后会将缓冲区当中的数据刷新到显示器上,而第二份代码当中未使用\n就使得程序缓冲区当中的数据需要在程序结束的时候才刷新到显示器上。
那么在第二份代码当中不使用\n但要使其与第一份代码执行出相同的效果,要使用什么样的方式呢?
在此就需要了解一个库函数 fflush。
在此 fflush 的作用就是将缓冲区当中的数据进行强制刷新 。我们知道在 C 语言的程序启动时默认会打开stdin、stdout、stderr 三个标准输入输出流,那么此时就可以将缓冲区当中的数据强制刷新到标准输出流 stdout 上。
改进的代码如下所示:
#
{
( );
fflush( );
sleep( );
;
}
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown 转 HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
HTML 转 Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online
include
<stdio.h>
#include <unistd.h>
int
main
()
printf
"hello world"
stdout
5
return
0
了解了缓冲区之后接下来就来尝试编写倒计时程序的代码,在此要求是从 10 开始倒计时,倒计时的数字在同一行内一直进行刷新,直到最后数字为 0 时结束。
#include <stdio.h>
#include <unistd.h>
int main () {
int cnt= 10 ;
while (cnt>=0 ) {
printf ("%d\r" ,cnt);
cnt--;
fflush(stdout );
sleep(1 );
}
printf ("\n" );
return 0 ;
}
以上的代码就是使用了以上我们学习的 fflush 在进行每一次的 printf 之后就进行强制的刷新,并且在每次输出一个数字之后使用\r 进行回车,这样就可以使得每一秒显示器上会出现一个数字且在同一行内输出。但是以后的代码还有一个问题,我们来通过运行看看。
通过以下的输出就可以看出问题的所在是在输出时一开始打印的 10 是两位数,之后再进行打印的是一位数,这就会使得打印出的结果变为了 90、80……
这个问题形成的原因是由于 printf 在输出时默认的是右对齐,此时要修改为左对齐只需要在 printf 的占位符%之后加上一个-2 即可,这样就可以限定最小宽度为 2。
#include <stdio.h>
#include <unistd.h>
int main () {
int cnt= 10 ;
while (cnt>=0 ) {
printf ("%-2d\r" ,cnt);
cnt--;
fflush(stdout );
sleep(1 );
}
printf ("\n" );
return 0 ;
}
3.进度条项目实现 通过以上的倒计时小程序我们完成了在实现进度条项目之前的练手,那么接下来就可以开始实现我们 Linux 学习当中的第一个项目,在此会实现两个版本的进度条项目,其中第一个版本的代码较为简单,但是不具有实用性,第二份的代码才具有实用性。
在实现代码之前首先要来明确我们的实现的项目的要求是什么,在此实现出的效果需大致如下所示:
首先是有一个进度条进行进度的推进直到进度条满为止,在进度条之后有一个显示当前进度的百分比数,之后有一个旋转的光标来表示当前进度条是在推进的。
3.1 v1 版本 在实现该进度条项目时分为三个文件,在 process.h 内进行实现函数的声明,在 process.c 内实现对应的函数,在 main.c 使用实现的函数来实现进度条。
那么接下来就先来实现第一个版本的进度条,在此创建一个函数 process_v1,在该函数内实现对应的代码。在该版本当中的进度条只需要每秒使得进度条向后推进指定的进度即可,并且在进度条之后的百分数也显示对应的进度值。
但在实现 process_v1 内的函数代码之前我们先要将该程序对应的 makefile 进行构建。
BIN=process.exe
SRC=$(wildcard *.c)
OBJ=$(SRC:.c=.o)
CC=gcc
$(BIN) :$(OBJ)
$(CC) -o $@ $^
%.o:%.c
$(CC) -c $<
.PHYNO:clean clean:
rm -f $(OBJ) $(BIN)
首先在实现进度条要从 0 到 100,那么就创建一个大小为 101 的字符数组来存储对应的进度数据,使用'#'来表示进度元素,接下来使用 memset 函数将数组 buffer 内的元素都初始化为 0。
由于要在进度条的最后使用一个动态的翻转来表明进度条是在运行的,那么在此就创建一个数组 tmp,该数组内存储对应的字符,进度条每增长一位就将对应的状态字符改变。
接下来创建一个变量 cnt 来表示当前进度条的进度值,使用 while 循环来实现 0 到 100 进度。在此过程中每次打印完字符串、进度条百分比、状态元素之后都要回车并且进行强制刷新 。
每次打印完之后将 buffer 数组内对应 cnt 下标位置元素修改为'#',再让 cnt++。
注:在此每次状态元素都使用 cnt%len,这样就可以使得得到的下标一直在 tmp 字符串的范围内。
以上使用的函数 usleep 和 sleep 类似也是进行休眠,不过单位是毫秒 。
#include "process.h"
#define MAX 101
#define STYLE '#'
void process_v1 () {
char buffer[MAX];
memset (buffer,0 ,sizeof (buffer));
const char * tmp="|/-\\" ;
int len=strlen (tmp);
int cnt=0 ;
while (cnt<=100 ) {
printf ("[%-100s][%d%%][%c]\r" ,buffer,cnt,tmp[cnt%len]);
fflush(stdout );
usleep(50000 );
buffer[cnt]=STYLE;
cnt++;
}
printf ("\n" );
}
编写完 process_v1 的代码之后接下来就使用 make 命令生成对应的可执行程序 process.exe。
3.2 v2 版本 在上述内容中,我们已经实现了进度条的第一个版本。尽管上述进度条程序确实能够运行,但问题在于我们上面实现的进度条速度是均匀的。这在现实中几乎是不可能的情况。更多时候,进度条会呈现出速度的变化。例如,在下载文件时,由于网速的影响,网速快时进度条移动得快,网速慢时进度条移动得慢。因此,接下来实现的第二个版本的进度条将更符合实际情况。
在此创建一个 process_v2 来表示该版本进度条的实现。
该函数的参数有两个分别为总的数据值以及当前完成任务的数据值。在该函数的内部就需要实现 cur 占 total 总量的百分比进度条,在此数据的变化就不在该函数内实现。
首先和之前的 process_v1 函数类似也是先创建一个大小为 101 的数组 buffer 来存储进度条的内容,再创建一个数组 tmp 来存储动态变化的字符。
之后和之前实现函数不同的是使用 cnt 变量的值来确定对应状态数组的下标,创建一个浮点数的变量 count 来统计当前进度条的百分比,再使用变量 sum 来统计进度条内#的个数。
#define MAX 101
#define STYLE '#'
void process_v2 (double totle,double cur) {
char buffer[MAX];
memset (buffer,0 ,sizeof (buffer));
const char * tmp="|/-\\" ;
int len=strlen (tmp);
static int cnt=0 ;
double count=cur*100 /totle;
int sum=(int )count;
int i=0 ;
for (;i<=sum;i++) {
buffer[i]=STYLE;
}
printf ("[%-100s][%.1lf%%][%c]\r" ,buffer,count,tmp[cnt]);
fflush(stdout );
cnt++;
cnt%=len;
}
实现了 process_v2 函数之后接下来就需要在 main.c 当中实现一个具体的场景,假设在此实现的是文件的下载,下载的总量是 1024mb,下载的速度是 0.1mb 每秒,那么接下来实现的函数就如下所示:
#include "process.h"
double totle=1024.0 ;
double speed=1.0 ;
void DownLoad () {
double cur=0 ;
while (cur<=totle) {
process_v2(totle,cur);
usleep(10000 );
cur+=speed;
}
}
int main () {
DownLoad();
return 0 ;
}
执行以上代码 make 之后形成的可执行程序,输出结果如下所示:
以上虽然实现的进度条能执行下去,但是以上还是匀速的,那么此时要使得呈现出来的进度条快慢是变化就需要使用随机数。
并且还将下载总的数据量的大小由 main 函数内调用 DownLoad 时传参。
在此之前先在 process.h 内引用对应的头文件
#include "process.h"
void DownLoad (double totle) {
srand(time(NULL ));
double speed=rand()%51 ;
double cur=0 ;
while (cur<=totle) {
process_v2(totle,cur);
usleep(10000 );
cur+=speed;
}
printf ("\n" );
}
int main () {
DownLoad(10240 );
return 0 ;
}
以上代码此时就会存在一个问题就是最终 cur 的值可能会无法和 totle 正好匹配,那么此时最后进度条就无法显示到 100%,因此接下来就要对这种情况进行处理。
在此只需要按照以上的方式处理即可实现最终的进度条到 100%。
以上实现的进度条以及基本满足我们的预期了,接下来就是对以上代码进行优化,在以上代码中 DownLoad 和 process_v2 的耦合度 较高,在此我们可以修改为回调函数 的方式来实现函数的调用。
并且在 process_v2 函数当中再添加一个变量来表示当中进度进行的操作是什么。
#pragma once
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <stdlib.h>
void process_v1 () ;
void process_v2 (const char * s,double totle,double cur) ;
#include "process.h"
#define MAX 101
#define STYLE '#'
void process_v2 (const char * s,double totle,double cur) {
char buffer[MAX];
memset (buffer,0 ,sizeof (buffer));
const char * tmp="|/-\\" ;
int len=strlen (tmp);
static int cnt=0 ;
double count=cur*100 /totle;
int sum=(int )count;
int i=0 ;
for (;i<=sum;i++) {
buffer[i]=STYLE;
}
printf ("[%s][%-100s][%.1lf%%][%c]\r" ,s,buffer,count,tmp[cnt]);
fflush(stdout );
cnt++;
cnt%=len;
}
void process_v1 () {
char buffer[MAX];
memset (buffer,0 ,sizeof (buffer));
const char * tmp="|/-\\" ;
int len=strlen (tmp);
int cnt=0 ;
while (cnt<=100 ) {
printf ("[%-100s][%d%%][%c]\r" ,buffer,cnt,tmp[cnt%len]);
fflush(stdout );
usleep(50000 );
buffer[cnt]=STYLE;
cnt++;
}
printf ("\n" );
}
#include "process.h"
typedef void (*call_t ) (const char *,double ,double ) ;
void DownLoad (double totle,call_t cb) {
srand(time(NULL ));
double speed=rand()%51 ;
double cur=0 ;
while (cur<=totle) {
cb("下载中:" ,totle,cur);
if (cur>=totle)break ;
usleep(1000 );
cur+=speed;
if (cur>totle)cur=totle;
}
printf ("\n" );
}
void UPLoad (double totle,call_t cb) {
srand(time(NULL ));
double speed=rand()%51 ;
double cur=0 ;
while (cur<=totle) {
cb("上传中:" ,totle,cur);
if (cur>=totle)break ;
usleep(1000 );
cur+=speed;
if (cur>totle)cur=totle;
}
printf ("\n" );
}
int main () {
printf ("下载总量:%d\n" ,10000 );
DownLoad(10000 ,process_v2);
printf ("下载总量:%d\n" ,222222 );
DownLoad(222222 ,process_v2);
printf ("下载总量:%d\n" ,99 );
DownLoad(99 ,process_v2);
printf ("下载总量:%d\n" ,5555 );
DownLoad(5555 ,process_v2);
printf ("下载总量:%d\n" ,1 );
DownLoad(1 ,process_v2);
printf ("上传总量:%d\n" ,20000 );
UPLoad(20000 ,process_v2);
return 0 ;
}