前言:在Linux工具(一)中,我们学习了yum软件安装工具和vim文本编辑器工具,那么本次我们就再来介绍两种工具,分别是,编辑器gcc/g++、项目自动化构建工具-make/Makefile ,接着我们再来写一个好玩的Linux进度条程序。那么,话不多说,我们马上就开始~
目录
1.Linux编译器-gcc/g++使用
预备知识
gcc的完成过程
预处理阶段
编译
汇编
链接
动静态库的理解
动态库vs静态库
2.Linux项目自动化构建工具-make/Makefile
依赖关系和依赖方法
实例引入
关于make命令原理及注意事项
为何make/makefile 总是不让我们重新编译代码
三个时间属性的详解
makefile的自动推导能力
makefile的补充语法
3.Linux第一个程序-进度条
背景引入
回车和换行是一回事吗?
模拟倒计时
进度条原理介绍(v1版本)
进度条与任务绑定(核心v2版本)
main.c(模拟下载函数)
process.h
process.c (进度条打印函数)
拓展:回调函数的使用
美化(v3版本)
小数位的产生与卡顿优化
最终源码在这
process.h
process.c
main.c
1.Linux编译器-gcc/g++使用
预备知识
1.在Linux系统下,我们的c++文件有三种的后缀表示方式,分别是, .cc,.cpp ,.cxx ,而我们的gcc编译器是系统自带的,只能编译c语言,如果我们向编译c++代码,我们需要g++编译器,如果我们没有安装g++编译器,我们可以采用命令
sudo yum install g++-c++
按照提示输入普通用户的密码后即可安装完成。
2.在Linux系统下,一切皆文件,而文件的后缀名在Linux系统中是没有意义的,但是这并不代表我们的一些工具不会对文件的类型进行甄别,像编译器就会对应识别相应的文件类型是否为c或c++文件,如果类型不符合,也会导致出错。
3.关于c语言中,程序的翻译过程:编译器对c/c++源文件的处理过程:
c/c++源文件 -> 预处理 -> 编译 -> 汇编 -> 链接 -> 可执行程序
预处理:宏的替换,条件编译,注释的删除,头文件展开到源文件中,预处理完之后还是c/c++语言,
编译:把c/c++语言编译成汇编语言
汇编 :把汇编语言编译成二进制目标文件(可重定位目标二进制文件,后面还会提到)
链接:把目标文件和系统的库文件进行连接,形成可执行程序
4. 我们知道编译器是专门用来编译处理代码的,那我们来思考一个有趣的问题,到底是先有的编译器还是先有的语言呢?
最初的编译器目的是将汇编语言翻译成二进制语言的二进制编译器,而在像c/c++语言出现后,也就需要更加进一步的翻译工具能将高级语言进一步翻译成低级语言,但是我们已经有了能把汇编语言翻译成二进制的编译器了,所以,我们以后可以直接用汇编语言编写这些高级语言的编译器,然后再套用二进制编译器将汇编语言翻译成二进制语言。上面的我们的程序翻译阶段为什么一定要经过高级语言翻译成汇编语言的过程中,是因为从汇编语言到二进制的翻译过程更加成熟稳定,我们要站在巨人的肩膀上,所以这一步是根据历史过来的。
gcc的完成过程
为了详细的展示上述的程序编译过程,下面通过代码演示来模拟各个过程发生的文件和内容的变化,模拟代码如下:
预处理阶段
gcc -E code.c -o code.i
采用以上的命令运行我们编辑的代码,其中选项“-E”,该选项的作用是让 gcc 在预处理结束后停止编译过程,因为gcc本身具有让源代码编译成可执行程序的能力,如果不加限制那么它就会自动执行完四个阶段直接生成可执行程序,而选项“-o”是指目标文件,“.i”文件为已经过预处理的C原始程序,如果不采用 -o 指令将翻译生成的文件保存在对应的 .i 文件中,否则将会直接打印到终端。
我们可以将生成的code.i 文件和我们的源文件进行对比:(底行模式下输入vs 想要打开的文件,用ctrl +ww 切换窗口)
可见,code.i 文件中包含了我们源文件中所包含的stdio.h 的头文件,然后我们可以找到code.i 的最底行,我们可以和源文件进行对比,我们可以发现,条件编译经过预处理被选择性的执行,通过条件编译,我们源文件的代码被动态裁剪了,宏定义也直接被替换成了具体的数字,注释也被删除了。
这个条件编译的具体应用说起来我们也经常使用,比如我们熟知的网上的一些ide,比如IDEA,VScode,等,他们基本都分为专业版和社区版本,而对应的专业版本往往需要收费,那么我们需要怎么实现这个功能呢,是不是需要重新维护一个软件呢?这样成本就太大了,这个时候,就可以用条件编译,让对应的用户在选择时可以通过某个条件来实现条件编译,让对应的代码可以动态裁剪,在社区版用户在安装时可以自动将专业版中的功能代码删除。
编译
在这个阶段中,gcc 首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查 无误后,gcc 把代码翻译成汇编语言。
我们可以采用如下命令来捕获翻译成的汇编代码:
gcc -S code.i -o code.s
上述命令是将我们预处理过的c程序代码,经过编译翻译成对应的汇编语言并保存到code.s 文件中,而 .s 就是我们一般保存汇编代码的后缀。
运行后我们可以得到code.s 文件,我们用vim打开:
汇编语语言这里不做过多解释,有兴趣的读者可以自行百度。
汇编
汇编阶段是把编译阶段生成的“.s”文件转成目标文件,可使用选项“-c”就可看到汇编代码已转化为“.o”的二进制目标代码了,其实,我们可以直接从源文件到这一步,但是因为我们前面已经执行过两步,已经有了汇编代码,所以,我们可以直接用上一步生成的汇编文件来形成可重定位二进制文件。
gcc -c code.s -o code.o
//gcc -c code.c 直接从源文件形成二进制文件
查看code.o 文件,我们发现是一堆乱码,这是因为code.o 文件本来就是就不是文本文件,而是一个二进制文件,我们可以采用一些工具来查看,这里我们用 od命令,有兴趣可以查一下这个命令:
od code.o
这个我们虽然看不懂,但是我们只要知道它现在已经是一个二进制文件了,但是,这个二进制文件还不能执行,我们可以跑一下试试,
其实如果经常用vs的话,我们应该经常见到这种文件,这种.o文件其实就是vs中的 .obj文件,
链接
上面的可重定位二进制文件还不能自己运行,还需要经过链接过程,吧库文件导入,才能形成可执行文件,我们可以使用命令:
gcc code.o -o mycode
./mycode //运行代码
动静态库的理解
我们程序处理过程中的链接阶段,我们代码中的一些功能函数,都需要链接到标准库中才能使用,这是别人提前编好的,我们只是调用它,可执行程序=代码+头文件+库文件,而头文件和库文件都会在我们安装开发环境中完成,之后就可以被我们调用,其实我们安装开发环境实际上是安装下载并拷贝头文件和库文件到开发环境的特定路径下,这个路径需要被编译器自己找到 ,在Linux中,以o 结尾的一般都是静态库,以a结尾的都是动态库,一般的开发环境提供的库中,动态库居多,而在windows中,静态库一般都是 xxx.lib,动态库都是 xxx.dll。那么。动静态库怎么理解,什么又叫做链接呢?
动态库vs静态库
动态库和静态库都是在软件开发中常用的库的形式,它们有一些区别和各自的特点:
动态库:
- 动态库在运行时被加载到内存中,并可以在程序执行过程中被多个应用程序共享使用。
- 动态库的代码在内存中只有一份,减少了内存的占用。
- 动态库的更新和升级相对容易,因为只需将新版本的库文件替换即可,无需重新编译整个程序。
- 动态库在运行时被动态链接,因此程序的可执行文件较小,但是运行效率上可能要稍慢一些。
- 动态库适用于需要动态加载和运行时共享的场景,通常用于插件、共享组件、动态扩展等。
静态库:
- 静态库在程序编译时会被整体地链接到可执行文件中。
- 静态库在每个使用它的应用程序中都有一份独立的拷贝,增加了可执行文件的大小。
- 静态库的代码与程序的其他部分一起编译和链接,使得可执行文件更加独立,无需依赖外部的库文件。
- 静态库在编译时会被静态链接,因此程序的可执行文件较大,但也具有独立完整性,运行效率也相对动态链接快一些。
- 静态库适用于不需要频繁更新和共享的场景,通常用于库的稳定性和可移植性要求较高的情况。
链接的方式也分为动态链接和静态链接,静态链接比较好理解,就是将库中文件直接拷贝一份链接到我们编译生成的二进制文件,共同形成可执行程序,对于动态链接,我们知道,每一个函数都有一个入口地址,当代码需要链接库中的功能函数时,动态链接只需要找到库函数的入口地址,直接动态链接这个地址,通过地址找到这个库函数,因此形成的可执行程序和文件相对较小,而在Linux下,默认情况下都会采用动态链接的方式,比如gcc,下面我们来演示一下gcc动静态链接的差异,
并且,我们的上面实例里的代码量还只是仅仅写了几个printf而已,如果放在工程上的话,差异将会更加明显(默认情况下,我们的系统不会安装静态库,我们可以自行百度进行安装对应语言的静态库,比如C++的静态库的安装 sudo yum install -y libstdc++-static )。
从上面的讲解中我们不难理解,我们安装一个开发环境,默认的都要为我们做什么?
1.下载开发环境,比如include头文件等;
2.设置合理的可查找路径,方便预处理时头文件展开到源文件中(头文件展开是按照系统默认的一个路径查找并拷贝过来的,这个特定的路径提前被编译器设置好并在下载时存放到该路径中)。
3.绑定程序的链接方式,比如静态链接的拷贝和动态链接的地址拷贝。
2.Linux项目自动化构建工具-make/Makefile
一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作 makefile带来的好处就是——“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。 make是一个命令工具,是一个解释makefile中指令的命令工具,一般来说,大多数的IDE都有这个命令,比如:Delphi的make,Visual C++的nmake,Linux下GNU的make。可见,makefile都成为了一 种在工程方面的编译方法。
依赖关系和依赖方法
现实中,依赖关系+依赖方法就能够描述清楚一件事情,比如我们的上面的程序处理的过程中,可执行依赖code.o的形成 ,code.o 依赖code.s ,code.s 的形成依赖于code.i ,而code.i 又依赖于code.c 的形成。这个就叫做依赖关系,而依赖方法就是在依赖关系下做的事情,比如在依赖关系下code.c 形成 code.i ...... ,这个就叫做依赖方法。
实例引入
说了那么多,没有具体的实例,不好理解,那我们就先来一个简单的实例来快速了解make/Makefile,
1.创建并编写makefile文件,这里的文件名字就只能叫makefile,字母不能错,但是可以忽略大小写,因为make命令只能识别makefile名称的文件并执行它,
2.编写makefile代码,其中有以下几点需要注意:
1,第一行不能空着,需要从第一行开始
2,编写依赖方法时,前面的空格需要时按一个tab键所空出来,不能是四个空格,也不能是别的,就只能是一个tab键。
3.像clean这种,没有被第一个目标文件直接或间接关联,那么它后面所定义的命令将不会被自动执行, 不过,我们可以显示要make执行。即命令——“make clean”,以此来清除所有的目标文件,以便重编译。但是一般我们这种clean的目标文件,我们将它设置为伪目标,用 .PHONY 修饰,伪目标的特性是,总是被执行的,没有依赖关系,当我们把mycode目标用以 .PHONY修饰时,那么该命令总是会被执行。
4,make指令后面不带东西默认自顶向下扫描,只执行扫描到的第一项,如果想要执行非第一个目标,就要在make后面加上想要实现的目标名称,下面的make clean 就会执行清理的功能,跟默认一个效果的,我们也可以输入 make mycode 。
3.使用make命令来编译运行代码
关于make命令原理及注意事项
1. make会在当前目录下找名字叫“Makefile”或“makefile”的文件。
2. 如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到“mycode”这个文件, 并把这个文件作为最终的目标文件,而伪目标clean不会形成,如果我们把clean和mycode调换一下位置,也就会先执行clean,mycode目标不会被执行。
3. 如果 mycode 文件不存在,或是mycode 所依赖的后面的code.c 文件的文件修改时间要比hello这个文件新(可以用 touch 测试,touch指令不仅可以用来创建新文件,还可以更新文件的时间),那么,他就会执行后面所定义的命令来生成mycode这个文件。
4. 如果mycode所依赖的code.c 文件不存在,那么make会在当前文件中找目标为code.c文件的依赖性,如果找到则再根据那一个规则生成code.c文件。
5. 当然,code.c 文件存在,于是make会生成 mycode 文件,然后再用 code.c 文件声明 make的终极任务,也就是执行文件mycode了。
6. 这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。
7. 在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make就会直接退出,并报错, 而对于所定义的命令的错误,或是编译不成功,make根本不理。
8. make只管文件的依赖性,即,如果在我找了依赖关系之后,冒号后面的文件还是不在,那么对不起, 我就不工作啦。
为何make/makefile 总是不让我们重新编译代码
在我们的源代码未被修改的前提下,我们尝试第二次make来编译我们的代码时,我们会发现gcc不让我们再重新编译了,这是因为gcc为了提高编译效率,编译实际上是一个很费时间的过程,我们当前演示的只是一个源文件的简单代码,那么问题是gcc如何知道我们的源文件有没有被修改的?
毫无疑问的是,gcc可以判断源文件的某些属性,来判断这个文件有没有被修改过,在我们上面的例子中,gcc会对比目标文件(mycode)和源文件(code.c)的更新修改时间,当目标文件的修改时间大于源文件的修改时间,则会被判定为文件没有改变,反之,则文件改变,目标文件就可以被重新编译,但是对于上面提到的被 .PHONY 修饰的目标文件,其依赖方法总是会被执行,不再受时间属性的约束,这就是总是被执行的意思。下面我们了解一下文件的这个时间属性:
三个时间属性的详解
命令 stat+[文件名] 可以查看文件的时间信息,无论是可执行程序,还是其他文件,用该命令都可以查看对应文件的属性,例如,我们来查看code.c 的属性:
我们看到这里面有三个时间属性,这三个时间属性的表示的范围有不同:
Access :表示该文件的最近访问时间
Modify :表示最近对文件的内容作修改的时间
Change :表示最近对文件的属性做更改的时间,文件=内容+属
注意事项:一般而言,一个文件被查看的概率是非常高的,而文件的内容和属性都存放在磁盘里,文件的访问时间实际上就是最近一次访问磁盘的时间,但是,一直对磁盘进行访问,会比较慢,为了快速访问,计算机中存在通过cache来代替访问磁盘以提高文件访问的效率,所以,对于Access time,可能会存在更新不准确的情况,均为正常现象。
这三个时间属性之间可能会相互影响,Modify time更改可能导致文件的属性做更改(比如文件大小),这就可能会影响Change time的改变,但是我们修改文件的属性(比如文件的权限修改),只会改变Change time,不会影响其他两个时间属性。
知晓了上面的三个时间属性的存在,我们很容易知道,判断文件的新旧靠的就是文件的Modify time (最近一次内容修改的时间),gcc的对比文件的新旧,也是靠不同文件的Modify time来比较。
makefile的自动推导能力
我们可以将makefile修改成我们上面的程序执行过程中各种文件步步推进形成可执行的形式:
这里体现除了makefile的推导能力,有点类似递归,对应着我们的程序处理过程,只不过是反过来的,这样一层层的找下去,最后从能够找到的源文件开始再逐层向上返回执行,执行makefile,就会直接形成code.i ,code.o,code.s 和最后的可执行程序,但是并不建议这样使用,这样使用的目的,是在于应对多问价同时编译时推导的。
makefile的补充语法
1.可以使用 @ 符号取消对依赖方法的打印,但是不会影响原来打印语句,如 echo 的打印操作
2.makefile中允许我们自己编写变量,类似于宏定义,后序在使用这个变量符号是要以 $(自己起的变量名)来引用替换,也是可以正常运行的,
为什么多此一举呢?这里就和宏定义的优点相似,我们可以自定义我们的编译器,源文件等等,未来修改时,只需要修改定义处的变量即可修改整个代码段的变量。
3.依赖关系支持简写,在依赖关系中已经出现的目标文件和所需文件已经知道的情况下,我们可以用 $^ 代表上面的已知文件, $@ 代表所需形成的目标文件。
3.Linux第一个程序-进度条
背景引入
我们先来看一个特别的程序
这个程序我们可以输出结果不言而喻,但是我们在输出时发现是sleep先执行的,不信你可以试一试,然后接着打印出了字符串,这就和输出缓冲区有关了,下面我们来详细说一下:
其实对于以上的代码,逻辑上一定是printf 语句先执行,只不过在执行时,printf 将要打印的语句不是直接显示在控制台,而是先暂时放在了输出缓冲区里,等到sleep执行完毕后,系统发现程序要结束了,所以就将输出缓冲区里的东西才打印出来到控制台。
输出缓冲区需要在一定的条件下才能刷新,将里面存放的数据打印到控制台,这里我们可以采用一个函数 :fflush(stdout); 来刷新输出缓冲区,这样,我们的字符串就能直接输出而不会在延迟等待了,同样的,我们平时用的换行符 \n 就是一种强制刷新缓冲区的方式,可以将缓冲区的字符串立即显示出来。
回车和换行是一回事吗?
日常生活着,我们常常把回车和换行都默认当做进入下一行的开头,但是回车和换行其实是两个概念,回车就是光标回到本行的头部,换行其实是光标直接原地移动到下一行对应原上一行光标所在的位置上去,这两者合起来才是我们日常所说的换行的行为,在C语言中,只是回车的字符我们用 '\r' 表示,回车换行用的是 '\n' 。
模拟倒计时
‘\r’ 的作用是回到当前行的首部,所以就会造成不可避免的覆盖问题,我们可以利用这原理,来实现向控制台动态打印倒计时的效果,假设我们从10开始打印,那么我们需要的前提条件是一个数组占两位,这样,覆盖时才能使得9 能覆盖 10,
进度条原理介绍(v1版本)
按照上面的逻辑思考,我们可以让我们的进度条以一个字符的最小单位进行输出,我们就以 # 为例,我们输出时,第一次输出一个 # ,然后第二次回车再输出两个#,覆盖前一个输出的 #,这样就达到了动态显示,并且进度一直在增长的效果,按这个思路,我们给出代码实现,其他的一些细节也也均为c知识,所以直接在代码里给出:
进度条与任务绑定(核心v2版本)
生活中的进度条一般都和具体的任务绑定执行,通过进度条来梵音任务执行的进度,但是上买的呢程序显然是一个静态的进度条,它只会以我们设定的速度跑完百分之百就停止,并不会受其他任务进度的影响,现在,我们就来模拟进度条和任务绑定执行的场景,
想要与任务绑定,就必须由当前任务进入来决定进度条的前进速度,那么我们需要如何来模拟这个下载任务呢?首先,我们知道,在我们下载东西时,除非网络特别流畅,否则我们多多少少都会遇到一些进度卡顿的情况,在一些情况下进度突然卡住(mhy,卡岩多久了还不优化?),我们还没有学习网络这一块的知识,但是,我们可以用一个时间函数的随机时间停止来模拟网络环境的随机性卡顿效果,下面我们来介绍一下这个函数:
这里的usleep,我们可以在括号里设置参数,该参数的单位是ms,相比于我们熟悉的sleep函数来说,其让控制台停止的时间控制更加精确,我们这里可以以1000位最小单位,也就是1s来控制,
接着,我们就需要对下载任务进行处理,如何模拟下载任务才能做到随机性呢?我们可以通过给常量设置随机的乘数来确保常量的伪随机性,这样,问题就能很好地解决,关于随机数的生成,这里我使用的是最简单的srand和rand函数的模式来实现的,其次,下载任务一般都是在主进程任务中进行,我们就将其放在main函数中实现,而将打印进度函数放在特定的process的源文件下,
main.c(模拟下载函数)
#include"process.h"
#include<stdlib.h>
#include<time.h>
#define target (1024*1024) //待下载任务总量
#define loadbaserate 1024 //基础下载速率,搭配随机值
void download()
{
int total=0;
srand(time(0));
printf("\033[?25l"); //隐藏光标
while(total<target)
{
//我们可以用随机数来模拟下载卡顿情况
total+=loadbaserate*(rand()%(50-0+1));//表示我们的下载速率在0-1024*50之间
int rate=(total*100)/target; //注意这里的宏一定要加括号,不然除以target等于没除,我因为这个地方半天没看出来
// printf("%d %d\n",total,rate);
process_v2(rate);
}
printf("\n");
printf("\033[?25h"); //显示光标
}
int main()
{
// process(); //version 1
download();
return 0;
}
process.h
#pragma once
#include<stdio.h>
#include<string.h>
#include<unistd.h> //定义usleep函数的头文件
#include<stdlib.h>
void process();
void process_v2(int rate);
process.c (进度条打印函数)
#include"process.h"
#include<stdlib.h>
#include<time.h>
#define size 100
char flag[3]={'\\','/','~'};
void process()
{
printf("\033[?25l"); //隐藏光标
//version 1
char bar[size+1]={0};
//memset(bar,'\0',sizeof(bar));
int idx=0;
while(idx<=size)
{
printf("[%-100s] [%d%%] [%c]\r",bar,idx,flag[idx%3]);//向左对其,占100个字符,显示当前百分比
fflush(stdout);
usleep(1000*150);
bar[idx++]='#';
}
printf("\n");
printf("\033[?25h"); //显示光标
}
//version 2
void process_v2(int rate)
{
static int idx=0;
static char bar[size+1]={0}; //static 静态数组可以让上一次数组中的内容得以保存
srand(time(0));
//printf("\033[?25l"); //隐藏光标
// printf("%d %d\n",idx,rate);
//如果rate没有超过idx,说明下载进度出现卡顿,此时应该继续向屏幕打印上一轮的进度
if(rate<=idx)
{
printf("[%-100s] [%d%%] [%c]\r",bar,idx,flag[idx%3]);//向左对其,占100个字符,显示当前百分比
fflush(stdout);
usleep(1000*50*(rand()%(10-0+1))); //卡顿时间一般要久一点,也可以设为随机值
}
if(rate>=size) rate=size;
while(idx<=rate)
{
printf("[%-100s] [%d%%] [%c]\r",bar,idx,flag[idx%3]);//向左对其,占100个字符,显示当前百分比
fflush(stdout);
usleep(1000*30*(rand()%(10-0+1))); //模拟下载速度时刻发生变化
bar[idx++]='#';
}
// printf("\033[?25h"); //显示光标
}
拓展:回调函数的使用
上述的对于download函数调用进度条打印函数我们还有另一种调用方式,回调函数,我们可以直接typedef 一个函数类型的指针,就可以直接调用打印函数来实现,这部分了解即可。
美化(v3版本)
在这个版本,我们重点在优化细节和表现形式上,在v2版本的基础上对进度条进行优化,这里我们主要优化几个点:小数百分比精确到一位,卡顿优化,还有就二十进度条颜色优化。
小数位的产生与卡顿优化
由于我们上面传进来的rate都是取的整数,所以不太好产生浮点数,为此,我们需要将其变为浮点数,然后我们还需要对打印进度条函数做一些细节上的改进,接着,v2版本的卡顿优化原因实际上与随机数0的产生有关,但是由于我们的随机数的产生范围是 [0,50] ,这就导致随机数0产生的可能性变得极小,也就间接导致了卡顿情况出现的很少甚至不出现(因为网络不可能一直都是流畅稳定的,总是要卡顿那么几下~~~)所以,我们现在将随机数的产生扩展到能够产生负值,通过判断产生的负值来确定是否需要卡顿(什么破网络呀~~),当然,我们的已下载量不可能出现减少的情况,所以我们可以将所有负数都看做进度为0,也就是已下载量较上次没有变化,下面是代码:
关于颜色优化,我们可以参考下面的资料及其示例自行修饰即可,在上面的代码中也是顺便将进度条的打印改成了蓝色,下来我们也可以修改自己喜欢的颜色。
还有就是我们可以实现色块模式的进度条,这里就不再展示了,只是提供一个思路,我们可以字符串的背景色分为两个部分,进度之前的打印一个背景色,进度之后还没有到达的设置成默认背景色,通过背景色区分进度条的状态即可。
最终源码在这
process.h
#pragma once
#include<stdio.h>
#include<string.h>
#include<unistd.h> //定义usleep函数的头文件
#include<stdlib.h>
//在打印函数头文件中命名
typedef void (*callback)(double);//将函数指针类型重命名为callback
void process();
void process_v2(double rate);
process.c
#include"process.h"
#include<stdlib.h>
#include<time.h>
#define size 100
#define precent 100.0
char flag[3]={'\\','/','~'};
void process()
{
printf("\033[?25l"); //隐藏光标
//version 1
char bar[size+1]={0};
//memset(bar,'\0',sizeof(bar));
int idx=0;
while(idx<=size)
{
printf("[%-100s] [%d%%] [%c]\r",bar,idx,flag[idx%3]);//向左对其,占100个字符,显示当前百分比
fflush(stdout);
usleep(1000*150);
bar[idx++]='#';
}
printf("\n");
printf("\033[?25h"); //显示光标
}
//version 2 版本
void process_v2(double rate)
{
static double idx=0.0;//我们以最小单位为0.1来跑进度条
static char bar[size+1]={0}; //static 静态数组可以让上一次数组中的内容得以保存
static int cnt=0;//cnt可以让我们的下载看起来不是那么卡顿,也就是后面的下载提示一直在旋转,可以有效缓解用户烦躁^-^
static int initialized = 0;
if (!initialized)
{
srand(time(NULL)); // 初始化随机数种子,仅在第一次调用时执行
initialized = 1;
}
// printf("%.1f %.1f\n",idx,rate);
//如果rate没有超过idx,说明下载进度出现卡顿,此时应该继续向屏幕打印上一轮的进度
if(idx>=rate)
{
printf("[\033[40;34m%-100s\033[0m] [%.1f%%] [%c]\r",bar,idx,flag[(++cnt)%3]);//向左对其,占100个字符,显示当前百分比
fflush(stdout);
usleep(1000*80*(rand()%(10-0+1))); //卡顿时间一般要久一点,也可以设为随机值
}
if(rate>=precent) rate=precent;//precent=100.0,我们在上面定义了,为了方便就不展示了
while(idx<=rate)
{
printf("[\033[40;34m%-100s\033[0m] [%.1f%%] [%c]\r",bar,idx,flag[(++cnt)%3]);//向左对其,占100个字符,显示当前百分比
fflush(stdout);
usleep(1000*20*(rand()%(10-0+1))); //模拟下载速度时刻发生变化
bar[(int)idx]='#';
idx+=0.1;
}
}
main.c
#include"process.h"
#include<stdlib.h>
#include<time.h>
#define target (1024*1024)
#define loadbaserate 1024
void download(callback cb)
{
int total=0;
srand(time(0));
printf("\033[?25l"); //隐藏光标
while(total<target)
{
//我们可以用随机数来模拟下载卡顿情况
int randnum=rand()%51 - 10;//使生成的随机数在-10-30之间,然后我们将负值看做是卡顿
total+=(randnum>0 ? loadbaserate*randnum : 0);//表示我们的下载速率在0-1024*50之间
double rate=(total*100.0)/target; //注意这里的宏一定要加括号,不然除以target等于没除,我因为这个地方半天没看出来
// printf("%d %f\n",total,rate);
// process_v2(rate);
cb(rate);
}
printf("\n");
printf("\033[?25h"); //显示光标
}
int main()
{
// process(); //version 1
download(process_v2);//直接调用函数
return 0;
}