
前面我们学习了 vim 编辑、gcc 编译、Makefile 构建,掌握了 Linux 下 C 程序开发的完整工具链。本篇我们用一个非常经典的实战项目「终端进度条」,把前面的知识全部串起来用一遍。
平时下载软件、安装包时,我们总能看到一行不断前进的进度条、百分比、还有转圈的加载动画。这么一个小小的进度条是怎么在终端里实现的?背后涉及两个非常重要的底层概念:回车与换行、行缓冲区。
本篇我们从底层原理讲起,一步步写出两个版本的进度条,最后做成标准的多文件工程,配合 Makefile 一键构建,带你完整体验从原理到工程落地的全过程。
一、前置知识一:回车与换行,傻傻分不清楚?
1.1 从老式打字机说起
「回车」和「换行」是两个从老式打字机时代延续下来的概念:
回车(Carriage Return):把打字的滑架(光标)推回到一行的最开头
换行(Line Feed):把纸张往上推一行,光标移到下一行
两个动作合在一起,才是我们平时说的「换到下一行开头」。
1.2 C 语言中的两个转义字符
对应到 C 语言里:
| 转义字符 | 含义 | 作用 |
|---|---|---|
| \r | 回车 | 光标回到当前行的行首,不换行 |
| \n | 换行 | 光标移到下一行,同时回到行首(现代系统一般自带回车效果) |
| 关键区别: | ||
| \n 会换行,新内容出现在下一行 | ||
| \r 不换行,新内容会覆盖当前行原来的内容 | ||
| 我们要实现「同一行不断更新的进度条」,核心靠的就是 \r:每次打印都回到行首,用新的进度覆盖旧的进度,视觉上就形成了动画效果。 |
二、前置知识二:行缓冲区 —— 为什么我的 printf 没有立刻输出?
2.1 一个反直觉的现象
先看三段代码,猜一猜各自的输出现象。
代码 1:带换行符
#include <stdio.h>
#include <unistd.h>
int main() {
printf("hello bite!\n");
sleep(3);
return 0;
}
现象:先打印 hello bite!,然后程序休眠 3 秒,最后退出。符合直觉。
代码 2:不带换行符
#include <stdio.h>
#include <unistd.h>
int main() {
printf("hello bite!");
sleep(3);
return 0;
}
现象:先休眠 3 秒,然后才打印 hello bite!,程序退出。和直觉完全相反。
2.2 行缓冲区机制
为什么会这样?因为 printf 输出的内容并不会直接写到显示器上,而是先放到行缓冲区里。
满足以下条件之一,缓冲区才会被「刷新」,内容才真正显示到屏幕上:
遇到换行符 \n
程序正常结束(main 返回)
缓冲区被写满了
手动强制刷新
代码 2 里没有 \n,程序又在 sleep,缓冲区没满也没结束,所以内容一直卡在缓冲区里看不到。等 sleep 完程序退出了,才一次性刷新出来。
2.3 手动刷新:fflush
我们可以用 fflush(stdout) 强制刷新标准输出缓冲区,让内容立刻显示。
代码 3:手动刷新
#include <stdio.h>
#include <unistd.h>
int main() {
printf("hello bite!");
fflush(stdout); // 强制刷新缓冲区,立刻显示
sleep(3);
return 0;
}
现象:先打印 hello bite!,再休眠 3 秒。和代码 1 效果一致。
进度条为什么需要 fflush?
进度条用 \r 回到行首覆盖打印,全程没有 \n。如果不手动刷新,内容会一直卡在缓冲区里,直到程序结束才一股脑显示出来,根本看不到动画效果。所以每次打印完进度都要 fflush(stdout) 一下。
三、热身练习:10 秒倒计时
搞懂了 \r 和 fflush,我们先做个简单的热身:在同一行显示倒计时,从 10 倒数到 0。
代码实现
#include <stdio.h>
#include <unistd.h>
int main() {
int i = 10;
while (i >= 0) {
// \r 回到行首,覆盖打印;%-2d 左对齐占2位,防止数字变短后残留字符
printf("%-2d\r", i);
fflush(stdout); // 强制刷新,立刻显示
i--;
sleep(1); // 每秒减1
}
printf("\n"); // 结束后换个行,别让提示符粘在同一行
return 0;
}
几个细节说明
为什么用 %-2d 左对齐?
数字从 10(两位)变成 9(一位)时,如果不占固定宽度,行尾会残留上一次的 0,变成 90。左对齐占两位,短数字补空格,就不会有残留。
为什么最后加 printf(“\n”)?
程序结束时光标还在进度条那一行,shell 提示符会直接接在后面,非常难看。最后打印一个换行,让提示符出现在下一行。
四、进度条 Version1:基础完整版
热身完毕,我们正式写第一个完整进度条。
4.1 实现思路
准备一个字符数组当「进度条本体」,初始全是 \0
循环从 0 到 100,每次往数组里填一个 = 字符
用 \r 回到行首,重新打印整条进度条、百分比、旋转光标
每次打印后 fflush 刷新,休眠一小段时间
循环结束后换行
4.2 旋转光标是怎么实现的?
很简单,用一个字符数组存四个状态 | / - \,然后用计数器对 4 取模,依次循环取字符,视觉上就是在转圈。
4.3 完整代码
// process.c v1版本
#include "process.h"
#include <string.h>
#include <unistd.h>
#define NUM 101 // 进度条总长度+1,留位置放'\0'
#define STYLE '=' // 进度条填充字符
void process_v1() {
char buffer[NUM];
memset(buffer, 0, sizeof(buffer)); // 初始化为全0
const char* lable = "|/-\\"; // 旋转光标的四个状态
int len = strlen(lable);
int cnt = 0;
while (cnt <= 100) {
// [%-100s] 左对齐占100位的进度条
// [%d%%] 百分比,两个%输出一个%
// [%c] 旋转光标
// \r 回到行首覆盖
printf("[%-100s][%d%%][%c]\r", buffer, cnt, lable[cnt % len]);
fflush(stdout);
buffer[cnt] = STYLE; // 进度前进一格
cnt++;
usleep(50000); // 休眠50毫秒,控制速度
}
printf("\n"); // 结束换行
}
五、进度条 Version2:工程实用升级版
5.1 v1 有什么问题?
v1 版本是自己在函数内部循环、自己控制速度,只能用来演示动画,没法用在真实项目里。
真实场景是什么样的?
比如下载文件:下载逻辑自己在跑,下了多少字节是业务逻辑算出来的。进度条函数只负责「你告诉我当前下了多少、总共多少,我帮你渲染成进度条」。
5.2 v2 升级点
参数化:函数接收 total(总量)和 current(当前量),由调用方控制进度
小数百分比:支持显示一位小数,更精确
状态保持:旋转光标的计数用 static 静态变量,每次调用自动往后走一格
解耦:渲染和业务逻辑分离,任何需要进度展示的场景都能调用
5.3 完整代码
// process.c v2版本
void FlushProcess(double total, double current) {
char buffer[NUM];
memset(buffer, 0, sizeof(buffer));
const char* lable = "|/-\\";
int len = strlen(lable);
static int cnt = 0; // 静态变量,记录旋转光标位置,函数调用间保持值
// 计算当前进度对应的格数
int num = (int)(current * 100 / total);
// 填充进度条
for (int i = 0; i < num; i++) {
buffer[i] = STYLE;
}
// 计算百分比,保留一位小数
double rate = current / total;
// 打印:进度条 + 百分比 + 旋转光标
printf("[%-100s][%.1f%%][%c]\r", buffer, rate * 100, lable[cnt % len]);
fflush(stdout);
cnt++;
}
5.4 真实场景模拟:下载进度
我们写一个模拟下载的函数,循环调用 v2 版本进度条:
// main.c
#include "process.h"
#include <stdio.h>
#include <unistd.h>
double total = 1024.0; // 总大小 1024MB
double speed = 1.0; // 下载速度 1MB/次
void DownLoad() {
double current = 0;
while (current <= total) {
FlushProcess(total, current); // 更新进度条
usleep(3000); // 模拟下载耗时
current += speed;
}
printf("\ndownload %.2fMB Done\n", current);
}
int main() {
DownLoad();
DownLoad();
DownLoad();
return 0;
}
每次调用 DownLoad 都会跑一遍完整的下载进度,非常接近真实软件下载的效果。
六、多文件工程组织
我们把项目拆成标准的三分结构:头文件 + 实现文件 + 主函数,配合 Makefile 一键构建。
6.1 头文件 process.h
放函数声明,防止重复包含。
#pragma once
#include <stdio.h>
// v1 演示版:自动循环播放进度条
void process_v1();
// v2 实用版:传入总量和当前量,由调用方控制
void FlushProcess(double total, double current);
6.2 实现文件 process.c
放两个函数的具体实现,前面已经写过完整代码。
#include "process.h"
#include <string.h>
#include <unistd.h>
#define NUM 101
#define STYLE '='
void process_v1() {
// ... v1 实现代码 ...
}
void FlushProcess(double total, double current) {
// ... v2 实现代码 ...
}
6.3 主函数 main.c
程序入口,调用演示。
#include "process.h"
#include <stdio.h>
#include <unistd.h>
double total = 1024.0;
double speed = 1.0;
void DownLoad() {
double current = 0;
while (current <= total) {
FlushProcess(total, current);
usleep(3000);
current += speed;
}
printf("\ndownload %.2fMB Done\n", current);
}
int main() {
// 可以先跑 v1 演示
// process_v1();
// 再跑 v2 真实场景
DownLoad();
DownLoad();
DownLoad();
return 0;
}
6.4 Makefile
自动化构建,一个 make 命令全部搞定。
SRC=$(wildcard *.c)
OBJ=$(SRC:.c=.o)
BIN=processbar
$(BIN):$(OBJ)
gcc -o $@ $^
%.o:%.c
gcc -c $<
.PHONY: clean
clean:
rm -f $(OBJ) $(BIN)
6.5 编译运行
# 编译
make
# 运行
./processbar
运行效果:终端同一行上,进度条不断前进,百分比不断增加,右侧光标不断旋转,完成一行后提示下载完成,继续下一个。
七、本篇总结
一个看似简单的终端进度条,背后藏着不少底层知识:
\r 回车:回到行首覆盖打印,是实现单行动画的核心
行缓冲区:没有 \n 时内容不会立刻显示,需要 fflush(stdout) 强制刷新
格式化输出:左对齐固定宽度、百分号转义、百分比计算
工程化思想:接口与实现分离,头文件放声明、源文件放实现,主函数调用
版本迭代:从演示版到实用版,参数化、解耦,让代码能融入真实业务
到这里,Linux 基础开发工具的内容就全部落地了。从 vim 写代码、gcc 编译四阶段、Makefile 自动化构建,到今天的完整项目实战,整套开发流程你已经全部跑通了。

:开发工具 Chapter4 —— 手把手实现终端进度条,从回车换行到多文件工程化&spm=1001.2101.3001.5002&articleId=163505262&d=1&t=3&u=c95572bdf9df49fabe0eca2c886e4136)
6951

被折叠的 条评论
为什么被折叠?



