

🥰个人专栏: 《C++知识分享》 《Linux 入门到实践:零基础也能懂》
🌠 有善始者实繁,能克终者盖寡
索引与导读
- 前言
-
- 一、核心知识必备
-
- 1)行缓冲与缓冲区刷新运行机制
- [2)转义字符 \r (回车) 与 \n (换行) 的区别](#2)转义字符 \r (回车) 与 \n (换行) 的区别)
-
- [1)实验一:对比 \n 和 \r 的效果](#1)实验一:对比 \n 和 \r 的效果)
- 2)实验二:结合进度条的逻辑
- 3)进度条的核心构成元素详解
- 二、实践开发
-
- 1)基础框架(实际应用较少)
-
- [1.1)头文件 process.h](#1.1)头文件 process.h)
- [1.2)源文件 process.c](#1.2)源文件 process.c)
- [1.3)测试文件 main.c](#1.3)测试文件 main.c)
- [1.4)💡 编译与运行方法](#1.4)💡 编译与运行方法)
- 2)关于Makefile自动化构建流程的编写
- 三、操作实践与效果展示
- [💻结尾--- 核心连接协议](#💻结尾— 核心连接协议)
前言
在 Linux 系统编程领域,进度条的实现不仅仅是简单的字符串拼接,而是对标准输出的缓冲机制与终端控制序列的深度解构 本篇将从底层逻辑出发,带你拆解这一交互组件的实现原理:从利用行首回车(\r)实现原地覆盖,到通过 flush 手动强制刷新行缓冲,再至利用标准输入输出流的原子性控制实现平滑更新 我们将通过分阶段构建的方式,由浅入深,不仅实现稳定的更新动效,更会深入探讨其在多进程/多线程任务监控中的工程化应用,旨在助你构建出既符合 Linux 编程规范、又具备生产环境性能与高可维护性的终端交互工具
一、核心知识必备
1)行缓冲与缓冲区刷新运行机制
C 语言的printf函数默认是行缓冲 ------这意味着 printf 产生的内容会先存放在内存缓冲区中
-
规则:缓冲区只有在以下三种情况发生时,才会把内容"倒"进终端:
-
遇到换行符
\n:这意味着一行写完了,可以刷新输出 -
缓冲区满了:没地方存了,被迫刷新
-
程序结束:程序要退出了,必须把缓存里的内容清理干净
-
错误示例:
cpp
#include <stdio.h>
int main()
{
printf("hello Lotso!");
sleep(3);
return 0;
}
修正:
cpp
#include <stdio.h>
int main()
{
printf("hello bite!");
fflush(stdout);
sleep(3);
return 0;
}
2)转义字符 \r (回车) 与 \n (换行) 的区别
-
换行(
\n) :光标移动到下一行的行首,但不会回到当前行开头;我们日常使用它的时候其实是回车 + 换行的作用(\r\n) -
回车(
\r):光标回到当前行的行首,但不会移动到下一行 -
进度条的核心是在同一行反复覆盖刷新 ,因此必须用
\r让光标回到行首,再重新打印新的进度信息
1)实验一:对比 \n 和 \r 的效果
你可以复制这两段代码到 Linux 中运行,观察结果
c
#include <stdio.h>
#include <unistd.h>
int main() {
for (int i = 0; i <= 5; i++) {
printf("Current count: %d\n", i); // 注意这里用的是 \n
sleep(1);
}
return 0;
}
- 运行结果:你会看到数字一行接一行地往下打印,屏幕被占满了
plaintext
Current count: 0
Current count: 1
Current count: 2
...
c
#include <stdio.h>
#include <unistd.h>
int main() {
for (int i = 0; i <= 5; i++) {
printf("\rCurrent count: %d", i); // 注意这里用的是 \r
fflush(stdout); // 强制刷新缓冲区,否则看不到动效
sleep(1);
}
printf("\n"); // 循环结束后补一个换行,避免后续的命令行提示符接在后面
return 0;
}
- 运行结果 :数字
0到5会在同一个位置跳动,屏幕上永远只显示一行字 这就是"覆盖刷新"
2)实验二:结合进度条的逻辑
c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main() {
int i = 0;
char bar[102];
memset(bar, 0, sizeof(bar)); // 初始化数组
while (i <= 100) {
// 打印:[进度条部分] 百分比%
printf("\r[%-100s] %d%%", bar, i);
fflush(stdout);
bar[i] = '#'; // 每次循环给进度条加一个 '#'
i++;
usleep(50000); // 暂停 0.05 秒,制造动画感
}
printf("\n"); // 结束后换行,保持整洁
return 0;
}
1. \r:将光标强行拉回每一行的最左侧
2. %-100s :这保证了进度条的"框"始终是 100 个字符宽 无论进度条是 1% 还是 100%,它的左括号 [ 和右括号 ] 始终站在屏幕的同一个位置,不会左右晃动
3. fflush(stdout):如果没有它,程序可能会一次性等到循环结束才给你刷出结果,你就看不见过程了
运行时的动态表现:
程序开始执行后,你会看到屏幕上出现了一个由方括号包围的进度区域:
- 动画过程 :
#字符会从左向右以每0.05秒一个的速度填充,右侧的数字从0%实时增加到100% - 原地覆盖 :因为使用了
\r和fflush(stdout),屏幕上不会出现多行文字,进度条始终像在同一个位置"变长"一样
当循环结束后,程序执行 printf("\n"),光标会换行。此时终端屏幕的最后一行会定格为:
Plaintext
[####################################################################################################] 100%
user@linux:~$
3)进度条的核心构成元素详解
一个完整的动态进度条通常包含 3 部分:
-
进度条主体 :用
=等字符填充,直观显示完成比例 -
百分比 :显示完成进度(
0%~100%) -
动态光标 :用
| / - \循环切换,提示程序正在运行 -
附加信息:如当前进度/总进度、传输速度,提升实用性
二、实践开发
我们先打造出一个基础框架,后面慢慢优化出`"彩色区分 + 速度显示 + 多场景适配" 的进度条
从头文件、实现文件、主函数三部分来实现
1)基础框架(实际应用较少)
1.1)头文件 process.h
头文件主要用来进行函数声明,防止多重包含
c
#ifndef _PROCESS_H_
#define _PROCESS_H_
// 回调函数指针:根据当前百分比,返回已下载的字节数
typedef double (*callback_t)(double progress);
// 核心通用进度条接口
void process_ui(double total_size, callback_t cb);
#endif
1.2)源文件 process.c
c
#include "process.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/time.h>
#define NUM 51 // 进度条主体长度
#define STYLE '#' // 填充字符
// ANSI 颜色控制宏
#define NONE "\033[m"
#define BLUE "\033[0;32;34m"
#define CYAN "\033[0;36m"
#define LIGHT_GREEN "\033[1;32m"
#define PURPLE "\033[0;35m"
// 自动转换速度单位 (B/s, KB/s, MB/s)
void format_speed(double speed_bytes_per_sec, char* speed_buf) {
if (speed_bytes_per_sec >= 1024 * 1024) {
sprintf(speed_buf, "%.2f MB/s", speed_bytes_per_sec / (1024 * 1024));
} else if (speed_bytes_per_sec >= 1024) {
sprintf(speed_buf, "%.2f KB/s", speed_bytes_per_sec / 1024);
} else {
sprintf(speed_buf, "%.2f B/s", speed_bytes_per_sec);
}
}
void process_ui(double total_size, callback_t cb) {
char buffer[NUM];
memset(buffer, '\0', sizeof(buffer));
const char* label = "|/-\\";
int len = strlen(label);
struct timeval start_time, current_time;
gettimeofday(&start_time, NULL); // 记录启动时间
double rate = 0.0;
int rotate_cnt = 0;
while (rate <= 100.0) {
// 计算流逝时间 (秒)
gettimeofday(¤t_time, NULL);
double elapsed = (current_time.tv_sec - start_time.tv_sec) +
(current_time.tv_usec - start_time.tv_usec) / 1000000.0;
// 计算实时速度
char speed_buf[32] = "0.00 B/s";
if (elapsed > 0.001) {
double current_bytes = cb(rate); // 通过回调获取当前数据量
format_speed(current_bytes / elapsed, speed_buf);
}
// 构造进度条字符串
int fill_num = (int)(rate / 2); // 100% 对应 50 个 '#'
for(int i = 0; i < fill_num; i++) buffer[i] = STYLE;
// 彩色格式化打印,\r 实现原地刷新
printf(BLUE"["NONE"%-50s"BLUE"]"CYAN"[%6.2f%%]"LIGHT_GREEN"[%s]"PURPLE"[%c]\r"NONE,
buffer, rate, speed_buf, label[rotate_cnt % len]);
fflush(stdout); // 强刷标准输出缓冲区
usleep(40000); // 模拟耗时 (40ms)
rate += 1.0;
rotate_cnt++;
}
printf("\n");
}
1.3)测试文件 main.c
c
#include "process.h"
#include <stdio.h>
static double g_total_size = 0.0;
// 场景回调函数:计算当前百分比对应的已完成字节数
double scene_cb(double progress) {
return g_total_size * (progress / 100.0);
}
int main() {
// 场景 1:5G 大文件下载 (MB/s 级别)
g_total_size = 512 * 1024 * 1024; // 512 MB
printf("\n==== [场景 1] 正在下载大文件 ====\n");
process_ui(g_total_size, scene_cb);
// 场景 2:IoT 慢速同步 (KB/s 级别)
g_total_size = 2.5 * 1024 * 1024; // 2.5 MB
printf("\n==== [场景 2] 正在同步弱网数据 ====\n");
process_ui(g_total_size, scene_cb);
return 0;
}
1.4)💡 编译与运行方法
bash
gcc main.c process.c -o progress_bar
./progress_bar
2)关于Makefile自动化构建流程的编写
我们在之前讲过 Makefile 的编写策略,不懂的可以看下面这篇文章,文章结尾有工程化的Makefile
三、操作实践与效果展示

💻结尾--- 核心连接协议
警告: 🌠🌠正在接入底层技术矩阵。如果你已成功破解学习中的逻辑断层,请执行以下指令序列以同步数据:🌠🌠
【📡】 建立深度链接: 关注本终端。在赛博丛林中深耕底层架构,从原始代码到进阶协议,同步见证每一次系统升级。
【⚡】 能量过载分发: 执行点赞操作。通过高带宽分发,让优质模组在信息流中高亮显示,赋予知识跨维度的传播力。
【💾】 离线缓存核心: 将本页加入收藏。把这些高频实战逻辑存入你的离线存储器,在遭遇系统崩溃或需要离线检索时,实现瞬时读取。
【💬】 协议加密解密: 在评论区留下你的散列码。分享你曾遭遇的代码冲突或系统漏洞(那些年踩过的坑),通过交互式编译共同绕过技术陷阱。
【🛰️】 信号频率投票: 通过投票发射你的选择。你的每一次点击都在重新定义矩阵的进化方向,决定下一个被全量拆解的技术节点。

