
大家好,欢迎来到 huangjin007_ 的博客
⭐ 个人主页:huangjin007_
🔥 文章收录专栏:Linux 内功修炼手册(系统篇)
总会有一些坚持
能从冰封的土地里
培育出十万朵怒放的蔷薇
Linux 系统篇(十) ------ 第一个系统程序 - 进度条
文章目录
- [Linux 系统篇(十) ------ 第一个系统程序 - 进度条](#Linux 系统篇(十) —— 第一个系统程序 - 进度条)
-
- 一、回车与换行
-
- [1.1 老式打字机](#1.1 老式打字机)
- [1.2 计算机中的表示:`\r` 和 `\n`](#1.2 计算机中的表示:
\r和\n)
- 二、行缓冲区
-
- [2.1 三个现象对比](#2.1 三个现象对比)
- [2.2 缓冲区到底是啥?](#2.2 缓冲区到底是啥?)
- 三、练手程序:一个简单倒计时
- 四、进度条项目:从朴素版本到工程化实现
-
- [4.1 朴素进度条 V1(原地刷新版)](#4.1 朴素进度条 V1(原地刷新版))
- [4.2 更通用的进度条:引入下载任务模拟](#4.2 更通用的进度条:引入下载任务模拟)
- [4.3 模拟下载和上传任务](#4.3 模拟下载和上传任务)
- [4.4 工程化管理:编写 Makefile](#4.4 工程化管理:编写 Makefile)
- 五、完整代码汇总
- 结语:
一、回车与换行
在正式开始写进度条之前,有一个绕不开的基础概念需要先讲清楚------回车(Carriage Return)和换行(Line Feed)。
1.1 老式打字机
故事要从很久以前的机械打字机说起。在那个年代,打字机上有一个可以左右移动的"字车",上面装着纸张。当你敲击按键时,字车会把纸张上的墨带打到纸上,留下字符。

- 回车 :打完一行字之后,你需要把字车推回到最左边,这就是"回车"。它对应一个动作:把打印头移动到当前行的行首。
- 换行 :如果你想开始写下一行,就需要把纸张向上卷起一行,这个动作叫做"换行"。它对应:把纸张向上移动一行。
在机械打字机上,这两个动作是分开操作的。先回车,再换行,才能从一行的末尾顺利移动到下一行的开头。
1.2 计算机中的表示:\r 和 \n
后来,这些概念被带到了计算机和电传打字机中,并成为了字符编码的一部分。在 C 语言中,我们用转义字符来表示它们:
\r------ 代表回车 ,功能是把光标移动到当前行的行首。\n------ 代表换行 ,功能是把光标移动到下一行(列位置不变)。
在 Linux 环境下编程时,如果你单独使用 \r,光标就会回到当前行的开头,但不会向下换行。这恰恰是实现进度条动态刷新的关键------我们可以反复在同一行的开头打印新的内容,从而覆盖掉旧内容,给用户造成一种"原地更新"的视觉感受。
二、行缓冲区
看下面三段代码,猜猜它们的运行现象有什么不同。
2.1 三个现象对比
代码一:带 \n 的 printf
c
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("hello Linux!\n");
sleep(3);
return 0;
}
运行结果:程序一开始就打印出 hello Linux!,然后等待 3 秒,程序结束。
代码二:没有 \n 的 printf
c
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("hello Linux!");
sleep(3);
return 0;
}
运行结果:程序运行后,屏幕上什么也没有 ,一片空白,等待 3 秒后,在程序即将退出时,才突然弹出 hello Linux!。
为什么会这样?难道 printf 没有执行吗?当然不是,printf 已经执行了,但输出的内容并没有立刻显示到屏幕上,而是被放到了一个叫"缓冲区"的地方。
代码三:不带 \n,但手动刷新缓冲区
c
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("hello Linux!");
fflush(stdout); // 强制刷新标准输出缓冲区
sleep(3);
return 0;
}
运行结果:程序一开始就打印出 hello Linux!,然后才等待 3 秒。
2.2 缓冲区到底是啥?
简单来说,缓冲区(Buffer)是 C 语言标准库在内存中开辟的一块临时空间,用来暂存待输出的数据。
为什么要有缓冲区?
因为直接操作硬件(比如显示器、磁盘)是非常慢的。如果每次调用 printf 都立刻去写硬件,程序的性能会非常差。标准库的做法是:先把你要输出的数据收集起来,等攒够一批,或者遇到某些特定条件时,再一次性写出去。
对于标准输出 stdout,它的缓冲模式通常是行缓冲。也就是说,遇到以下几种情况时,缓冲区才会被"刷新"(真正写到屏幕):
- 缓冲区满了。
- 遇到换行符
\n(行缓冲的核心特征)。- 程序正常结束 (
return或exit),系统会刷新所有缓冲区。- 手动调用
fflush(stdout)强制刷新。- 等待输入时 (比如
scanf有时也会触发刷新,但这取决于实现,不绝对)。
回到代码二的例子:printf("hello Linux!"); 执行后,字符串被写入了输出缓冲区,但因为没有 \n,缓冲区没满,程序也没结束,所以它就一直待在内存里。紧接着 sleep(3) 让程序暂停了 3 秒,这期间屏幕当然是一片空白。直到 main 函数执行 return 0 准备退出时,系统才会自动清理并刷新缓冲区,于是字符串才被打印出来。
代码一因为带了 \n,printf 遇到换行符,立即触发刷新,所以字符串直接显示。代码三则是通过 fflush(stdout) 人工干预,强制把缓冲区的数据推送到终端。
三、练手程序:一个简单倒计时
有了 \r 和 fflush 的知识,我们可以先写一个小例子巩固一下------一个在终端原地倒计时的程序。
c
#include <stdio.h>
#include <unistd.h>
int main()
{
int i = 10;
while(i >= 0)
{
printf("%-2d\r", i); // 用 \r 让光标回到行首
fflush(stdout); // 立刻刷新,否则看不到变化
i--;
sleep(1); // 每秒更新一次
}
printf("\n"); // 倒计时结束后换行,让命令行提示符正常显示
return 0;
}
解析:
"%-2d":格式化打印一个左对齐、宽度为 2 的整数。-表示左对齐,2表示至少占两位。这样当数字从10变成9时,9后面会多一个空格,正好把原来10中个位的0覆盖掉,不会留下残影。"\r":打印完数字后,光标回到行首,而不是换到下一行。这样下一次printf就会从行首开始打印,覆盖掉上一次的数字。fflush(stdout):由于printf的格式串中没有\n,行缓冲不会自动刷新,所以必须手动强制刷新,否则屏幕上始终不会有任何变化。sleep(1):让程序暂停 1 秒,模拟倒计时的间隔。- 循环结束后单独打印一个
"\n":倒计时完成时,把光标移到下一行,这样 shell 提示符会另起一行,而不是直接黏在0后面。
运行效果:你会看到屏幕上数字从 10 逐步递减到 0,全程只在同一行原地变化,不会刷出一屏新行。
四、进度条项目:从朴素版本到工程化实现
掌握了倒计时的原理,进度条的实现思路就非常清晰了:我们也是用 \r 反复覆盖同一行,只是这次显示的内容不是数字,而是一个由字符拼接而成的进度条(比如 [### ][50%]),并且还需要加一点小细节。
下面我们从最简单的版本开始,逐步演进到模块化、带回调函数、支持模拟下载和上传的完整工程。
4.1 朴素进度条 V1(原地刷新版)
我们先写一个最基本的进度条函数,它从 0% 跑到 100%,每次刷新一行。
c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#define NUM 101 // 进度条总格数 + 1 (用于字符串结尾)
#define STYLE '=' // 进度条填充字符
void process()
{
char bar[NUM];
memset(bar, 0, sizeof(bar)); // 全部初始化为 '\0'
const char* label = "|/-\\"; // 旋转动画的字符序列
int len = strlen(label);
int count = 0;
while(count <= 100)
{
// 用 %-100s 保证进度条字符串宽度固定为 100,不足的部分用空格填充
printf("[%-100s][%d%%][%c]\r", bar, count, label[count % len]);
fflush(stdout);
bar[count] = STYLE; // 每次往 bar 里多填一个 '='
usleep(30000); // 休眠 30 毫秒,控制进度速度
count++;
}
printf("\ndownload successfully!\n");
}
int main()
{
process();
return 0;
}
代码详解:
bar[NUM]是一个大小为 101 的字符数组,前 100 个位置用来放进度条字符=,最后一个位置始终为'\0'(由memset保证)。进度从 0% 开始时,bar里全是空字符,printf用%-100s打印时会输出 100 个空格(因为空字符不会显示,但宽度占用了)。随着循环递增,bar[count]被赋值为=,进度条逐渐被填满。"%-100s":-表示左对齐,100表示字段最小宽度为 100。如果字符串长度不足 100,剩余位置会用空格填充。label是一个包含 4 个字符的字符串:'|'、'/'、'-'、'\'(注意反斜杠在字符串中需要转义,所以写成'\\')。每次循环取label[count % len],就能顺序循环播放这四个字符,产生一个旋转小棒的效果,用来提醒用户下载正在进行中。usleep(30000):让程序暂停 30000 微秒,也就是 30 毫秒。因为sleep只能以秒为单位,这里用usleep实现更精细的时间控制(需要#include <unistd.h>)。
运行这个程序,你会看到一个从 [ ] 逐渐变为 [====================================] 的进度条,后面跟着百分比和一个不停旋转的字符。

4.2 更通用的进度条:引入下载任务模拟
上面的 process() 函数虽然能跑,但它是"自娱自乐"型------进度条自己控制自己的进度,没有和外部任何任务挂钩。实际应用中,进度条应该反映某个真实任务的完成情况,比如下载文件、处理数据等。所以我们把进度条的更新逻辑分离出来,做成一个可以接收"当前进度"参数的函数。
process.h(头文件声明)
c
#pragma once
#include <stdio.h>
void process();
void Flushprocess(double total, double current);
这里我们声明了两个函数:原有的 process 用于测试,新函数 Flushprocess 用于接收总任务量和当前进度,来动态刷新进度条。
process.c(实现)
c
#include "process.h"
#include <string.h>
#include <unistd.h>
#define NUM 101
#define STYLE '='
void Flushprocess(double total, double current)
{
char bar[NUM];
memset(bar, 0, sizeof(bar));
const char* label = "|/-\\";
int len = strlen(label);
static int count = 0; // 这里把 count 声明为静态变量,用来控制旋转动画
// 计算已经填充的格子数:current / total * 100 取整
int num = (int)(current * 100 / total);
// 填充 bar
for(int i = 0; i < num; i++)
{
bar[i] = STYLE;
}
// 百分比
double rate = current / total;
// 旋转动画的字符索引
count %= len; // 确保不越界
printf("[%-100s][%.1f%%][%c]\r", bar, rate * 100, label[count]);
count++;
fflush(stdout);
}
几个关键改动:
- 函数参数
total和current分别表示任务总量和当前完成量,单位可以根据实际场景定义,这里我们内部统一用double保持通用性。static int count:在Flushprocess被多次调用时,count变量不会每次都被重新初始化为 0,而是保留上一次的值。这样旋转动画才能连贯地播放下去。如果不加static,每次调用count都会从 0 开始,动画就跳来跳去。- 进度计算:
num = (int)(current * 100 / total);算出当前进度占 100 格中的几格;rate = current / total算出百分比小数。- 输出格式
[%.1f%%]:%.1f保留一位小数,%%在printf中用来输出一个百分号本身。
4.3 模拟下载和上传任务
现在 Flushprocess 准备好了,我们需要写一个"任务函数"来模拟实际工作,并不断调用 Flushprocess 来更新进度条。
main.c(使用回调函数解耦任务与进度显示)
c
#include "process.h"
#include <unistd.h>
#include <stdio.h>
// 定义一个回调函数类型:接收总量和当前量,无返回值
typedef void (*callback_t)(double total, double current);
double total = 1024.0; // 模拟总数据量 1024MB
double speed = 1.0; // 每次循环增加 1MB
void Download(callback_t cb)
{
double current = 0.0;
while(current <= total)
{
cb(total, current); // 调用回调更新进度条
usleep(3000); // 模拟网络延迟
current += speed; // 数据量增加
}
printf("\ndownload %.2lfMB Done\n", current);
}
void Upload(callback_t cb)
{
double current = 0.0;
while(current <= total)
{
cb(total, current);
usleep(3000);
current += speed;
}
printf("\nupload %.2lfMB Done\n", current);
}
int main()
{
Download(Flushprocess); // 把 Flushprocess 作为回调传入
Upload(Flushprocess);
return 0;
}

设计思路解析:
- 回调函数 :
callback_t是一个函数指针类型,指向一个形如void func(double, double)的函数。Download和Upload函数接受这样一个函数指针cb,在循环体内调用cb(total, current)来通知"外界"当前进度是多少。至于这个"外界"怎么处理进度(比如打印进度条、记录日志、还是显示在图形界面),完全由调用者传入的具体函数决定。- 模拟任务 :
total = 1024.0,speed = 1.0,每次循环current += speed,所以从 0MB 跑到 1024MB 需要 1024 次循环。usleep(3000)让每次循环暂停 3 毫秒,整个模拟过程约 3 秒,肉眼能清晰看到进度条的变化。main函数 :直接调用Download(Flushprocess),就把进度条显示函数"注入"到了下载任务中。上传任务同理。如果想换一种进度显示方式(比如只打印百分比),只需换一个回调函数即可,完全不用修改Download和Upload的代码。
4.4 工程化管理:编写 Makefile
当项目包含多个源文件时,每次手动用 gcc 编译会很麻烦。我们写一个简单的 Makefile 来自动化构建。
makefile
SRC = $(wildcard *.c) # 获取当前目录所有 .c 文件
OBJ = $(SRC:%.c=%.o) # 将 .c 后缀替换成 .o
BIN = processbar # 最终生成的可执行文件名
$(BIN): $(OBJ)
gcc -o $@ $^ # 链接所有 .o 文件生成可执行程序
%.o: %.c
gcc -c $< # 编译每个 .c 为 .o
.PHONY: clean
clean:
rm -f $(BIN) $(OBJ) # 清理编译生成的文件
Makefile 关键点:
wildcard函数:自动匹配通配符,获取文件列表。$(SRC:%.c=%.o):模式替换,把main.c、process.c变成main.o、process.o。$@代表目标文件,$^代表所有依赖文件,$<代表第一个依赖文件。这些是 Makefile 的自动变量,可以简化书写。.PHONY: clean:声明clean是一个伪目标,即使当前目录存在一个名为clean的文件,也依然执行 clean 命令。
使用方法:在终端进入项目目录,执行 make 即可编译生成 processbar。执行 ./processbar 运行程序。想清理的话,make clean。
五、完整代码汇总
process.h
c
#pragma once
#include <stdio.h>
void process();
void Flushprocess(double total, double current);
process.c
c
#include "process.h"
#include <string.h>
#include <unistd.h>
#define NUM 101
#define STYLE '='
void Flushprocess(double total, double current)
{
char bar[NUM];
memset(bar, 0, sizeof(bar));
const char* label = "|/-\\";
int len = strlen(label);
static int count = 0;
int num = (int)(current * 100 / total);
for(int i = 0; i < num; i++)
{
bar[i] = STYLE;
}
double rate = current / total;
count %= len;
printf("[%-100s][%.1f%%][%c]\r", bar, rate * 100, label[count]);
count++;
fflush(stdout);
}
void process()
{
char bar[NUM];
memset(bar, 0, sizeof(bar));
const char* label = "|/-\\";
int len = strlen(label);
int count = 0;
while(count <= 100)
{
printf("[%-100s][%d%%][%c]\r", bar, count, label[count % len]);
fflush(stdout);
bar[count] = STYLE;
usleep(30000);
count++;
}
printf("\ndownload successfully!\n");
}
main.c
c
#include "process.h"
#include <unistd.h>
#include <stdio.h>
typedef void (*callback_t)(double total, double current);
double total = 1024.0;
double speed = 1.0;
void Download(callback_t cb)
{
double current = 0.0;
while(current <= total)
{
cb(total, current);
usleep(3000);
current += speed;
}
printf("\ndownload %.2lfMB Done\n", current);
}
void Upload(callback_t cb)
{
double current = 0.0;
while(current <= total)
{
cb(total, current);
usleep(3000);
current += speed;
}
printf("\nupload %.2lfMB Done\n", current);
}
int main()
{
Download(Flushprocess);
Upload(Flushprocess);
return 0;
}
Makefile
makefile
SRC = $(wildcard *.c)
OBJ = $(SRC:%.c=%.o)
BIN = processbar
$(BIN): $(OBJ)
gcc -o $@ $^
%.o: %.c
gcc -c $<
.PHONY: clean
clean:
rm -f $(BIN) $(OBJ)
结语:
今天的内容到这里就结束了,希望你能有所收获~
干货整理到手抖,觉得有用的话,赏个三连回回血?__(:ᗤ」ㄥ)_ _