前言:
- 在之前的学习中,我们已经基本掌握了关于了 Linux 下的一些工具的使用,接下来我们运用之前学到的知识,我将带领大家写了一个关于 进度条 的小程序来练练手!!!
(一)理解 \r && \n
在我们正式的写进度条之间,我先给大家理清一下这两个概念,带大家看看到底什么是
\r \n
1、可显字符 和 控制字符
在我们之前学习的 C语言中,有很多的字符。但是在宏观上大概可以分为两类字符,它们分别是:
- a)可显字符:当我们从键盘上输入这个字符时,显示器上就可以显示这个字符,即输入什么就显示什么。这类字符称为可显示字符,如a、b、c、$、+和空格符等都是可显示字符。
- b)控制字符:通常表示出现于特定的信息文本中,表示某一控制功能的字符。例如我们今天要讲到的 \n \r 等字符
不知道大家发现没有,不管是在我们日常敲代码又或者像我此时写文章的时候,当我们写完一行后若是没有 自动换行功能 此时就需要敲下键盘中的【Enter
】键以此来达到换行的效果。可是对于这个按键,大家可能都认为就是简单的敲一下键盘上的一个键,但是实际上在计算机内部是做了两件事的,即 —— 【换行】+【回车】。具体如下:
- \n:表示新起一行,此时光标位于行末 (换行)
- \r:表示回到当前文本行的最开始处(回车)
此时可能就有点小伙伴会有疑惑,说不对呀!我之前写 C语言的时候就是 printf打印 \n 之后就可以了呀,你这里怎么说是有两部呢?
- 那是因为各位当前的语言范畴它呢就把 \n 就默认成为了 回车加换行,所以呢你看到的就是这个样子
现在我们知道了进行“换行操作” 其实是经历过两步的。其实很早之前在我们的老式键盘上就已经体现出来了,不知道各位小伙伴有没有仔细观察过呢?
2、代码演示
当我们知道现象后,接下来我们就需要去验证一下,看我所说的是否是真的。接下来,我写几行代码给大家演示一下
- 注意:因为在之前的讲解中,我们已经知道了 【vim】【make\Makefile】以及【gcc】的基本使用,接下来我们就不解释这些工具的使用了
a)首先,我们的代码是先写出基础的【Makefile】,我们在创建一个【test.c】文件用来写代码
b)紧接着我们先写【Makefile】,同时在【test.c】文件中写入两行代码。具体如下:
c)最后,我们可以执行这个程序,看最后的结果是如何的
现象解释:
- 从上我们不难发现一个事情。最后带有 【\r】的一行没有打印出来。
- 原因就是因为【\r】将光标回到最开始的位置,你可以理解为本来是应该打印 【hello hello】,但是光标此时回到最左侧,最后刷新显示的时候就什么都没有了!
到此,具体的现象我就带大家看到了,原因我也给大家分析了。
- 但是这就完了吗?其实并没有,我们想要充分理解,我们还要学习一下关于缓冲区的概念
因此,接下来我们将要学习的便是关于缓冲区的基本知识了!!!
(二)缓冲区的理解
1、什么是缓冲区
- 缓冲区又称为缓存,它是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。
- 缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区。
2、为什么要引入缓冲区
那么当我们有了关于缓冲区的概念之后,此时我们就会想为什么会引入 “缓冲区” 这个概念呢?
举个简单的例子:
- 当我们从磁盘里读取信息时,我们先把读出的数据放在缓冲区,计算机再直接从缓冲区中取数据,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数;
- 再加上计算机对缓冲区的操作远快于对磁盘的操作,故缓冲区的使用可大大提高计算机的运行速度。
3、代码演示,引出现象
有了上述的基本认识之后,接下来我们通过简单的代码观察其中的现象,让大家有直观的感受,进而我们在深入探讨
- 在这之前先认识两个Linux下的库函数
sleep() 函数:——>睡眠
- 头文件包含在
<unistd.h>
中 - 注意区分一点:Windows中的 sleep() 单位是 毫秒;而Linxu中的sleep()单位是 秒
fflush()函数 ——> 刷新流
- 该函数一般用来刷新输出流 ->stdout
- 格式:
int fflush(FILE *stream);
还是以上述验证字符的代码为例,我给几段代码以及输出现象,大家先感受一下
a)代码
1 #include<stdio.h> 2 #include<unistd.h> 3 int main() 4 { 5 printf("hello world"); 6 7 sleep(2); 8 9 return 0 ; 10 }
运行结果
现象描述:
- 以上代码输出的最后我们没有加上【\n】,从结果我们不难看出,我们要输出的【hello world】并没有在第一时间打印,而是在睡眠2秒后和【命令提示符】一同进行打印,这是为何呢?(注意,问题来了哟!!!)
b)代码
1 #include<stdio.h> 2 #include<unistd.h> 3 int main() 4 { 5 printf("hello world\n"); 7 sleep(2); 8 9 return 0 ; 10 }
运行结果
现象描述:
- 从上述的结果我们可以发现:和我们平时写的代码几乎没有区别的,当加上
sleep()
函数,相当于在打印输出完之后让程序 “延迟” 2秒,然后才会显示【命令提示符】
c)代码
1 #include<stdio.h> 2 #include<unistd.h> 3 int main() 4 { 5 printf("hello world"); 6 fflush(stdout); 7 sleep(2); 8 9 return 0 ; 10 }
运行结果
现象描述:
- 紧接着我们在下行加入了
fflush()
这个函数,将其放在sleep()
函数之前,也就相当于是优先刷新了一下缓冲流,此时就可以看到【hello world】立马先被打印了出来,等上2秒后才显示的【命令提示符】
d)代码
1 #include<stdio.h> 2 #include<unistd.h> 3 int main() 4 { 5 printf("hello world\r"); 6 7 sleep(2); 8 9 return 0 ; 10 }
运行结果
现象描述:
- 从上述我们可以观察到:当我们在输出语句后加上了
\r 时
,当开始执行后程序便开始睡眠, 然后在2秒睡眠后便直接打印出了【命令提示符】(注意:问题又来了哟!!!)
e)代码
1 #include<stdio.h> 2 #include<unistd.h> 3 int main() 4 { 5 printf("hello world\r"); 6 fflush(stdout); 7 sleep(2); 8 9 return 0 ; 10 }
运行结果
现象描述:
- 通过上述现象我们不难发现:通过
fflush()
刷新流,我们提前显示了一下需要打印的数据,此时就可以清晰的观察到,其实我们原本要打印的数据是在的,结果被【命令提示符】覆盖掉了
4、深入理解,解答现象
从上述的代码展示以及最后的结果我们提出了以下几个问题,分别是:
- 不加换行符
\n
时为何是先睡眠再打印? 加上 \n
后数据会立刻显示出来,完成睡眠后才显示提示符?- 加上回车
\r
后观察不到我们输出的数据。然而刷新一下就有了?
接下来,我会一一为大家解答上述疑惑!!!
- 在解答之前,我们先来看一些需要大家了解的文字知识!
①缓冲区的类型
缓冲区可以分为三种类型:全缓冲、行缓冲和不带缓冲
- 注意:在这里我们主要讲的是关于【行缓冲】的基本知识
1、全缓冲
- 在这种情况下,当填满标准I/O缓存后才进行实际I/O操作。全缓冲的典型代表是对磁盘文件的读写。
2、行缓冲
- 在这种情况下,当在输入和输出中遇到换行符时,执行真正的I/O操作。这时,我们输入的字符先存放在缓冲区,等按下回车键换行时才进行实际的I/O操作。典型代表是键盘输入数据。
3、不带缓冲
- 顾名思义,也就是不进行缓冲,标准出错情况 【stderr】就是典型代表,这使得出错信息可以直接尽快地显示出来。
②缓冲区的刷新
当发生以下情况后,缓冲区将会执行刷新操作:
- 1、缓冲区满时会自动刷新
- 2、执行特定的函数时
- 3、行缓冲遇到换行符时也会刷新
- 4、关闭文件时,也会自动刷新
可见,缓冲区满或关闭文件时都会刷新缓冲区,进行真正的I/O操作。
另外,我们可以使用 fflush 函数来刷新缓冲区(执行I/O操作并清空缓冲区)
接下来,我就来回答一下上述我们提出的几个问题吧!
①不加换行符 \n
时为何是先睡眠再打印?
- 对于上述的代码而言,程序执行时一定是顺序执行的,因此一定是先打印输出语句中的内容;
- 那么就有很多小伙伴好奇,我们怎么看不到这个现象呢?因为它被存放到了缓冲区里,由于
sleep()
函数的缘故,导致这个缓冲区没有被刷新而已,所以它并没有丢失
②加上\n
后数据会立刻显示出来,完成睡眠后才显示提示符?
- 首先,我们需要知道一点。那就是无论我们加不加
\n
,数据都会被保存在缓冲区里。 - 因为输出的最后一个字符是 \n ,并且是往显示器里面进行打印,此外缓冲区还有很多的刷新策略。今天我们关心的是行缓冲,即->只要碰到了换行符,这就意味着此时就会把换行符之前的所有内容全部显示出来
- 所以字符串是以行缓冲的方式保存在了行缓冲区里,最后当我们要退出的时候就会显示出来,所以此时保存在冲区里的数据就会被刷新出来
③加上回车 \r
后观察不到我们输出的数据。然而刷新一下就有了?
- 对于 \r 我们可以通过上述的输出可以发现,此时光标会回到行首;
- 因为,当我们打印的时候,当遇到 \r 的时候,光标就会回到最开始,但是这些数据并没有在缓冲区中被移除,依旧是存在的;
- 当我们 sleep() 的时候,光标打印输出的内容时遇到 \r 就会回到最开始,但是程序执行完之后,shell还会打印提示符,此时就会覆盖掉程序输出的内容
(三)倒计时功能的实现
当我们领悟到上述所讲的知识之后,接下来我们先简单的实现一个——倒计时。
1、9以内的倒计时
- 我们先来简单的实现一下9以内的倒计时,思路很简单,就是循环输出即可。然后在打印完每个数字之后使用
sleep(1)
代码如下:
1 #include<stdio.h> 2 #include<unistd.h> 3 int main() 4 { 5 int i=9; 6 for(;i>=0; i--) 7 { 8 printf("%d\n",i); 9 sleep(1); 10 } 11 12 return 0 ; 13 }
运行结果如下:
上述就是最简单的倒计时实现了。但是这跟我们想象的似乎不一样啊是不是:
- 我们希望看到的是在一行实现相应的功能,并且每次输出的结果把上次输出的结果覆盖掉,那么我们应该怎么做呢?
接下来我就带大家看看怎么实现:
代码如下:
1 #include<stdio.h> 2 #include<unistd.h> 3 int main() 4 { 5 int i=9; 6 for(;i>=0; i--) 7 { 8 printf("%d\r",i); //注意这里变为了\r 9 fflush(stdout); 10 sleep(1); 11 } 12 13 return 0 ; 14 }
此时我们再去查看最终的结果:
2、10以内的倒计时实现
不知道大家觉得【0-9】和【0-10】这二者实现倒计时是否一样呢?其实是不一样的哟!!
- 眼见为实,我们把【i】的值改为10,看最终的结果是不是我们想要的那样。
结论:
- 通过上述我们不难发现一件事,当是两位数时只有前面的数字会被覆盖,10后面的这个0会始终被保留下来,最后倒计时结束后便成了
00
那么我们要如何修改才会和上述的一样呢?
- 在计算机打印时,假设我们要打印的是【100】这个数,那么是不是计算机就是按整数【100】的形式打印出来的呢?
- 其实不是这样的。在计算机打印中,它会把100 当成三个字符进行打印,即在计算机看来 100,就是由 字符 1 0 0 组成;
- 将整型数字先转换为字符串的形式,然后去遍历这个字符串,用
putc()
这个函数将字符一一地打印在显示器上
a)修改版
我们可以像如下一样进行修改,即可实现我们上述的效果。
代码如下:
1 #include<stdio.h> 2 #include<unistd.h> 3 int main() 4 { 5 int i=10; 6 for(;i>=0; i--) 7 { 8 printf("%2d\r",i); //改为%2d 9 fflush(stdout); 10 sleep(1); 11 } 12 return 0 ; 13 }
运行结果:
到此,在这里就简单的实现出来了一个倒计时的“小玩具”。
(四)进度条小程序( 🔥)
有了以上的知识铺垫,接下来就到了实现 ——>进度条小程序的实现过程了!
1、进度条样式说明
首先,就是给大家先说明一下本次进度条我们最终呈现出来的样式是什么样的。
因为,Linux下不是图形化的,因此我们这里实现的进度条就不是大家所熟知的网上看见的那种形状。
首先,我先给出我们的进度条的大概样式,最后呈现出来的就是以下这种现象:
- 【##################################>】【100%】【/】
说明:
1.主体部分大概就是用个【】来进行概括,中间用 ## 这样的符号来表示我们的进度条的进度样式;
2.后一个【】则表示相应的进度情况;
3.因为我们是在Linux环境下,无法做到这种图形化界面。最后就是用旋转字符的样式来代替我们在 Windows下的缓冲的样式
2、多文件实现
在这里我们给出的是多文件这样的实现方案。因此在正式的上手之前,我们需要创建相应的文件来表示相应的代码。
- 【proc.h】:存放头文件的文件夹
- 【proc.c】:代表头文件匹配的源文件,进度条实现的逻辑存放在这里面
- 【mainc】:来进行程序的调度
先直接给出程序的大概框架,让大家先见见:
- 接下来,我们要去编译的话,此时我们还需要创建文件列表,即【makefile】来存放,这个在之前我们已经详细的讲过了,这里就不多讲了。直接给出,具体如下:
- 紧接着,我们边去测试一下程序是否可以正常的编译运行:
此时,可能好多小伙伴就会有疑惑,在创建的文件列表【Makefile】中只有 main.c和 proc.c 而没有头文件 proc.h 文件
- 对于以上问题,之前在讲解【
gcc
】的时候我大概提到过关于这个知识点。 - 我们在进行多文件编译时候是不需要考虑【头文件】的,因为在预处理阶段头文件就会在它被包含的
.c
源文件中进行展开,因此加不加结果都是一样的。 - 一般我们是不加的
3、主体架构实现
第一步:
#define SIZE 101
解释说明:
- 因为进度条嘛!进度就是从0到100 的,将整体进度条看作是一个字符串,因此我们设置101来存放
第二步:
memset(bar, '\0', sizeof(bar));
解释说明:
- 因为实现这个进度条推演就是一个循环的方式,每次都去修改这个字符数组当前位置上的字符,将其变为 “=” ,紧接着去打印的时候下一个位置就会多出来一个 “=” ,从而实现往后慢慢推进的过程
- 初始化时就把数组全部的位置初始化为 \0 ,紧接着随着进度条的推进去修改为 “=” 即可
代码如下:
1 #include"proc.h" 2 3 #define SIZE 101 4 5 void process() 6 { 7 char bar[SIZE]; 8 9 memset(bar,'\0',sizeof(bar)); 10 11 int i=0; 12 while(i<+100) 13 { 14 printf("[%s]\n",bar); 15 bar[i++]='#'; 16 sleep(1); 17 } 18 }
- 此时我们编译一下,看最终的结果是怎么样的:
但是此时我们可以发现,这是不断的换行实现的,但是在我们的认知中进度条就是在 “一行 ”上实现的呀。因此,此时显然不符合我们的预期
- 我们的预期是在一行上进行不断推进的过程。因此,我们就不能加上【\n】这个字符;
- 我们使用【\r】,当每次打印的时候,都从当前行的起始位置处进行操作。
当我们完成上述要求之后,紧接着来编译代码看最终的结果是不是我们期望的那样,具体如下:
- 代码如下:
1 #include"proc.h" 2 3 #define SIZE 101 4 5 void process() 6 { 7 char bar[SIZE]; 8 9 memset(bar,'\0',sizeof(bar)); 10 11 int i=0; 12 while(i<+100) 13 { 14 printf("[%s]\r",bar); //此时变为\r 15 bar[i++]='#'; 16 sleep(1); 17 } 18 }
- 运行结果:
此时,出现了一个 “小坑” ,我们可以发现并没有显示出任何东西大家知道什么吗?
- 我相信聪明的小伙伴已经知道了,即 缓冲区没刷新!
fflush(stdout);
- 再次运行,结果如下:
不过此时有点小伙伴就会有这样一个问题,是什么呢?
- 我们可以观察到这个进度条推进的速度很慢,那有没有办法让它快一点呢?
答案是有的,此时有需要另外一个库函数了,那就是 【usleep】函数。
- 大家可以用 【man】手册去查一下到底什么意思,我在这里给出简略的回答:
- 代码如下:
1 #include"proc.h" 2 3 #define SIZE 101 4 #define ARP '>' 5 6 void process() 7 { 8 char bar[SIZE]; 9 10 memset(bar,'\0',sizeof(bar)); 11 12 int i=0; 13 while(i<+100) 14 { 15 printf("[%s]\r",bar); 16 fflush(stdout); 17 bar[i++]='#'; 18 usleep(100000); //变为usleep 19 } 20 }
- 运行如下:
主体的进度条预留出了一个100的空间,好呈现进度条从0 ~ 100的推进,就可以上面说到过的格式化占位符
printf("[%100s]\r", bar);
- 运行结果:
我们可以发现怎么是从反方向走的,这也不是符合我们的需求啊!
- 别急,其实很好解决,只需在 输出的 100前加上 【-】 即可实现从左开始输出。
printf("[%-100s]\r", bar);
- 运行结果:
当然我们还可以实现更多的样式,例如假设我们要实现【===>】这样的,我们可以怎么操作呢?
- 代码如下:
1 #include"proc.h" 2 3 #define SIZE 102 //记住,此时当你加入的符号过多时,空间也应该随之变大 4 #define ARP '>' 5 #define STYLE '=' //我们在这里用宏定义样式,便于我们修改 6 7 void process() 8 { 9 char bar[SIZE]; 10 11 memset(bar,'\0',sizeof(bar)); 12 13 int i=0; 14 while(i<+100) 15 { 16 printf("[%-100s]\r",bar); 17 fflush(stdout); 18 bar[i++]= STYLE; 19 bar[i]=ARP; 20 21 usleep(100000); 22 } 23 }
- 运行结果如下:
4、进度的实现
实现完主体的框架之后,紧接着我们需要去实现一下百分比递增
- 代码如下:
1 #include"proc.h" 2 3 #define SIZE 102 4 #define ARP '>' 5 #define STYLE '=' 6 7 void process() 8 { 9 char bar[SIZE]; 10 11 memset(bar,'\0',sizeof(bar)); 12 13 int i=0; 14 while(i<+100) 15 { 16 printf("[%-100s][%d]\r",bar,i); //我们只需在最后加上输出的值即可 17 fflush(stdout); 18 bar[i++]= STYLE; 19 if(i != 100) bar[i]=ARP; 20 21 usleep(100000); 22 } 23 }
- 运行如下:
但是此时我们可以发现,输出只是数字,并不是百分数啊!
- 我们只需在【%d】的后面在加上 % 即可,即——>【%d%%】
- 结果如下:
到此,关于进度的实现便完成了。接下来就是关于缓冲功能了!!!
5、缓冲功能的实现
终于到了最后。马上就要揭开我们进度条的了 ”庐山真面目“了。
- 此时我们只需要定义一个字符数组,并用【
const
】来修饰。 - 目的很简单就是防止里面的内容被修改。
const char* label = "|/-\\";
- 最后在打印的时候,要去实现一个轮回就需要用到一个取余操作,每次打印的都是【0 ~ 3】的倍数,因此模上4 即可
printf("[%-100s][%d%%][%c]\r", bar, i , label[i % 4]);
运行如下:
到此,我们就实现了一个进度条小程序的设计。最终代码如下:
1 #include"proc.h" 2 3 #define SIZE 102 4 #define ARP '>' 5 #define STYLE '=' 6 7 void process() 8 { 9 const char* label = "|/-\\"; 10 char bar[SIZE]; 11 12 memset(bar,'\0',sizeof(bar)); 13 14 int i=0; 15 while(i<+100) 16 { 17 printf("[%-100s][%d%%][%c]\r", bar, i , label[i % 4]); 18 fflush(stdout); 19 bar[i++]= STYLE; 20 if(i != 100 )bar[i]=ARP; 21 22 usleep(100000); 23 } 24 }
(五)总结
到此,关于进度条小程序的所有知识便讲解完毕了!接下来,我们一起回顾一下
- 首先我们学习了C语言中的两个控制字符【
\n
】与【\r
】,知道了这两者的作用及功能; - 紧接着我们又引出了缓冲区的概念,并且通过代码的形式给大家具体的呈现了出来。知道了缓冲区是需要被刷新才会被显示出来的;
- 有了以上知识点的铺垫,我们通过设计的倒计时的功能给大家变相的总结了上文,并且对上述知识进行了运用;
- 最后,就是总结上文,之前的一切都是在给我们即将设计的 进度条小程序做“背景板”。通过几个方面的讲述带领大家对这个小程序进行了实现。
以上就是本文的所有知识,感谢各位的支持!!!