简陋的进度条程序
简陋的进度条程序
processbar.h
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>#define NUM 103
#define Body '='
#define Head '>'typedef void (*callback_t)(double);//version 1
void process();//version 2
void process_flush_a(double rate);//version 3
void process_flush_b(double rate);
processbar.c
#include "processbar.h"
#include <unistd.h>
const char* lable = "|\\-/";// version 1
void process()
{char buffer[NUM] = {'\0'};int count = 0;buffer[0] = Head;while(count<=100){//打印字符数组printf("[%-100s][%3d%%] %c\r",buffer,count,lable[count%4]);//打印执行后要立即刷新屏幕,让打印内容出现在屏幕上fflush(stdout);buffer[count++] = Body;if(count < 100){buffer[count] = Head;}usleep(50000);}printf("\n");
}// version 2static char buffer[NUM] = {'\0'};
static int count = 0;
void process_flush_a(double rate)
{int temp = (int)rate;if(temp < 1) buffer[0] = Head;printf("[%-100s][%.1f%%] %c\r",buffer,rate,lable[count%4]);fflush(stdout);buffer[temp++] = Body;if(temp < 100){buffer[temp] = Head;}count++;count %= 4;}// version 3void process_flush_b(double rate)
{int temp = (int)rate;// 先打印白色背景的进度条底色printf("\033[48;5;7m"); // 设置背景为白色for (int i = 0; i < 100; i++) {printf(" ");}printf("\033[0m\r"); // 重置颜色并回到行首// 打印紫色进度部分printf("\033[48;5;135m"); // 设置背景为紫色for (int i = 0; i < temp; i++) {printf(" ");}printf("\033[0m"); // 重置颜色// 打印百分比和旋转符号printf("[%.1f%%] %c\r", rate, lable[count % 4]);fflush(stdout);count++;count %= 4;
}
main.c
#include "processbar.h"
#define FILESIZE 1024*1024*1024//模拟场景:下载任务
void download(callback_t cb)
{srand(time(NULL)^1023);int total = FILESIZE;while(total){usleep(10000);int one = rand()%(1024*1024);total -= one;if(total < 0) total = 0;int doned = FILESIZE - total;double rate = ((double)doned / (double)(FILESIZE)) * 100.00;cb(rate);}//这里直接调用函数也是可以的//process_flush_a(rate);printf("\n");
}int main()
{// process();download(process_flush_a);//download(process_flush_b);return 0;
}
Makefile
pb: main.o processbar.ogcc main.o processbar.o -o pbmain.o: main.c processbar.hgcc -c main.c -o main.oprocessbar.o: processbar.c processbar.hgcc -std=c99 -c processbar.c -o processbar.o.PHONY:clean
clean:rm -f *.o pb
这个进度条程序是在Linux系统写的,在Windows下运行可能需要更换一下头文件。
这个程序的运行主要依托下面两个知识点:
回车\r
与换行\n
这个故事要从机械打字机时代开始讲起。
1. 历史起源:机械打字机
在计算机诞生之前,电动机械打字机是处理文本的主要工具。它有两个独立的操作:
- 回车 (Carriage Return, CR)
- 动作:将打印头( carriage )移回当前行的最左端(起始位置)。
- 目的:为了在下一行的相同起始位置继续打印,或者在同一行重复打印(如生成下划线)。
- 换行 (Line Feed, LF)
- 动作:将卷筒旋转一行,使纸张上移一行。
- 目的:将打印位置移动到下一行。
所以要开始新的一行,需要依次执行两个动作:换行(移动到下一行)和回车(移动到行首)。这两个动作是分开的,由两个不同的杠杆或按钮控制。
早期的计算机设计者沿用了这个逻辑,因为计算机最早的输出设备就是电传打字机(Teletype)。
这个从我们的键盘上也可以看的出来:
2. 计算机中的表示:ASCII 码
计算机字符编码标准 ASCII(American Standard Code for Information Interchange)为这两个操作分配了特定的控制字符:
- 回车符 (Carriage Return):ASCII 码 13 (十六进制
0x0D
),在C语言和许多其他语言中用\r
表示。 - 换行符 (Line Feed):ASCII 码 10 (十六进制
0x0A
),在C语言和许多其他语言中用\n
表示。
因此,在纯文本层面,\r
和 \n
就是两个特殊的控制字符,它们没有可见的图形表示,而是用来控制光标或文本流的布局。
3. 不同操作系统的分歧
这里就是所有混乱的根源。对于“如何表示一行的结束”,不同的操作系统阵营做出了不同的选择:
-
Unix / Linux / macOS (现代版本) / Android
- 标准:只使用
\n
(LF) 来表示新行。 - 哲学:
\n
的含义就是“新行”(Newline),它同时包含了回车和换行的语义。更简洁、高效。
- 标准:只使用
-
Windows / DOS
- 标准:使用
\r\n
(CR LF) 两个字符的组合来表示新行。 - 哲学:严格遵循了打字机的传统,先回车再换行。这是从CP/M系统继承下来的历史遗产。
- 标准:使用
-
Classic Mac OS (macOS 9 及之前版本)
- 标准:只使用
\r
(CR) 来表示新行。 - 哲学:另一个历史分支。苹果的现代系统(基于Unix的macOS)已经转向使用
\n
。
- 标准:只使用
这种分歧导致的问题:
当你把一个在Linux上创建的文本文件用Windows的记事本打开时,所有内容可能会显示在同一行,因为记事本只认识 \r\n
作为换行符,而不认识单独的 \n
。
反之,一个带有 \r\n
的Windows文本文件在Unix系统上显示时,有时会在行尾多出一个 ^M
符号(这是Vi/Vim等编辑器显示 \r
字符的方式)。
现代的高级文本编辑器(如VSCode, Sublime Text, Notepad++等)都能自动识别并正确处理这两种换行符。
4. C语言中的 \r
和 \n
在C语言中,\r
和 \n
是转义序列,它们被编译器分别解释为ASCII码13和10。
关键点:C语言标准库中的I/O函数有一个重要的“文本模式”和“二进制模式”的区别,这直接影响了对 \r
和 \n
的处理。
文本模式 ("r"
, "w"
) vs 二进制模式 ("rb"
, "wb"
)
-
在文本模式下 (
"w"
,"r
"):- 写入时:当你在程序中使用
printf("Hello\nWorld");
并向文件写入时,C标准库会根据当前平台进行转换。- 在 Windows 上,
\n
会被转换为\r\n
再写入文件。 - 在 Linux 上,
\n
保持不变,直接写入文件。
- 在 Windows 上,
- 读取时:当从文件读取时,标准库会进行反向转换。
- 在Windows上,文件中的
\r\n
会被转换回单个的\n
传入程序。 - 在Linux上,文件中的
\n
保持不变传入程序。
- 在Windows上,文件中的
- 目的:为了让程序员只需使用
\n
,而不用关心底层操作系统的差异,实现代码的跨平台性。\n
在C语言中就是“新行”的抽象表示。
- 写入时:当你在程序中使用
-
在二进制模式下 (
"wb"
,"rb
"):- 没有任何转换。你写入什么字节,文件里就存储什么字节;你读取到什么字节,程序就看到什么字节。
- 如果你在Windows上用
fputs("Hello\nWorld", file)
并以二进制模式"wb"
写入,文件里存储的就是Hello\nWorld
(\n
是ASCII 10)。 - 如果你写入
"Hello\r\nWorld"
,文件里存储的就是Hello\r\nWorld
。
控制台输出行为
当你在C程序中使用 printf("\n");
向控制台(屏幕)输出时:
- 在绝大多数系统上,
\n
都会同时实现换行和回车的效果,将光标移动到下一行的行首。控制台设备本身已经将\n
解释为“新行”。 - 单独使用
\r
会有趣:printf("Loading... 50%\r");
。这会将光标移回当前行的行首,然后后续的输出会覆盖当前行的内容。这常被用来制作命令行中的进度提示或动态更新效果,例如:
输出会原地更新百分比,而不是打印新行。#include <stdio.h> #include <unistd.h> // for sleep()int main() {for (int i = 0; i <= 100; i += 10) {printf("\rProgress: %3d%%", i); // \r 回到行首,覆盖上次的内容fflush(stdout); // 强制刷新输出缓冲区,立即显示sleep(1);}printf("\nDone!\n");return 0; }
小点总结
概念 | C语言转义符 | ASCII码 | 历史含义 | 现代含义(主要) |
---|---|---|---|---|
回车 (Carriage Return) | \r | 13 (0x0D) | 将打印头移回行首 | 将光标移回行首(常用于覆盖输出) |
换行 (Line Feed) | \n | 10 (0x0A) | 将纸张上移一行 | 新行(抽象概念,效果是光标移到下一行行首) |
核心要点:
- 历史:
\r
(回车) 和\n
(换行) 源于机械打字机的两个独立动作。 - 分歧:Windows用
\r\n
表示行尾,Unix/Linux用\n
表示行尾。 - C语言抽象:在代码中,你应始终使用
\n
来表示换行,以保证可移植性。 - C语言I/O模式:
- 文本模式:隐藏了操作系统的差异,帮你自动转换
\n
和\r\n
。 - 二进制模式:直接进行字节操作,不做任何转换。
- 文本模式:隐藏了操作系统的差异,帮你自动转换
- 特殊用途:
\r
可用于命令行下的动态输出效果。
输出缓冲区
一、什么是输出缓冲区?
想象一下你要寄很多明信片给朋友。
- 没有缓冲区:每写完一张明信片,你就立刻跑到邮筒去投递一张。如果你的朋友遍布全球,你这整天就光在路上跑了,效率极低。
- 有缓冲区:你先把所有明信片都写完,堆在桌子上(这个“桌子”就是缓冲区)。等攒够一定数量(比如10张),或者你决定今天不再写了,你再把这一整叠明信片一次性拿去投递。
输出缓冲区(Output Buffer) 就是计算机世界中这样一个“桌子”。它是一块临时的内存区域,用于暂存程序准备输出的数据。程序不直接与输出设备(屏幕、磁盘、网络)打交道,而是先把数据写到这块内存里,之后再由特定的机制(刷新缓冲区)将数据一次性发送到目标设备。
二、为什么要有输出缓冲区?它的作用是什么?
设立缓冲区的主要目的可以归结为两个字:效率。
1. 极大减少系统调用次数,提升性能
这是最核心的原因。与硬件设备(如磁盘、显示器、网络接口)进行I/O操作(write
系统调用)是非常耗时的。它需要从用户模式切换到内核模式,让操作系统来协调硬件,这个过程比在内存中操作数据要慢成千上万倍。
- 无缓冲:如果每次
printf(‘a’)
都直接让系统向屏幕写一个字符,程序大部分时间都在等待这个缓慢的I/O操作完成。 - 有缓冲:程序可以飞快地把
‘a’, ‘b’, ‘c’, ‘d’, ‘e’, ‘\n’
这几个字符依次放入内存中的缓冲区。仅当缓冲区满了,或者遇到换行符‘\n’
等特殊条件时,才发起一次系统调用,将所有数据一次性写入。这将成千上万次的慢速操作减少为几次,性能提升是巨大的。
2. 协调不同设备之间的速度差异
CPU和内存的处理速度(纳秒级)与磁盘、网络I/O的速度(毫秒级)有着天壤之别。缓冲区就像一个“水库”,在洪水季(CPU高速产生数据)时蓄水,然后在枯水季(I/O设备空闲时)平稳放水,避免了洪水直接冲击下游(I/O设备忙不过来)。
三、缓冲区在C语言中的具体体现
C标准库(stdio.h
)为我们管理了缓冲区,通常有三种缓冲策略:
缓冲模式 | 行为 | 典型应用 |
---|---|---|
全缓冲(Fully Buffered) | 缓冲区填满时,才执行真正的I/O操作。 | 磁盘文件。这是最高效的方式,因为磁盘块读写有固定大小。 |
行缓冲(Line Buffered) | 遇到换行符 ‘\n’ 时,或者缓冲区填满时,执行I/O操作。 | 标准输入(stdin)和标准输出(stdout)(当它们连接到终端时,即命令行界面)。这方便我们与程序交互,一行的输入/输出是自然的单位。 |
无缓冲(Unbuffered) | 数据立即输出,不经过缓冲区。 | 标准错误(stderr)。这样错误信息可以立即被用户看到,即使程序后面崩溃了,错误信息也已经打印出来了。 |
关键函数:fflush
int fflush(FILE *stream);
这个函数强制将指定流的缓冲区中的内容立即写入,即使它还没满。这在需要确保用户立即看到提示信息时非常有用。
示例:
#include <stdio.h>
#include <unistd.h> // for sleep()int main() {// 不加 fflushprintf("Waiting for 3 seconds");sleep(3); // 程序休眠3秒printf("...Done.\n");// 加 fflushprintf("Waiting for 3 seconds");fflush(stdout); // 强制刷新标准输出的缓冲区sleep(3);printf("...Done.\n");return 0;
}
- 第一种情况:字符串
“Waiting for 3 seconds”
被放入行缓冲区,但因为它没有以‘\n’
结尾,缓冲区不会自动刷新。你要等到3秒后“...Done.\n”
被打印时,才会一次性看到全部输出。 - 第二种情况:调用
fflush(stdout)
后,缓冲区被强制清空,内容立即显示在屏幕上,然后程序才开始休眠。这样用户就能立即看到提示信息。
四、一个常见的“坑”与调试技巧
很多初学者在调试程序时,会用 printf
打印日志来跟踪执行流程,有时会遇到令人困惑的情况:
#include <stdio.h>
#include <unistd.h>int main() {printf("Starting the loop...\n");for (int i = 0; i < 5; i++) {printf("Counter: %d ", i); // 注意:没有换行符sleep(1); // 每秒一次}printf("\nLoop finished.\n");return 0;
}
你期望的输出是每秒打印一个数字:
Counter: 0 Counter: 1 Counter: 2 ...
但实际的输出可能是:程序休眠了5秒,然后一下子把所有 Counter: X
都打印了出来。
为什么?
因为 printf(“Counter: %d “, i);
的输出内容没有换行符 ‘\n’
,不足以触发行缓冲的刷新条件。并且缓冲区很小,这几条短消息也没能把它填满。所以所有的消息都暂存在缓冲区里,直到程序最后遇到 printf(“\nLoop finished.\n”);
中的 ‘\n’
,或者 main
函数结束自动刷新所有缓冲区时,才被一次性输出。
解决方法:
- 在需要立即输出的消息末尾加上换行符
\n
。 - 在
printf
后手动调用fflush(stdout)
。 - (不推荐)可以使用
setvbuf
函数将stdout
设置为无缓冲模式。
小点总结
方面 | 解释 |
---|---|
是什么 | 一块用于暂存输出数据的内存区域。 |
为什么要有 | 核心目的:提升I/O效率。通过减少昂贵的系统调用次数,协调CPU与I/O设备之间的速度差异。 |
工作模式 | 全缓冲(文件)、行缓冲(终端)、无缓冲(stderr)。 |
如何控制 | 使用 \n 触发行缓冲刷新;使用 fflush(stdout) 强制立即刷新。 |
注意事项 | 理解缓冲机制是写出正确交互式程序的关键,避免因缓冲导致的输出延迟误解程序逻辑。 |
输出缓冲区是系统设计中的一个经典权衡:用一点点内存空间作为代价,换取巨大的性能提升。
终端倒计时
结合回车和缓冲区的知识,我们可以在Linux写一个倒计时的小程序:
#include <stdio.h>
#include <unistd.h>int main()
{int n = 10;while(n >= 0){printf("%-2d\r",n);fflush(stdout);--n;sleep(1);}return 0;
}
这里有同学可能就会对代码中的一句有疑惑:
//为啥是这样
printf("%-2d\r",n);
//而不是这样
printf("%d\r",n);
大家可以试试看这样打印出来的倒计时是不是这样的:
10 90 80 70 60 50...10 00
(在这里我无法给大家演示回车的效果,只能在一行里面写完了。其实说完整点就是:终端会在同一位置依次显示 10
→90
→…→10
→00
)
很明显这个并不是我们想要的倒计时的结果。
这个就涉及printf打印的本质了。
核心结论
printf
本质上并不是在直接打印整数、浮点数等,它最终都是在打印单个字符。 它扮演的是一个“翻译官”或“格式化器”的角色,其核心工作是:将内存中各种格式的二进制数据,按照格式说明符(如 %d
, %f
)的指示,转换为相应的字符序列(字符串),然后将这个字符序列逐个字符地送入输出缓冲区。
我们可以用一个简单的公式来理解:
printf
= 解析格式字符串 + 将参数转换为字符序列 + 写入缓冲区
详细原理分解
让我们一步步拆解 printf("Hello, %d!", 42);
这个调用的执行过程。
第1步:解析格式字符串
printf
接收到的第一个参数是一个字符串 "Hello, %d!"
。函数会逐个扫描这个字符串的每一个字符:
- 遇到普通字符
'H'
:直接将这个字符放入输出缓冲区。 'e'
:放入缓冲区。'l'
:放入缓冲区。'l'
:放入缓冲区。'o'
:放入缓冲区。','
:放入缓冲区。' '
(空格):放入缓冲区。- 遇到
'%'
:这表示一个格式说明符的开始。函数会继续读取下一个字符'd'
,从而知道它需要处理一个int
类型的参数。
第2步:处理可变参数并转换
C语言的可变参数机制(va_list
, va_arg
)使得 printf
能从栈上或指定的寄存器中按顺序获取你传入的额外参数。
- 获取参数:根据格式说明符
%d
,函数知道要去获取一个int
类型的参数。于是它从参数列表中取得整数值42
。 - 整数到字符串的转换:这是最关键的一步。函数并不会直接去打印数字
42
的二进制形式。相反,它在内部调用了一个类似itoa
(integer to ASCII) 的函数,将整数值42
转换为对应的字符序列['4', '2']
。- 转换算法大致是:不断将数字除以10,得到余数,然后将余数加上字符
'0'
的ASCII码,得到对应的数字字符。 42 / 10 = 4
… 余2
->'2'
= 2 + ‘0’4 / 10 = 0
… 余4
->'4'
= 4 + ‘0’- 将得到的字符逆序排列,得到
'4'
,'2'
。
- 转换算法大致是:不断将数字除以10,得到余数,然后将余数加上字符
第3步:将转换后的字符序列送入缓冲区
现在,printf
有了需要输出的完整字符序列:
'H', 'e', 'l', 'l', 'o', ',', ' ', '4', '2', '!'
它将这些字符一个一个地放入与 stdout
(标准输出)关联的输出缓冲区中。
第4步:刷新缓冲区
根据缓冲区的设置(通常是行缓冲):
- 如果缓冲区满了,或者
- 如果遇到了换行符
\n
,或者 - 程序正常结束
缓冲区的内容会被一次性刷新(flush)。这时,操作系统才真正执行系统调用(如 write
),将缓冲区中的每一个字符的ASCII码发送到标准输出(通常是终端屏幕)。
终端接收到这些字符的ASCII码,再根据字体设置,将它们在屏幕上渲染成你看得懂的图形。
用代码和内存视角来理解
假设我们在一个32位小端序系统上:
int num = 42; // 整数42在内存中的二进制表示: 0x0000002A
char str[] = "Hello"; // 字符串在内存中: 'H','e','l','l','o','\0'printf("Number: %d", num);
printf("String: %s", str);
-
对于
%d
:printf
找到变量num
的内存地址,读取4个字节的数据0x2A000000
(注意字节序)。- 它不会直接输出这4个字节!而是将这4个字节代表的整数值 42 计算出来。
- 然后将数值 42 转换为两个字符
'4'
(ASCII码 52) 和'2'
(ASCII码 50)。 - 最后将
N','u','m','b','e','r',':',' ','4','2'
这些字符的ASCII码送入缓冲区。
-
对于
%s
:printf
获取到str
的地址,然后从这个地址开始,逐个读取字符直到遇到终止符'\0'
。- 它直接将读到的字符
'H','e','l','l','o'
送入缓冲区。字符串本身在内存中就已经是字符序列了,所以这里不需要转换,只是简单的内存拷贝。
总结与类比
概念 | 解释 | 类比 |
---|---|---|
内存中的数据 | 整数、浮点数等以二进制形式存储。 | 原材料(如小麦、生肉)。 |
格式说明符 %d , %x | 指定如何“解读”和“转换”二进制数据。 | 菜谱(是做面包还是做面条)。 |
转换过程 | 将二进制数转换为十进制、十六进制等对应的字符序列。 | 烹饪过程(将原材料加工成食物)。 |
输出缓冲区 | 暂存转换后得到的字符序列。 | 餐盘(存放做好的食物)。 |
最终输出 | 将缓冲区中的字符ASCII码发送到设备。 | 上菜(把餐盘里的食物端上桌)。 |
所以:
printf
打印的不是原始的整数或浮点数本身,它打印的是这些数据经过转换后得到的字符表示。无论是整数、浮点数,还是字符串,最终都是以单个字符的形式被处理和输出的。字符串之所以看起来是“直接”打印的,只是因为它的内存表示本身就已经是最终的字符序列形式,无需转换。
继续
#include <stdio.h>
#include <unistd.h>int main()
{int n = 10;while(n >= 0){printf("%d\r",n);//fflush(stdout);--n;sleep(1);}return 0;
}
知道printf打印的原理后,理解这个程序的结果就很简单了。因为我们第一个打印的数字是10。那么printf实际在终端上打印出来的是两个字符'1'
和'0'
当我们打印完这两个字符后,就进行了回车操作\r
,所以这个时候我们又回到了行首,也就是字符'1'
所在位置。接下来我们要打印的9 8 7...1 0
都是单个字符的,所以这些9 8 7...1 0
字符只会会覆盖字符'1'
。所以就导致了我们看到的是10 90 80 70 60 50...10 00
。原因就是下一位字符'0'
没有被覆盖,所以会一直显示在屏幕上。
那么我们把printf("%d\r",n);
换成printf("%-2d\r",n);
就好了,这是为什么呢?
printf("%-2d\r", n);
这条语句的作用是在终端的当前行开头,以左对齐的方式打印一个占 2 个字符宽度的整数 n
,并将光标移回行首,具体拆解如下:
1. 格式说明符 %-2d
的含义
-
%d
:表示打印整数(n
的值)。 -
2
:指定字段宽度为 2 个字符。如果n
的位数少于 2(比如n=3
),会在数字后面补空格以凑满 2 个字符;如果n
的位数等于或超过 2(比如n=10
),则按实际位数打印,不截断。这样一来补的空格就可以覆盖掉打印10的时候的'0'
了。 -
-
:表示左对齐(默认是右对齐)。即数字靠左显示,多余的宽度用空格在右侧填充。示例:
- 当
n=5
时,%-2d
会打印5
(数字 5 + 1 个空格,共 2 个字符,左对齐)。 - 当
n=12
时,%-2d
会打印12
(刚好 2 个字符,无需补空格)。
- 当
2. 转义字符 \r
的作用
\r
是回车符,它的功能是将终端的光标移回当前行的开头(但不换行)。
这意味着:下一次在同一行打印内容时,会覆盖当前行已有的内容(从行首开始覆盖)。
整体效果
这条语句通常用于动态更新终端显示的数值(比如计数器、进度值),避免频繁换行导致的界面混乱。
进度条代码讲解
#include "processbar.h"
#include <unistd.h>
const char* lable = "|\\-/";// version 1
void process()
{char buffer[NUM] = {'\0'};int count = 0;buffer[0] = Head;while(count<=100){//打印字符数组printf("[%-100s][%3d%%] %c\r",buffer,count,lable[count%4]);//打印执行后要立即刷新屏幕,让打印内容出现在屏幕上fflush(stdout);buffer[count++] = Body;if(count < 100){buffer[count] = Head;}usleep(50000);}printf("\n");
}// version 2static char buffer[NUM] = {'\0'};
static int count = 0;
void process_flush_a(double rate)
{int temp = (int)rate;if(temp < 1) buffer[0] = Head;printf("[%-100s][%.1f%%] %c\r",buffer,rate,lable[count%4]);fflush(stdout);buffer[temp++] = Body;if(temp < 100){buffer[temp] = Head;}count++;count %= 4;}
我们先看第一版本的。
#include "processbar.h"
#include <unistd.h>
//#define NUM 103
//#define Body '='
//#define Head '>'
const char* lable = "|\\-/";// version 1
void process()
{char buffer[NUM] = {'\0'};int count = 0;buffer[0] = Head;while(count<=100){//打印字符数组printf("[%-100s][%3d%%] %c\r",buffer,count,lable[count%4]);//打印执行后要立即刷新屏幕,让打印内容出现在屏幕上fflush(stdout);buffer[count++] = Body;if(count < 100){buffer[count] = Head;}usleep(50000);}printf("\n");
}
这个版本的进度条是为了实现进度条写的。也就是说它就是只能看看而已,没有什么实际作用。
版本1的进度条的原理和我们打印倒计时的类似,打印倒计时我们是打印整数,打印进度条我们打印的是字符串。
我们让字符数组buffer中的内容,随着count的增加,逐渐被字符'='
覆盖。然后我们打印字符数组就ok了。这样我们就可以实现一个进度条的效果出来。
为什么我们把数组的大小设置为103呢?首先,count从0开始,直到count==100,那么也就是要进入101次循环,所以数组中就得有101个位置存储'='
,其次就是'\0'
,所以起码要有102个位置。为了保险起见,我们就设置了103的大小。
其次我们还有一个常量字符串lable,这个主要是用来模仿,加载时候有东西在转,我们用count%4主要是为了能一直打印lable中的字符。
接着我们看第二个版本的,这个要结合main.c来看
main.c
#include "processbar.h"
#define FILESIZE 1024*1024*1024//模拟场景:下载任务
void download(callback_t cb)
{srand(time(NULL)^1023);int total = FILESIZE;while(total){usleep(10000);int one = rand()%(1024*1024);total -= one;if(total < 0) total = 0;int doned = FILESIZE - total;double rate = ((double)doned / (double)(FILESIZE)) * 100.00;cb(rate);}//这里直接调用函数也是可以的//process_flush_a(rate);printf("\n");
}int main()
{// process();download(process_flush_a);//download(process_flush_b);return 0;
}
version 2
#include "processbar.h"
#include <unistd.h>
//#define NUM 103
//#define Body '='
//#define Head '>'
const char* lable = "|\\-/";// version 2static char buffer[NUM] = {'\0'};
static int count = 0;
void process_flush_a(double rate)
{int temp = (int)rate;if(temp < 1) buffer[0] = Head;printf("[%-100s][%.1f%%] %c\r",buffer,rate,lable[count%4]);fflush(stdout);buffer[temp++] = Body;if(temp < 100){buffer[temp] = Head;}count++;count %= 4;}
version2主要是为了更加贴近现实,因为我们日常中的进度条不只是单纯打印出来看个样子而已,而是为了展示某个运行的认为的进度,最常见的就是下载任务了。也就是说进度条这个东西是依附于运行中的任务的,所以我们就模拟了一个场景。
后面还有一个version3,这个版本主要是让进度条有了颜色,大家试一下就知道了。是基于version2版本改的,逻辑是差不多的,不过需要了解一下printf打印颜色的机制,这里我就不过多赘述了。