前言:
- 会不会编写Makefile,从侧面说明了一个人是否具备完成大型工程的能力。
- 一个工程中的源文件不计其数,按类型、功能、模块分别放在若干个目录中,Makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作。
- Makefile的好处在于自动化编译,一旦编写好Makefile文件,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。
- make是一个用于解释Makefile中指令的命令工具,通常大多数的IDE都有这个命令,如:Delphi的make,Visual C++的nmake,Linux下GUN的make。 可见,Makefile成为了一种在工程方面的编译方法。
- make是一条命令,Makefile是一个文件,二者搭配使用以完成项目自动化构建。
1 make与Makefile使用
首先以对 test.c
文件的编译为例来了解make与Makefile的基本使用:
Makefile的基本编写规则:
- Makefile文件中保存了编译器和连接器的参数选项,并且描述了所有源文件之间的关系。make程序会读取Makefile文件中的数据,然后根据文件中编写的相关指令调用编译器、汇编器、链接器以产生最后的输出。
- Makefile文件中包括了
依赖关系
和依赖方法
。如上图所示,我们以目标文件:依赖文件
的格式来编写依赖关系,表示要生成目标文件,需要依赖有对应的依赖文件。在图例中即是:test文件(可执行文件)的生成依赖于test.o文件(二进制目标文件);test.o文件的生成依赖于test.s文件(汇编语言文件);test.s文件的生成依赖于test.i文件(经过预处理后的C原始程序);test.i文件的生成依赖于test.c文件(源文件)。而单有依赖关系是无法通过依赖文件生成目标文件的,因为依赖关系只告诉了目标文件从何而来,并没有说如何根据依赖文件生成目标文件,因此还需要依赖方法,所谓依赖方法,其实就是我们平常在命令行上输入的一条条指令,如示例中:我们通过命令gcc test.o -o test
可以从test.o文件得到test文件,这也就是依赖关系test:test.o
对应的依赖方法;以此类推,gcc -c test.s -o test.o
是依赖关系test.o:test.s
对应的依赖方法;gcc -S test.i -o test.s
是依赖关系test.s:test.i
对应的依赖方法;gcc -E test.c -o test.i
是依赖关系test.i:test.c
对应的依赖方法。有了依赖关系和依赖方法,我们就可以生成对应的目标文件。 有两点值得注意的是:每次编写依赖方法时应在依赖关系下另起一行并在开头空一个Tab
键的位置,这是固定的编写格式;依赖文件列表可以为空,当其为空时,则表示不生成该目标文件,只执行相应的命令。 - Makefile中使用
.PHONY
来声明伪目标,格式为:.PHONY:伪目标
。Makefile中的伪目标表示目标名称,并不代表真正的文件名,与实际存在的同名文件没有相互关系,因此伪目标不管同名文件是否存在都会执行对应的生成指令。伪目标的作用有两个:使目标对象无论如何都要重新生成;并不生成目标文件,而是为了执行一些指令。 - Makefile中可以在行首使用
#
以表示行注释。
make的工作规则:
- 在默认情况下,make会在当前目录下按顺序寻找文件名为
GUNmakefile
、makefile
、Makefile
的文件。因此,如上图所示,执行make命令前需要先在项目所在当前目录下创建相应的文件(这里是Makefile)。 - make的执行规则是:若make命令之后没有跟指定目标文件,则默认只生成Makefile中所有目标文件中的第一个(如图示例中的test文件即为第一个目标文件),否则就只生成指定目标文件。 但如果该目标文件的依赖文件不存在,make会根据语法规则递归生成第一个目标文件的所有依赖文件后再回头生成第一个目标文件,如上图示例中:一开始当前目录下只有源文件test.c,根据依赖关系,要生成第一个目标文件test,就先要生成其依赖文件test.o,以此类推,就是要依次先生成文件test.i、test.s、test.o,所以也可以看到,在执行make命令时,根据回显的命令执行过程,是按照与Makefile文件中编写的依赖方法的倒序来执行命令的,并且除第一个目标文件test外还生成了中间目标文件test.i、test.s、test.o。
- 依赖方法(目标后所跟的命令)不总是被执行的,如果目标文件不存在,或是目标文件所依赖的文件的修改时间要比目标文件新,才会执行后面相应的命令以形成目标文件。也就是说,make会根据语法规则分析目标对象与依赖对象的时间信息,来判断是否在上一次目标生成后,源文件(依赖文件)发生了修改,若发生了修改,才需要重新生成。在最开始的图例中,一开始当前目录下是不存在任何目标文件的,所以执行make命令时,根据Makefile文件中的依赖关系,会正常生成所有的目标文件。而如果是目标文件已存在的情况,如下图所示:在当前目录下已存在目标文件,且目标文件的最近修改时间要比源文件的最近修改时间新,此时执行make命令表示当前目标已是最新,不再执行命令。而当通过
touch
命令更新源文件最近修改时间后 (说明:touch
命令在文件不存在时,会创建文件,而如果文件已经存在,则将文件的修改时间更新至最新;当然这里还可以通过重新编辑保存文件的方式来更新文件最近修改时间) ,使得源文件的最近修改时间新于当前目录下的目标文件,再次执行make命令则显示成功,并且当前目录下的目标文件的最近修改时间进行了更新,又新于源文件了。
那如果想让某目标下的命令不论时间如何每次都能执行呢?如上Makefile编写规则中说到,可以采用声明伪目标的方式,使得对应命令总是被执行,但此处并不建议将目标可执行文件声明为伪目标,因为每次编译都需要一定的消耗,如果当前目标已是最新,没必要再执行命令生成一样的目标。
- make在找寻依赖关系的过程中,如果出现错误,如:最后被依赖的文件找不到,那么make就会直接退出并报错;而对于所对应的命令(依赖方法)的错误,或是编译不成功,make仍会执行正确的命令再退出。具体示例如下:
- make只关注文件间的依赖关系,如果找到了依赖关系,但当前目录下却没有依赖关系中对应的依赖文件,那make将停止工作,如下图所示:移除当前目录下的test.c文件(最终依赖文件),虽然Makefile中编写了依赖关系,但由于找不到依赖文件,make停止了工作。
关于项目清理:
工程是需要被清理的。如最开始图例中所示,我们使用了 .PHONY
声明了伪目标 clean
(顾名思义,该目标的意义在于清理),这个伪目标并没有所谓的依赖文件,显然是起到上述中的第二个作用,即不生成目标文件,总是执行相应的指令。可以看到, clean
对应的指令为 rm -f test.i test.s test.o test
,目的在于清除所有的目标文件,以便重新编译,也就是所谓的项目清理。由于伪目标 clean
没有被第一个文件直接或间接关联,因此默认情况下执行 make
命令, clean
目标下的命令不会自动执行,需要通过指定目标,即以 make clean
的命令格式使得 clean
目标下的命令被执行。而基于伪目标的特性,无论当前目录下是否有同名文件clean,或是否有已经生成的目标文件,只要执行 make clean
命令,clean对应的指令都会被执行(如下图所示)。
2 模拟实现进度条
在编写进度条程序之前,得先谈谈两个概念:\n
和 \r
在我们编写C程序时常常会用到 \n
换行符来使内容另起一行输出,那 \r
又表示什么呢?其实在我们日常编辑中,可以看到,无论是输入还是输出,文字显示总是跟着光标的位置,光标在哪,则在哪输入输出。同样的,我们的程序在输出时也是跟随着光标的移动进行。\n
表示的是回车并换行,即在输出完当前内容后,将光标移动到下一行的行首再进行之后内容的输出,当然,光标的移动不是说突然就变到了那个位置,而是根据上下左右的方向一步步到达指定位置的,而光标移动到下一行行首的方式可以分为两种:一是先向左移动到当前行的行首,再向下移动到下一行;二是先向下移动到下一行,再向左移动到下一行行首。如图所示,键盘上的回车键等同于 \n
,其造型则表示出了光标的移动方式。而 \r
表示的是只回车不换行,即输出完当前内容后,将光标移动到当前的行的行首,因此,如果再碰到 \r
之后如果还有内容需要输出的话,会重新从当前行行首开始输出,覆盖上一次的输出内容。
\n
我们已经比较熟悉了,那下面以一个例子来看看 \r
对输出的影响:
- 编写如下程序进行测试:
- 测试结果:如图所示,当执行编译后生成的可执行程序test时,我们并没有看到相关内容的输出,执行后就输出了命令提示行。前面说到
\r
会使光标回到当前行行首,而后输出的内容会覆盖之前的内容,那是不是因为命令提示行输出覆盖太快所以没能看到内容显示呢?那我们试着在输出内容后进行延时。
- 增加延时后测试结果:可以看到,增加延时后,依旧没有显示相关内容,只是在延时期间光标一直停在当前行首,直到延时结束输出命令提示行。
- 为什么使用了回车符
\r
后我们没能看到相关内容输出呢?这里不得不提到一个概念:缓冲区
。我们所编写的内容并不是直接输出到屏幕上(标准输出流)的,而是先输出到缓冲区中,在由缓冲区输出到屏幕。这里没能看到相关内容的输出是因为其仍保留在缓冲区中没有输出,只有当缓冲区刷新时,其中的保留的内容才会输出,而对于没有添加换行符和回车符或者添加的是换行符\n
的情况,通常会自动刷新缓冲区,因此没有主动刷新也可以正常输出内容。那了解的原因所在后,我们在程序中主动刷新缓冲区,再看看输出结果:可以看到,增加刷新缓冲区后,相关内容正常显示了,也符合输出后光标回到当前行首,由命令提示行覆盖输出内容。
我们不是要编写进度条程序吗?这与 \r
有什么关系呢?想来进度条大家都不陌生,就是以一行上显示的移动进程来表示某项工作的进度。也就是说,从0-100的进度需要再同一行上进行变化,而 \r
具有输出内容后将光标移动到当前行首的作用,这就契合了进度条的变化过程,基于此,以下模拟实现进度条:
- 我们将进度条程序的主要实现封装为一个函数,编写在
proc.c
文件中,将对应的函数声明编写在头文件proc.h
中,再创建测试文件procTest.c
,在其中编写主函数并调用进度条函数。这里实现了两种不同形式的进度条:符号移动版和色块移动版。
proc.h
proc.c(后附源代码,有需要者可再自行演示)
procTest.c
进度条函数源代码:
#include "proc.h"
#define STYLE '=' //进度条移动符号
//符号形式移动版
void process(){
char bar[101];//进度条字符串,预留一个'\0'的位置
char status[4] = {'|', '/', '-', '\\'}; //表示运行状态,循环数组中的字符
memset(bar, '\0', sizeof(bar));//初始将进度条字符串中的内容全部置为'\0'
int i = 0;//考虑执行标准问题,这里在外初始化
for(; i <= 100; i++){
//控制格式,循环输出进度条字符串,通过字符串的变化来表示进度条的变化
//\033[选项;选项;选项m表示对其后输出内容的颜色控制,0m默认无颜色
printf("[\033[0;36m%-100s\033[0m][%d%%][%c]\r", bar, i, status[i%4]);
fflush(stdout);//刷新缓冲区,确保字符串内容正常输出
bar[i] = STYLE;//每次输出后修改一个字符为对应的进度条移动符号
if(i < 99)
bar[i+1] = '>';//增加箭头显示,当进度达到100%,去掉箭头
usleep(100000);//延时0.1s显示
}
printf("\n");//结束后换行输出命令提示行
}
//色块形式移动版
void process_color(){
char bar[102];//进度条字符串,预留两个'\0'的位置
char status[4] = {'|', '/', '-', '\\'};
memset(bar, ' ', sizeof(bar));//初始将字符串中内容全部置' '(空字符)
bar[101] = '\0';//保持最后一个字符总是为'\0'
int i = 0;
//控制中间的'\0'字符将整个字符数组分为两个字符串输出,确保两个字符串的长度加起来总是为100
//循环输出两个字符串,控制前一个字符串总是有颜色,后一个字符串总是无颜色
for(; i <= 100; i++){
bar[i] = '\0';//控制分隔字符串的'\0'移动
//输出带背景色的空字符串与不带背景色的空字符串
printf("[\033[0;30;46m%s\033[0m%s][%d%%][%c]\r", bar, bar+i+1, i, status[i%4]);
fflush(stdout);//刷新缓冲区
bar[i] = ' ';//将前一个字符串中的内容均置为空字符' '
usleep(100000);//延时0.1s显示
}
printf("\n");//结束后换行输出命令提示行
}
- 前面我们讲解了make与Makefile的基本使用,接下来我们就可以在进度条程序中使用起来了,如下,先在当前目录中创建Makefile文件,文件编写如下:
接着执行make命令生成进度条可执行程序如下:
- 结果演示(网络原因可能稍有卡顿):
符号移动版:
色块移动版:
以上是我对make与Makefile工具使用的一些学习记录总结,如有错误,希望大家帮忙指正,也欢迎大家给予建议和讨论,谢谢!