目录
- 前言
- [一、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 两个容易被忽略的储备知识
在动手写进度条之前,有两个关于 C 语言输出的"常识"必须先讲清楚。这两个知识点虽然基础,但极易被忽略,却是实现进度条动态刷新效果的核心基石。
1.2.1 回车(\r)与换行(\n)的区别
我们在写作文或打字时,习惯把"回车换行"当成一个动作,但在计算机底层,这其实是两回事:
- 回车(Carriage Return,
\r) :光标回到当前行的开头。就像老式打字机,打字头移回最左边,但纸没有卷动。 - 换行(Line Feed,
\n) :光标移动到下一行的同一列位置。纸卷动了一行,但打字头没动。
在 C/C++ 的标准输出中,\n 通常被系统解释为"回车+换行"的组合动作(即 \r\n)。
- 如果你只想让光标回到行首而不换行(为了实现覆盖打印),必须使用
\r。 - 如果你想立即刷新输出并换行,通常使用
\n或std::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%,视具体设计而定),等号的数量就增加一个。 - 数值显示 :在右中括号的右侧,需要显示当前的具体进度数值,例如
20%、50%、100%等。 - 旋转光标 :在数值的右侧,还需要有一个旋转的光标(如
-、\、|、/循环变化)。
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 循环,而是根据传入的 current 和 total 实时计算比率:
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 |
亮白色背景 |
这个我就不做演示了,可以自行下去尝试
结语
