Linux 系统编程实战:从零手写一个高性能终端进度条

目录

  • 前言
  • [一、Linux 下的第一个小应用:进度条项目背景与规划](#一、Linux 下的第一个小应用:进度条项目背景与规划)
    • [1.1 为什么我们要手搓一个进度条](#1.1 为什么我们要手搓一个进度条)
    • [1.2 两个容易被忽略的储备知识](#1.2 两个容易被忽略的储备知识)
      • [1.2.1 回车(\r)与换行(\n)的区别](#1.2.1 回车(\r)与换行(\n)的区别)
      • [1.2.2 缓冲区机制与强制刷新](#1.2.2 缓冲区机制与强制刷新)
  • 二、倒计时功能的实现与原理剖析
    • [2.1 单字符位置的动态刷新原理](#2.1 单字符位置的动态刷新原理)
    • [2.2 多位数倒计时的"坑":残留字符问题](#2.2 多位数倒计时的“坑”:残留字符问题)
    • [2.3 解决方案:格式化输出与对齐控制](#2.3 解决方案:格式化输出与对齐控制)
      • [2.3.1 粗暴法:手动补空格](#2.3.1 粗暴法:手动补空格)
      • [2.3.2 优雅法:使用 `%2d` 定宽输出](#2.3.2 优雅法:使用 %2d 定宽输出)
      • [2.3.3 终极方案:左对齐标志 `-`](#2.3.3 终极方案:左对齐标志 -)
  • 三、进度条的设计与实现
    • [3.1 文章目标与系统级编程思维](#3.1 文章目标与系统级编程思维)
    • [3.2 进度条的功能设计与视觉规范](#3.2 进度条的功能设计与视觉规范)
      • [3.2.1 为什么需要旋转光标?](#3.2.1 为什么需要旋转光标?)
    • [3.3 工程环境搭建与代码结构](#3.3 工程环境搭建与代码结构)
      • [3.3.1 头文件与源文件的初始化](#3.3.1 头文件与源文件的初始化)
      • [3.3.2 Makefile 的编写规范](#3.3.2 Makefile 的编写规范)
    • [3.4 核心逻辑实现与时间控制](#3.4 核心逻辑实现与时间控制)
      • [3.4.1 缓冲区初始化与循环逻辑](#3.4.1 缓冲区初始化与循环逻辑)
      • [3.4.2 usleep 与时间单位详解](#3.4.2 usleep 与时间单位详解)
      • [3.4.3 调整刷新频率](#3.4.3 调整刷新频率)
    • [3.5 常见问题分析:换行与刷新](#3.5 常见问题分析:换行与刷新)
    • [3.6 进度条的"换行"灾难与缓冲区刷新机制](#3.6 进度条的“换行”灾难与缓冲区刷新机制)
      • [3.6.1 为什么进度条会换行?](#3.6.1 为什么进度条会换行?)
      • [3.6.2 去掉 `\n` 后的"假死"现象](#3.6.2 去掉 \n 后的“假死”现象)
    • [3.7 光标回退与动态覆盖原理](#3.7 光标回退与动态覆盖原理)
      • [3.7.1 为什么进度条会"向右跑"?](#3.7.1 为什么进度条会“向右跑”?)
      • [3.7.2 回车符 `\r` 的妙用](#3.7.2 回车符 \r 的妙用)
      • [3.7.3 善后处理:最后的换行](#3.7.3 善后处理:最后的换行)
    • [3.8 格式化输出与对齐陷阱](#3.8 格式化输出与对齐陷阱)
      • [3.8.1 固定宽度与左对齐](#3.8.1 固定宽度与左对齐)
      • [3.8.2 百分比的转义与定宽](#3.8.2 百分比的转义与定宽)
  • 四、进阶优化:旋转光标与防"假死"
    • [4.1 实现逻辑](#4.1 实现逻辑)
  • 五、工程化重构:回调函数与真实场景适配
    • [5.1 从"内部循环"到"外部驱动"](#5.1 从“内部循环”到“外部驱动”)
    • [5.2 引入回调函数 (Callback)](#5.2 引入回调函数 (Callback))
  • [六、printf 的彩色输出与终端美化](#六、printf 的彩色输出与终端美化)
    • [6.1 核心原理:ANSI 转义序列](#6.1 核心原理:ANSI 转义序列)
    • [6.2 常用控制参数对照表](#6.2 常用控制参数对照表)
      • [6.2.1 样式与显示属性](#6.2.1 样式与显示属性)
      • [6.2.2 前景色(字体颜色)](#6.2.2 前景色(字体颜色))
      • [6.3.3 背景色(背景填充颜色)](#6.3.3 背景色(背景填充颜色))
  • 结语

🎬 云泽Q个人主页
🔥 专栏传送入口 : 《C语言》《数据结构》《C++》《Linux》《蓝桥杯系列》《笔试算法》《AI赋能》《STM32》《Python

⛺️遇见安然遇见你,不负代码不负卿~


前言

大家好啊,我是云泽Q,欢迎阅读我的文章,一名热爱计算机技术的在校大学生,喜欢在课余时间做一些计算机技术的总结性文章,希望我的文章能为你解答困惑~

一、Linux 下的第一个小应用:进度条项目背景与规划

1.1 为什么我们要手搓一个进度条

在前面的文章,大家已经熟悉了 vim 编辑器,掌握了 gcc/g++ 编译器,学会了编写 Makefile 进行自动化构建...

既然工具链已经齐备,我们现在的目标就是赶紧把这些工具串联起来,在 Linux 环境下"手搓"出我们的第一个小应用------进度条(Progress Bar)

为什么要选进度条?主要有两个原因:

  1. 知识整合:这是一个绝佳的练习机会,能把之前零散学习的编译、构建、编辑等知识串成一条线。
  2. 工程化过渡:进度条项目,是我们从"单文件测试"向"多文件工程"过渡的最佳载体。该篇文章会直接采用多文件结构(至少两个源文件加一个头文件)

1.2 两个容易被忽略的储备知识

在动手写进度条之前,有两个关于 C 语言输出的"常识"必须先讲清楚。这两个知识点虽然基础,但极易被忽略,却是实现进度条动态刷新效果的核心基石。

1.2.1 回车(\r)与换行(\n)的区别

我们在写作文或打字时,习惯把"回车换行"当成一个动作,但在计算机底层,这其实是两回事:

  • 回车(Carriage Return, \r :光标回到当前行的开头。就像老式打字机,打字头移回最左边,但纸没有卷动。
  • 换行(Line Feed, \n :光标移动到下一行的同一列位置。纸卷动了一行,但打字头没动。

在 C/C++ 的标准输出中,\n 通常被系统解释为"回车+换行"的组合动作(即 \r\n)。

  • 如果你只想让光标回到行首而不换行(为了实现覆盖打印),必须使用 \r
  • 如果你想立即刷新输出并换行,通常使用 \nstd::endl

1.2.2 缓冲区机制与强制刷新

C 语言的标准输出(stdout)默认采用行缓冲模式。这意味着:

  • 只有遇到 \n、程序正常结束或缓冲区满时,数据才会真正从内存冲刷到显示器上。
  • 如果 printf 的内容里没有 \n,数据其实还停留在内存的缓冲区里,屏幕上什么都看不到。

这就引出了一个问题:在做进度条这种需要"原地刷新"且不带换行符的场景下,如何让用户看到实时变化?答案是使用 fflush(stdout) 强制刷新缓冲区。

c 复制代码
#include <stdio.h>
#include <unistd.h>

int main() {
    // 场景演示:没有 \n 也没有 fflush
    printf("hello yunze"); 
    sleep(1); // 此时屏幕是空的!数据在缓冲区里睡觉
    return 0; // 程序结束瞬间,缓冲区自动刷新,字才出来
}

而在下面的代码中,我们加入了 fflush

c 复制代码
#include <stdio.h>
#include <unistd.h>

int main() {
    printf("hello yunze");
    fflush(stdout); // 强制把缓冲区的数据推到显示器
    sleep(1);       // 此时你能立刻看到 "hello yunze",然后程序暂停1秒
    return 0;
}

这就是为什么我们在做动态效果时,必须配合 fflush 的原因------我们要让数据"立即"显示,而不是等它攒够了再显示。

二、倒计时功能的实现与原理剖析

有了上面的理论基础,我们就可以开始编写进度条的雏形了。为了让大家理解透彻,我们先写一个"倒计时"功能,它本质上就是进度条的核心逻辑。

2.1 单字符位置的动态刷新原理

最简单的倒计时是从 9 数到 0。利用 \r 将光标移回行首,再输出新数字,就能实现"原地覆盖"的效果。

c 复制代码
#include <stdio.h>
#include <unistd.h>

int main() {
    int cnt = 9;
    for (; cnt >= 0; cnt--) {
        printf("%d\r", cnt); // \r 让光标回到行首
        fflush(stdout);      // 必须强制刷新,否则看不到动态变化
        sleep(1);            // 模拟耗时操作,每秒变一次
    }
    printf("\n"); // 循环结束后换行,避免命令行提示符紧跟在数字后面
    return 0;
}

在这个例子中,每次循环都只输出一个字符(0-9),所以覆盖得非常完美。但一旦数字位数发生变化,问题就来了。

2.2 多位数倒计时的"坑":残留字符问题

如果我们把倒计时改成从 10 开始,你会发现一个诡异的现象:

c 复制代码
// 错误示范
int cnt = 10;
for (; cnt >= 0; cnt--) {
    printf("%d\r", cnt);
    fflush(stdout);
    sleep(1);
}

运行结果并不是完美的 10 -> 9 -> 8...,而是会变成 10 -> 90 -> 80...。为什么会这样?

  • 当打印 10 时,屏幕上是字符 '1''0'
  • 当打印 9 时,\r 把光标移回行首,9 覆盖了原来的 '1'
  • 关键点 :原来的 '0' 并没有被覆盖!因为 9 只有一个字符宽,它只占了第一个位置。所以屏幕上看起来就是 90

显示器是字符设备,它不会自动帮你擦除旧内容,新内容只会按顺序覆盖旧内容。没被覆盖到的地方,旧字符就会一直赖在那里。

2.3 解决方案:格式化输出与对齐控制

为了解决这个问题,我们需要保证每次输出的字符宽度是固定的。这里有两种思路:

2.3.1 粗暴法:手动补空格

在数字后面加个空格,强行把后面的字符盖掉。

c 复制代码
printf("%d \r", cnt); // 注意 %d 后面有个空格

这种方法虽然能解决问题(比如打印 9 就能盖住 10),但不够优雅,而且如果数字位数更多,你就得加更多空格,很难控制。

2.3.2 优雅法:使用 %2d 定宽输出

更专业的做法是使用格式化占位符 %2d,告诉 printf:"不管数字几位,给我占两个字符的坑位"。

c 复制代码
printf("%2d\r", cnt);

但这又引入了新问题:默认右对齐

  • 当输出 10 时,占两位,没问题。
  • 当输出 9 时,%2d 会默认把它放在右边,左边补空格,变成 " 9"
  • 此时光标回到行首,先打印空格,再打印 9。前面多出来一个空格

2.3.3 终极方案:左对齐标志 -

我们需要的是左对齐:有效数字靠左,不够的位数用空格在右边补齐。这样右边的空格就能完美覆盖掉上一轮的残留字符。

c 复制代码
printf("%-2d\r", cnt); // 减号 - 表示左对齐

加上 - 之后:

  • 输出 10 -> "10"
  • 输出 9 -> "9 " (9后面有个空格,把原来的0盖掉了)
  • 输出 8 -> "8 "

至此,一个完美的、支持多位数的倒计时功能就完成了。只要保证左半部分是定长的,无论右边怎么变,都能通过一次刷新清理干净。

三、进度条的设计与实现

3.1 文章目标与系统级编程思维

接下来就开始正式编写进度条了。这个程序虽然不大。但主要目的是让大家在 Linux 系统中,切身感受一下"系统级"或者偏"系统级"的代码究竟该怎么写。

这与 C 语言基础或数据结构有着本质的区别。在之前的文章中我们更多关注的是算法逻辑、数据结构的组织;而在编写进度条时,我们需要更多地关注系统特性。例如:

  • 刷新机制:屏幕内容是如何更新的?
  • 动态刷新:如何让输出内容在同一行不断变化?

这些都是系统级编程中必须掌握的基础概念。接下来,我们将通过两个版本的迭代,逐步实现一个标准的 Linux 风格进度条。

3.2 进度条的功能设计与视觉规范

在设计第一个版本的进度条时,我们需要明确它的视觉表现形式。一个标准的进度条通常包含以下几个核心区域:

  1. 边界标识 :最左侧是一个左中括号 [,最右侧是一个右中括号 ],用于界定进度条的范围。
  2. 进度主体 :在中括号内部,使用等号 = 来表示已完成的进度。每增加 1%(或者每增加 2%,视具体设计而定),等号的数量就增加一个。
  3. 数值显示 :在右中括号的右侧,需要显示当前的具体进度数值,例如 20%50%100% 等。
  4. 旋转光标 :在数值的右侧,还需要有一个旋转的光标(如 -\|/ 循环变化)。

3.2.1 为什么需要旋转光标?

旋转光标的作用非常关键。有时候我们在进行进度条推进时,可能会遇到进度暂时卡住、速度不再增加的情况。此时,如果光标还在旋转,就能向用户传达一个明确的信息:程序并没有死机,它还在运行,只是当前处理比较慢。 这是一个非常重要的用户体验细节。

3.3 工程环境搭建与代码结构

在正式编写逻辑之前,我们需要先搭建好工程结构。根据项目文件列表,我们采用标准的 C 语言工程结构,将代码分为三个文件:

  • main.c:主函数入口。
  • process.c:进度条功能的具体实现。
  • process.h:头文件,包含函数声明和必要的库引用。
  • Makefile:自动化编译脚本。

3.3.1 头文件与源文件的初始化

首先看 process.h,为了防止头文件被重复包含,我们使用了 #pragma once 指令,并引入了标准输入输出库 <stdio.h>。同时,声明了 void Process(); 函数供外部调用。

c 复制代码
// process.h
#pragma once

#include <stdio.h>

void Process();

接着是 main.c,它的结构非常简单,直接调用 Process() 函数即可启动进度条程序。

c 复制代码
// main.c
#include "process.h"

int main()
{
    // version1
    Process(); // 进度条程序
    return 0;
}

最后是 process.c 的初始框架。目前我们只写了一个简单的打印语句作为占位,后续将在这里填充核心逻辑。

c 复制代码
// process.c
#include "process.h"

void Process()
{
    printf("进度条程序\n");
}

3.3.2 Makefile 的编写规范

为了高效管理编译过程,我们编写了如下 Makefile。这里定义了几个关键变量:

  • Bin:最终生成的可执行文件名,设为 process_bar
  • Cc:编译器,设为 gcc
  • Src:利用 $(wildcard *.c) 自动收集当前目录下所有的 .c 文件。
  • Obj:利用模式替换 $(Src:.c=.o) 自动生成对应的 .o 目标文件列表。

编译规则分为两步:先将 .c 编译为 .o,再将 .o 链接为可执行文件。此外,还提供了 clean 伪目标用于清理工程,以及 Print 伪目标用于调试打印变量值。

makefile 复制代码
Bin=process_bar
Cc=gcc
Src=$(wildcard *.c)
Obj=$(Src:.c=.o)

$(Bin):$(Obj)
	@echo "$^ link to $@"
	@$(Cc) -o $@ $^

%.o:%.c
	@echo "compliling $< to $@"
	@$(Cc) -c $<

.PHONY:clean
clean:
	@echo "Clean Project ... Done"
	@rm -f $(Obj) $(Bin)

.PHONY:Print
Print:
	@echo $(Bin)
	@echo $(Cc)
	@echo $(Src)
	@echo $(Obj)

3.4 核心逻辑实现与时间控制

process.c 中,我们开始编写 Version 1 的核心逻辑。首先定义了一些宏常量:SIZE 设为 102(考虑到字符串结束符 \0 和边界),LABEL 定义为字符 '='

3.4.1 缓冲区初始化与循环逻辑

我们需要一个字符数组 out_bar 作为输出缓冲区,并使用 memset 将其初始化为全 \0。接着进入 while 循环,计数器 cnt 从 0 累加到 100。在每次循环中,我们将 out_bar[cnt] 赋值为 '=',从而实现进度条长度的动态增长。

c 复制代码
// process.c (Version 1 核心逻辑片段)
#include "process.h"
#include <string.h>
#include <unistd.h>

#define SIZE 102
#define LABEL '='

// 进度条的version1版本
void Process()
{
    int cnt = 0;
    char out_bar[SIZE];
    memset(out_bar, '\0', sizeof(out_bar));
    
    while(cnt <= 100)
    {
        // 注意:这里的打印格式后续需要修正,目前仅为演示逻辑
        printf("[%s][%d]\n", out_bar, cnt); 
        
        out_bar[cnt] = LABEL;
        cnt++;
        
        sleep(1); // 临时休眠控制
    }
}

3.4.2 usleep 与时间单位详解

在上述代码中,我们使用了 sleep(1) 来控制进度条的刷新速度。但在实际开发中,sleep 的单位是秒,这对于进度条来说太慢了。我们需要更精细的时间控制,因此要引入 usleep 函数。

这里必须强调一个关键知识点:usleep 的参数单位是微秒(microsecond),而不是毫秒(ms)。

  • 毫秒 (ms) : 1 s = 10 3 ms 1 \text{ s} = 10^3 \text{ ms} 1 s=103 ms
  • 微秒 (us) : 1 s = 10 6 us 1 \text{ s} = 10^6 \text{ us} 1 s=106 us
  • 纳秒 (ns) : 1 s = 10 9 ns 1 \text{ s} = 10^9 \text{ ns} 1 s=109 ns

如果你不理解 microsecond,可以联想微软的英文 "Microsoft"。如果要让程序休眠 1 秒,传递给 usleep 的参数必须是 10 6 10^6 106,即 1,000,000

3.4.3 调整刷新频率

为了让演示效果更佳,我们不希望程序跑完 100% 需要 100 秒。我们可以通过调整 usleep 的参数来控制总时长:

  • 如果参数是 10 6 10^6 106(1秒),跑完需 100 秒。
  • 如果参数是 10 5 10^5 105(0.1秒),跑完需 10 秒。
  • 如果参数是 50000 50000 50000(0.05秒),跑完约 5 秒。
  • 如果参数是 30000 30000 30000(0.03秒),跑完约 3 秒。

在实际调试中,我们可以将参数设置为 30000 左右,让整个进度条在 3 秒钟左右跑完,这样既不会太快看不清,也不会太慢让人等待。

3.5 常见问题分析:换行与刷新

在前面的实现过程中,我们也会遇到一个问题,比如进度条为什么会换行?为什么去掉了换行符反而打印不出来了?为什么进度条是向右"跑"而不是原地"长"?

3.6 进度条的"换行"灾难与缓冲区刷新机制

3.6.1 为什么进度条会换行?

在最初的 process.c 代码(Version 1)中:

c 复制代码
printf("[%s][%d]\n", out_bar, cnt);

这里最大的问题在于末尾的 \n(换行符)。在终端中,\n 的作用是将光标移动到下一行的起始位置。如果你每打印一次进度就换行,那么你的终端屏幕上就会出现 100 多行输出,而不是我们在同一行看到的动态增长效果。因此,进度条绝对不能换行

3.6.2 去掉 \n 后的"假死"现象

既然不能换行,那我们直接把 \n 去掉行不行?

c 复制代码
// 错误示范:去掉了 \n
printf("[%s][%d]", out_bar, cnt); 

当你把代码改成这样并运行后,你会发现一个奇怪的现象:程序运行了一段时间,屏幕上什么都看不到,仿佛程序卡死了一样,直到最后程序结束,才突然一次性打印出最终结果。

原理剖析:

这就是前面提到的缓冲区机制

解决方案:

必须手动强制刷新缓冲区。我们需要调用 fflush(stdout);

c 复制代码
printf("[%s][%d]", out_bar, cnt);
fflush(stdout); // 强制将缓冲区内容立即输出到屏幕

加上这一句后,数据就能实时打印出来了,但又出现了新的问题。

3.7 光标回退与动态覆盖原理

3.7.1 为什么进度条会"向右跑"?

虽然去掉了换行并加了刷新,但你会发现进度条并不是在原位变长,而是像贪吃蛇一样一直往右边延伸:第一次打印完,第二次接着在后面打,第三次继续往后堆。这是因为默认情况下,打印结束后光标停留在当前字符串的末尾,下一次打印自然就从那里开始了。

期望效果:

我们希望每次打印完当前状态后,光标能瞬间回到当前行的最左侧,然后覆盖掉上一次的内容。这样利用人眼的视觉暂留,看起来就像是进度条在原地动态增长。

3.7.2 回车符 \r 的妙用

要实现"回到行首但不换行",我们需要使用回车符 \r(Carriage Return)。

  • \n (Line Feed):换行,光标移到下一行。
  • \r (Carriage Return):回车,光标移到当前行的最开头。

修改代码如下:

c 复制代码
// 正确做法:使用 \r 代替 \n
printf("[%s][%d]\r", out_bar, cnt);
fflush(stdout);

此时再次编译运行,你会看到进度条终于如我们所料,从左向右依次刷新,实现了动态增长的效果。

3.7.3 善后处理:最后的换行

还有一个细节需要注意。当进度条走到 100% 结束时,如果直接退出程序,Shell 重新接管终端,Shell 会打印它的命令提示符且不会主动帮你换行,它只会从当前光标所在的位置开始打印。会覆盖掉进度条的尾部,非常难看。

因此,在 while 循环结束后,我们需要补一个换行符,让光标跳到下一行,把进度条"定格"在屏幕上。

c 复制代码
// 循环结束后
printf("\r\n"); // 或者 printf("\n"); 均可,确保光标换行

3.8 格式化输出与对齐陷阱

3.8.1 固定宽度与左对齐

在早期的代码中,如果直接使用 %s 打印 out_bar,由于字符串长度在不断变化(从空到 100 个字符),会导致后面的百分比数字 [1%][100%] 位置不断跳动,界面非常抖动。

为了解决这个问题,我们需要给进度条预留一个固定宽度的空间。

  • 右对齐(默认)[%100s]。这会导致进度条从最右边开始往左填,或者是数字顶着右边,效果不符合直觉。
  • 左对齐[%-100s]。加上负号 - 表示左对齐。这样无论 out_bar 有多长,它都占满 100 个字符的位置(不足补空格),保证后面的百分比信息始终固定在右侧。

3.8.2 百分比的转义与定宽

打印百分比时,C 语言中 % 是特殊字符,必须用 %% 来转义输出一个真实的百分号。同时,为了防止 1% 变成 10% 时位数变化引起抖动,我们需要限制数字的宽度。

  • %3d%%:表示整数占 3 位宽度,右对齐。
  • %.1f%%:如果是浮点数,保留一位小数。

综合起来,完美的打印语句应该是这样的:

c 复制代码
printf("[%-100s][%3d%%]\r", out_bar, cnt);

如果不喜欢旋转光标也可以像下面这样写

四、进阶优化:旋转光标与防"假死"

4.1 实现逻辑

旋转光标的状态与下载进度本身无关,它只和 Process 函数被调用的次数有关。我们可以定义一个静态字符串数组或指针:

c 复制代码
const char *lable = "|/-\\";
int len = strlen(lable);

在每次打印时,通过取模运算来循环获取字符:

c 复制代码
// index 每次调用自增,对长度取模实现循环
lable[index % len] 

将其加入格式化输出中:

c 复制代码
printf("[%-100s][%3d%%][%c]\r", out_bar, cnt, lable[index % len]);

这样,即使进度条不动,那个小光标也会不停地旋转,给用户一种"活着的"感觉。

五、工程化重构:回调函数与真实场景适配

5.1 从"内部循环"到"外部驱动"

真实的下载过程是:下载一点数据 -> 更新一下进度 -> 再下载一点。进度条函数不应该自己循环,而应该由下载函数来驱动。

5.2 引入回调函数 (Callback)

为了让进度条组件通用化(解耦),我们不应该在 Download 函数里硬编码调用 Process。更好的做法是使用函数指针作为回调。

定义回调类型:

c 复制代码
typedef void (*flush_t)(double total, double current, double speed, const char *user_info);

改造下载函数:

Download 函数接收一个回调函数指针 cb。每当下载了一部分数据,就调用这个 cb 来刷新界面。

c 复制代码
void DownLoad(double total, flush_t cb) {
    double current = 0.0;
    // ... 模拟下载逻辑 ...
    current += speed;
    
    // 关键:通过回调通知外部更新进度
    cb(total, current, speed, "MB/s"); 
}

改造 Process 函数:

现在的 Process 不再维护自己的 cnt 循环,而是根据传入的 currenttotal 实时计算比率:

c 复制代码
void Process(double total, double current, double speed, const char *user_info) {
    if(current > total) return;
    
    // 1. 计算比率
    double rate = current * 100 / total;
    
    // 2. 填充进度字符
    memset(out_bar, '\0', sizeof(out_bar));
    int i = 0;
    for(; i < (int)rate; i++) {
        out_bar[i] = LABLE;
    }
    
    // 3. 刷新进度条 (包含旋转光标、速度信息等)
    printf("[%-100s][%5.1lf%%][%c] | %.1lf/%.1lf, speed: %.1lf%s\r", \
           out_bar, rate, lable[index], total, current, speed, user_info);
    fflush(stdout);
    
    // 4. 处理完成后的换行
    if(current >= total) printf("\r\n");
}

通过这种回调机制 ,我们的进度条代码就变成了一个独立的模块。无论是下载文件、解压数据还是安装游戏,只要传入相应的数据和回调函数,就能复用这套漂亮的进度条逻辑。

六、printf 的彩色输出与终端美化

虽然我们的进度条能动态增长、能加旋转光标了,但全是在用黑白字符在终端里"傻跑",看着还是不够炫酷、不够优雅。其实,我们在 printf 输出时,是完全可以控制字体颜色和背景色的,也就是实现彩色输出

6.1 核心原理:ANSI 转义序列

在 Linux(以及 macOS、Windows 新版终端)中,控制终端字体颜色、背景和光标位置的标准方法是使用 ANSI 转义序列(ANSI Escape Codes)

它的核心格式非常固定:

c 复制代码
\033[参数m
  • \033:是转义字符 ESC 的八进制表示(在代码中也可以写成 \x1b\e)。
  • [:表示控制序列的开始。
  • 参数:由一个或多个数字组成,用分号 ; 隔开,代表不同的颜色或样式。
  • m:表示控制序列的结束,即设置属性结束。

⚠️ 极其重要的注意事项:

每次使用彩色输出后,必须在字符串末尾加上重置属性控制码 \033[0m。如果不加,终端会"记住"这个颜色设置,导致你后续输出的所有普通文本全部变成该颜色。

c 复制代码
// 正确示范:打印红色的"Hello",后面的文本恢复默认颜色
printf("\033[31mHello\033[0m World\n");

6.2 常用控制参数对照表

我们可以通过组合不同的参数,来精细控制输出的显示效果。参数之间直接用分号 ; 连接即可,例如 \033[1;31;42m 表示:高亮加粗 + 红色前景色 + 绿色背景色

6.2.1 样式与显示属性

参数 效果说明 备注
0 重置/默认 关闭所有属性,恢复终端默认状态
1 高亮 / 加粗 字体加粗或变亮
4 下划线 文字下方出现下划线
5 闪烁 文字会进行慢速闪烁(部分现代终端可能屏蔽)
7 反显 / 反色 交换前景色和背景色
8 隐藏 / 消隐 字体颜色与背景色相同(不可见)

6.2.2 前景色(字体颜色)

参数 颜色 参数(高亮) 颜色(高亮)
30 黑色 90 灰色/亮黑色
31 红色 91 亮红色
32 绿色 92 亮绿色
33 黄色 93 亮黄色
34 蓝色 94 亮蓝色
35 紫色(洋红) 95 亮紫色
36 青色(深蓝绿) 96 亮青色
37 白色 97 亮白色

6.3.3 背景色(背景填充颜色)

背景色的代码非常规律,刚好是在前景色的基础上加 10

参数 颜色 参数 颜色
40 黑色背景 100 灰色/亮黑背景
41 红色背景 101 亮红色背景
42 绿色背景 102 亮绿色背景
43 黄色背景 103 亮黄色背景
44 蓝色背景 104 亮蓝色背景
45 紫色背景 105 亮紫色背景
46 青色背景 106 亮青色背景
47 白色背景 107 亮白色背景

这个我就不做演示了,可以自行下去尝试


结语

相关推荐
Mapleay1 小时前
设备树/ACPI 与 fwnode 内核基础设施
linux
半摆烂日常1 小时前
自建WMS和买成品:三年成本对比
大数据·服务器·数据库·python·深度学习·低代码·numpy
宋军涛2 小时前
Windows 操作系统【任务计划程序】简单使用
服务器·windows
蒸蒸yyyyzwd2 小时前
cpp 选手秋招学习笔记 day27
服务器·c++·面试·八股
waiting9711182 小时前
Ubuntu 26.04 + Android14安装与编译教程
android·linux·ubuntu
Lsetea2 小时前
ACME 报 rate limit:Let‘s Encrypt 限速类型与重试策略
运维·https·证书·ssl·acme
qetfw2 小时前
Debian 部署 phpMyAdmin:Apache、PHP 与 MariaDB 管理界面配置
linux·debian·php·apache
大模型码小白2 小时前
ChatGPT 的回答也能被结构化抓取?AI Bot Scraper 对比测评
服务器·开发语言·数据库·人工智能·spring·chatgpt
A黄俊辉A3 小时前
两分钟上手docker
运维·docker·容器