文章目录
- 编写Linux下的第一个小程序——进度条
- 进度条的样式
- 前置知识
- 回车和换行
- 缓冲区
- 对回车、换行、缓冲区、输出的测试代码
- 简单的测试样例
- 倒计时程序
- 进度条程序
- 理论版本
- 基本框架
- 代码实现
- 真实版本
- 基础框架
- 代码实现
编写Linux下的第一个小程序——进度条
在前面的基础开发工具的学习中,我们学习了几个比较重要的开发工具。
当然,对于在开发过程中常用的工具是gcc/g++、vim、make,有了这三个工具的基础后,我们就可以尝试着写一个在Linux系统下开发的小项目了。
而本篇文章将讲解在Linux系统下开发的小程序——进度条。
进度条的样式
在这里,我们得明确进度条的样式。有了进度条的样式之后,我们才好根据我们要求的样式进行相对应的程序包编写。
进度条在命令行上的样式大致就是这个样子,但是对于进度条而言,需要特别注意的是:
1.旋转光标应当一直在旋转,以表明当前进度仍在进行(只不过某段时间内进度为0)
2.百分比应该动态的增大
3.进度条展示的位置应该是有东西不断地再填充中间的空位,从肉眼上看的效果就是进度条再动态的前进,其实背后是显示屏在不断地刷新和字符不断地填充输出而已。
基于上述的原理,我们就可与依次来编写对应的程序。
前置知识
但是,当前我们还得再补充一些基础的知识,这是针对于IO流的输入和输出的问题。
回车和换行
首先,我们来重新了解一下回车和换行。
在我们以往的认知中,我们会很自然地认为”回车“和”换行“是同一件事情。其实不是的。
就以我们写作文的作文格子来举例,如上图所示
单纯的换行是把笔尖直接置到同一列的下一行的位置。
单纯的回车是把笔尖置到这一行的第一个开头位置。
回车换行先把笔尖置于开头后再进行换行。
这也很清楚的说明,换行和回车其实不是一样的操作。但是为什么在c/c++程序里面,打印‘\n’确实出现了回车 + 换行的操作呢?
这是因为编译器自行解析了,编译器在碰到换行符的时候,会解析成“回车 + 换行”。
在c/c++程序中,换行符是‘\n’,回车符是‘\r’
c/c++碰到‘\n’会自动解析成‘\r’ + ‘\n’。
缓冲区
这个概念其实早在c语言学习的时候就讲到过了,但是我们这里还是再进行一次复习,以便后面顺利编写进度条的代码。
我们来看看下面这一个代码:
#include<stdio.h>
#include<unistd.h>int main(){printf("Hello World");sleep(3);return 0;
}
我们来看看这个代码的输出结果:
暂停了三秒钟…
我们会发现,显示器上会暂停三秒钟,也就是在这三秒钟内是没有任何的输出的。但是三秒后,Hello World就被输出在了显示器上,但是没有换行效果。
可是我们学过c/c++程序的代码执行顺序,是从上往下执行的。也就是说,printf函数必然是比sleep函数先执行的。那么printf执行后,打印的内容放在哪里呢?答案就是缓冲区。
其实我们之前讲过,Linux下一切皆为文件。即使是我们的显示器(FILE* stdout
)。printf是先把内容输出到缓冲区的。缓冲区再输出到显示器上是执行行刷新原则,即碰到换行符才会把内容给输出到标准输出流这个显示器文件上。
上面那一段代码就是先把Hello World写入缓冲区,但是又没碰到换行符。所以就没有办法先把字符串输出到显示器上。
但是后面能打印出来的原因是——程序结束后会自动刷新缓冲区,所以内容就会被输出了。
–
当然,如果我们想要自行刷新缓冲区从而输出字符串也是可以的,可以使用函数fflush,我们可以对stdout进行刷新(fflush(stdout)):
此时我们就可以发现,字符串先打印出来到显示器了,然后再进行休眠。
对回车、换行、缓冲区、输出的测试代码
在写进度条项目之前,我们还是需要进行一些前置准备——测试代码。
我们上面仅仅是理论上讲解了回车、换行、缓冲区的知识,这些其实和系统层面是有关系的。我们以前很多时候写的程序是停留在语法层面上的。所以对系统层面上的一些内容不是很清楚。在这里我们需要进行相关功能的测试,结合对输出的理解,这样子后面写进度条这个小项目的时候写起来就快很多了。
简单的测试样例
我们下面看第一个测试代码:
#include<stdio.h>
#include<unistd.h>
int main(){printf("%d\r", 1235456);fflush(stdout);sleep(3);printf("%s\n", "xx123456");return 0;
}
我们来看看这个代码,最后的输出效果是怎么样的:
我们会发现,这里会先打印出123456这个数字,停留三秒后,由于回车符的作用,光标会重新放在开头处,然后再次打印字符串”xx123456“的时候就会把原先打印的字符给覆盖掉。
所以这里的输出效果是,先在一行中打印数字123456,停留三秒后,打印字符串”xx123456“,覆盖掉原来的123456,最后输出结束。
这个代码结合了回车、换行、缓冲区的知识,能够更好地帮助我们理解这些内容。
倒计时程序
上面的测试代码已经初步地了解和掌握了换行、回车、缓冲区的一些相关的用法。但是还有一些问题是我们在写进度条的代码的时候会遇见的。
这些问题其实在这个简单地倒计时程序里就可以体现出来,接下来我们一起来看一下:
初版代码:
#include<stdio.h>int main(){int i = 9;while(i >= 0){printf("%d\n", i);--i;}return 0;
}
这个代码费城非常简单,在这里就不多说了。就是输出倒计时9 ~ 0,只不过是会换行输出。这里写这个代码的是为了和后面的改进版本进行对比:
但是我们想要的倒计时不是这个样子的,我们希望的是,每次屏幕上闪动动一个数字,一秒后在相同的位置变成下一秒的数字,那这个时候该怎么办?
版本二:
这个不久正好用到了上个部分测试样例中用到的原理吗?我们使用回车符就可以了,然后每次控制打印的数字的显示时间即可:
#include<stdio.h>
#include<unistd.h>int main(){int i = 9;while(i >= 0){printf("%d\r", i);sleep(1);--i;}return 0;
}
但是我们发现一个问题,就是不打印。这个其实就是没有刷新缓冲区的原因:
添加了刷新缓冲区的代码后,我们发现,打印完最后一个数字0后被命令行覆盖掉了。因为命令行的前面这一串内容也可以看作是字符串,但是我们刚刚输出的数字倒计时没有进行换行,所以最后命令行字符串打印的时候直接覆盖掉了,所以我们可以自行添加一个换行符:
#include<stdio.h>
#include<unistd.h>int main(){int i = 9;while(i >= 0){printf("%d\r", i);fflush(stdout);sleep(1);--i;}printf("\n");return 0;
}
上面的代码看似没有问题,但其实还有一些小问题没有解决,比如我们把i改成10看看:
我们会发现,打印完10之后,剩下的数字都会打印在开头的位置,但是末尾会有一个0出现。
这个原因其实很简单,只是我们得明白一个原理:
所谓的打印,其实就是打印字符!
我们将123456打印在显示器上,其实并不是把数字打印在上面,而是把123456字符打印在了显示器上而已。所以 这里也是一样的道理。10是两个字符,但是0 ~ 9是一个字符,所以10正常输出后,剩下的0 ~ 9因为回车符\r的原因,会在行开头位置打印,但是因为占位是1个字符,所以后面那个0没办法被覆盖掉。
解决办法很简单,改一下printf函数的打印格式就好了:
打印的时候在占位符前面加一个数字2,表示一次性打印的字符位宽为2(这个数字取决于倒计数里面最大的那个的数位),不足的就填充。但是这样子我们会发现,个位数打印的时候左边是空的,这非常不美观。
所以这就用需要用到我们在printf函数中学到的“左对齐”打印了:
最终版本:
#include<stdio.h>
#include<unistd.h>int main(){int i = 10;while(i >= 0){printf("%-2d\r", i);fflush(stdout);sleep(1);--i;}printf("\n");return 0;
}
printf函数默认是右对齐的,所以字符不够的时候前面会被填充。所以这里我们可以在数字的前面加一个-,这样子就会左对齐了:
至此,我们就完成了倒计时程序的编写。
同时,我们还进一步地用实践来验证了一我们前面所讲的一些前置知识,有了这些前置知识,我们就可以更好地去编写我们对应的进度条程序了。
进度条程序
这个部分我们将一起来编写进度条的程序,就按照我们在前面提及的样式进行编写。
在这里我们会给出两个版本:理论版本和真实版本。
理论版本就是我们现在只负责写进度条的变动逻辑。两个版本的差异呢?这是因为理论版本只写了进度条的变动逻辑,但是进度条这个东西一般都是配合一些任务进行的,进度跳的逻辑应该是穿插在某些程序的内部的。
所以由此得知,理论版本的进度条其实是没办法运行的,就只能作为进度条的跳动的理论展示而已,所以我们会分开两个版本进行编写。
但是写出来理论版本就好办多了,因为真实版本也就是把进度条的逻辑嵌套在其他的代码中。
理论版本
我们先来看理论版本的代码。
基本框架
我们需要四个文件:
Process.h 包含进度条代码需要用到的头文件、变量、声名进度条展示函数
Process.c 定义进度条展示的函数
main.c 对编写的进度条代码进行测试
Makefile 进行自动化编译
对于Makefile的编写,其实我们早就学习过了。
而且我们在Makefile的学习中,我们把对应的Makefile的形式改的更加通用,所以我们只需要修改一下文件中的部分文件名即可:
//Makefile
BIN=Process.exe
SRC=$(wildcard *.c)
OBJ=$(SRC:.c=.o);
FFLAG=-o
FLAG=-c
RM=rm -rf$(BIN):$(OBJ)gcc $(FFLAG) $@ $^
%.o:%.cgcc $(FLAG) $<.PHONY:clean
clean:$(RM) $(BIN) $(OBJ)
其实最后我们发现也就是修改了一下生成的可执行文件而已。
接下来是项目的主体框架:
//Process.h
#pragma once
#include<stdio.h>
#include<unistd.h>
#include<string.h>#define Num 101
#define STYLE '='void Process();//Process.c
#include"Process.h"void Process(){}//main.c
#include"Process.h"int main(){Process();return 0;
}
代码实现
首先,我们得知道进度条的变动的原理,其实就是打印一个字符数组。那是如何做到进度条的前进的呢?
其实就是不断地打印这个进度条,把上一次的进度给覆盖掉,每次打印的进度条里面需要填充更多的符号以表示进度跳动而已。
我们在这里先看代码,然后再详细解释:
void Process(){char buffer[Num];memset(buffer, 0, sizeof(buffer));int i = 0;char lable[] = "/-|\\";int length = strlen(lable);while(i <= 100){printf("[%-100s][%d%%][%c]\r", buffer, i, lable[i % length]);fflush(stdout);buffer[i] = STYLE;usleep(10000);++i;}printf("\n");
}
首先,我们确定了进度条的最大百分比是100%,所以需要100个空间打印字符(表示进度),这些字符是放在一个char数组内的,本质就是字符串。打印的时候需要碰到\0才会停止,所以存储进度条字符的数组是需要101个空间的,所以Num的值为101。
而且我们把buffer这个数组中的所有值初始化为\0,这样子后面我们就不需要再处理字符串的\0问题了。
在这个理论版本,我们就假设进度条每次进度+1%,所以打印百分比很简单,就是打印我们定义的计数器i。
当然,我们还需要设置一个旋转光标,以表示当前进度还在继续,只不过是进度很慢而已,所以需要一直旋转光标。光标的旋转其实也是字符的快速打印,把中间间隙去掉,在我们的肉眼来看其实就是旋转的效果,所以定义了一个label数组,里面就是光标旋转的时候可能会有图案。但是这里要注意的,由于 \ 这个字符本身在c/c++程序中有转义字符的意思,所以要想打印出 \ ,需要对其再加一层 \ 的转义。
对于光标的打印,其实就是不断地打印label数组中的字符,但是不能越界访问label数组,所以需要对坐标进行取模运算。
然后每次打印完进度条后,就应该改动进度条,以便下一次的打印。在这个理论版本中,就是不断地往这个数组内部填充字符。
还有一个点就是,由于在printf函数中%是被赋予了特殊意义,所以只写一个%是打印不出来的,要想打印出来%就需要写两个。
当然上面还有一些细节我没有讲到,但其实都是在前面的前置知识中有说过的。所以在这里就不再过多的赘述了。
最后,我们来看几张效果图:
这里就随意的选取几张图看看,感兴趣的可以自行拷贝实验一下。
真实版本
前面是理论版本,也就是我们完成了进度条的动态操作。但是这个程序一般都是放在一些下载/上传等场景下配合使用的。所以这个部分我们就来模拟一下下载场景。
基础框架
还是一样,我们需要准备四个文件:
ProcessBar.h 包含进度条代码需要用到的头文件、变量、声名进度条展示函数
ProcessBar.c 定义进度条展示的函数
main.c 定义下载函数和其相关变量
Makefile 进行自动化编译
有了前面的基础,这里就不过多的废话了:
//Makefile
BIN=Load.exe
SRC=$(wildcard *.c)
OBJ=$(SRC:.c=.o);
FFLAG=-o
FLAG=-c
RM=rm -rf$(BIN):$(OBJ)gcc $(FFLAG) $@ $^
%.o:%.cgcc $(FLAG) $<.PHONY:clean
clean:$(RM) $(BIN) $(OBJ)
//main.c
//使用随机数来模拟每次下载的量 需要用到下面两个头文件
#include<stdlib.h>
#include<time.h>
#include"ProcessBar.h"
#define TOTAL 1024.0//总的下载量void Download(){}int main(){Download();return 0;
}//ProcessBar.h
#pragma once #include<unistd.h>
#include<string.h>
#include<stdio.h>
#define Num 101
#define STYLE '='static int lable_move = 0;//用于光标数组的坐标void Process(int cur, double total);//进度条函数//ProcessBar.c
#include"ProcessBar.h"void Process(int cur, double total){}
但是这里需要稍微解释一下,这里的进度条打印函数相比理论版本是多了两个参数的。即当前下载的量cur和总共要下载的量total。
因为当前这里的进度条函数是嵌套在下载函数Download当中的,但是进度条最终的是什么?是进度。进度是需要计算的。这里的进度条需要展示下载的进度,所以需要获取到当前下载的量和总共要下载的量,以便能够在内部进行计算。所以这里需要传入两个参数。
代码实现
接下来我们将对代码的实现进行讲解。
首先我们得明白,在真实版本下的进度条,我们是不需要想理论版本那样,去进行屏幕沉睡(sleep函数)的控制,也不需要我们自己去计算下一次的进度。因为这些事情都是下载函数Download去操作的。
换而言之:Process函数只负责接收当前的进度和总任务量,进行进度的计算,然后根据计算量去显示进度条即可!
这里我们也是先直接展示代码:
//main.c
#include<stdlib.h>
#include<time.h>
#include"ProcessBar.h"
#define TOTAL 1024.0void Download(){srand((unsigned int)time(NULL));int speed = rand()%10;//0 ~ 9int cur_load = 0;while(cur_load <= TOTAL){//打印进度条Process(cur_load, TOTAL);usleep(20000);//sleep(1);// if(cur_load == TOTAL){printf("\n");return;}//重新计算下载量cur_load += speed;if(cur_load > TOTAL) cur_load = TOTAL;speed = rand()%10;}
}int main(){Download();return 0;
}//ProcessBar.h
#pragma once #include<unistd.h>
#include<string.h>
#include<stdio.h>
#define Num 101
#define STYLE '='static int lable_move = 0;void Process(int cur, double total);//ProcessBar.c
#include"ProcessBar.h"void Process(int cur, double total){char buffer[Num];memset(buffer, 0, sizeof(buffer));char lable[] = "/|-\\";int length = strlen(lable);double Precent = (cur * 100 / total);int i = 0;for(i = 0; i < (int)(Precent); ++i){buffer[i] = STYLE;} printf("[%-100s][%.2lf%%][%c]\r", buffer, Precent, lable[lable_move]);fflush(stdout);++lable_move;lable_move %= length;
}
我们下面来解释一下上述代码的逻辑:
首先对于main.c来说,其实就是写一个Download的下载函数,然后main函数中调用一下即可。但是这里我采用了随机数的方式来获取每次的下载量(speed),让下载量有所不同。
但是这里就需要注意一个问题了,很有可能某一次打印完进度条后,再接收下载量可能就超过TOTAL了,这样子下一次就不会进入循环了。但其实还是有最后一小部分的进度是没有显现出来的。所以当下载量speed大于剩余可以下载的量时候,就需要调整speed大小。然后为了能够进入循环,所以循环条件内,下载量cur_load是需要可以等于TOTAL的。
但是又因为我们对下载量进行特殊处理,如果我们打印完进度条(Process函数)后,不进行判断,就会导致死循环(因为剩余下载量0 <= speed)。所以这里的循环其实是内部进行退出的。不这么写会导致死循环的。
然后就是退出循环的时候,需要打印一个换行符,这样子才能使得进度条单独成一行。
然后就是Process函数的处理,其实就是修改一下理论版本的进度条函数就可以了。
只不过就是我们需要自行的计算进度,然后把进度对应的字符数填入数组后再打印数组。
但是这里还有一点是不一样的,就是光标的旋转。前面写理论版本的时候我是偷了一个懒,直接拿计数器取余i就获得了label数组的下标了。
但其实这样子不对,光标应该是有一个它自己的独立的坐标数的,即使有时候进度条进度增加为0%,但是它也应该在转,这表示当前进度仍在进行。如果写成理论版本那样就是进度增加为0%的时候光标是不转的。只不过理论版本的计数器i一直在变,所以光标也会一直转。
所以这里需要一个计数器,每次打印进度条就获取对应位置中label数组的元素。但是这里是需要使用一个静态变量的。因为静态变量存储在静态区,每次++后是会被保存下来这个状态的。如果是使用栈区上的局部变量,那每次进来都是重新初始化的,这不符合要求。然后再注意这个坐标不要越界即可。
最后,我们还是来看几张效果图:
这里也是随机截的几张图,我们发现确实是我们想要的结果。
至此,我们所有的关于进度条的内容就讲完了。