本篇文章,我们继续分享与Linux相关的知识。本次分享的主要内容是make/Makefile工具的用法,怎么去使用make/Makefile这个工具来完成代码的编译,生成一个可执行程序。
make/Makefile
上一篇文章,我们简单的介绍了make/Makefile这个工具。make是一条指令,Makefile是当前目录下的一个文件。
下面,我们来见见make/Makefile这个工具的使用。
我们先编写一段简单的代码。
然后,我们用vim,编写makeflie文件(如果当前目录,没有makeflie这个文件,vim会自动创建)。
进入makefile文件后,我们将填入如下内容。第一行是依赖关系,这里表示mycode目标文件依赖mycode.c文件。第二行是依赖方法,这里表示mycode的生成方法。
做完以上的准备工作后,我们直接make就可以生成相应的可执行程序了。我们运行该程序,运行结果符合预期,打印hello Linux。
现在,我们有了生成可执行程序的方法,还差一个删除可执行程序的方法。我们需要在makefile文件里加入如下内容。第三行也是依赖关系,这里表示clean这个目标文件不依赖任何文件。第四行也是依赖方法,这里表示clean目标文件的方法。
保存退出后,我们使用make clean这个指令,就可以看到当前目录的mycode被删除了。
这时,就会有同学就会有疑问了。凭什么,生成可执行程序,可以直接make,而删除可执行程序就要make clean。
此时,我们的目录下的mycode已经被删除了。
我们试一试make mycode指令,发现也能生成可执行程序mycode。
我们把clean和mycode指令的位置交换一下。再来尝试一下。
此时,我们保存退出,再使用make指令,就变成了删除可执行程序。
到这里,就不和大家卖关子,make自上向下扫描makefile文件,它会将你所需要形成的第一个目标文件充当make的默认动作。
为了让大家对make有更进一步的认识,我们按照编译源文件的四个步骤来生成可执行程序。
我们将makefile中的内容改成如下内容。
然后,执行make指令,我们得到了mycode这个可执行程序。
我们更改一下.i,.s在makefile中的位置,看看能否正常生成可执行程序。
从图中看,我们可以知道,我们更改文件的顺序操作,对可执行程序的生成并没有影响,这是为什么呢?这是因为make会自动推到makefile中的依赖关系。还有一点就是,有没有感觉makefile有点类似我们所学过的递归。
make实际上是一个栈式结构。
为了方便后面的解释,我把改成一步到位的方式。
第二行的内容可以简写成下面这样,$^表示冒号左边的文件,$@表示冒号右边的文件。
我们每次使用make指令时,它都会回显依赖方法到显示屏上。如果你不想让它回显,只需要在相应的依赖方法前加上@符号就可以了。
我们以clean为例,加上@符号。
此时,我们再使用make clean,它不会再进行回显了。
此时,当前目录是没有生成可执行程序的,我们使用make指令,进行编译生成。可当我们第二,第三次使用make时,系统不让我们使用了。这是为什么?
这是因为我们的源文件的内容并没有进行修改。再怎么进行编译,结果都是一样的。make为了提高编译效率,就禁止了我们对无内容变化的源文件,进行多次编译的操作。
这个是怎么做到的呢?
首先,我们要有一个意识。一定是源文件形成可执行程序,先有源文件,才有可执行程序,一般而言,源文件的最近修改时间比可执行文件要老。
如果我们更改了源文件,历史上曾经还有可执行文件,那么源文件的最近修改时间,一定比可执行程序要新。
下面,我们来验证一下。这里,我们需要认识一个新的指令,stat指令。
stat指令
该指令可以用来查看文件的访问,修改,更改时间。
Access是有访问意思,后面跟着的是最近一次访问该文件的时间。我们对文件不管进行什么操作,都要对文件进行访问。所以,访问时间需要频繁修改。访问时间也是数据,频繁修改,就需要频繁对磁盘数据进行修改。这样十分的消耗资源,为此,有人给出了一个解决方案,间隔一段时间后,才会对访问时间进行一次修改。
前面的文章,我们有提到,文件=文件内容+文件属性。
Modify后面的时间,表示文件内容的最近一次修改的时间。
Change后面的时间,表示文件属性的最近一次修改的时间。文件内容每次的修改,会改变文件的大小。文件大小属于文件属性。也就是说,每次修改文件的内容,会同时影响Modify和Change这个两个时间。
我们给mycode.c多增加几行打印。
当我们再次使用stat指令进行查看,很明显能看到文件的时间发生了更改。
我们现在比较一下源文件和可执行程序的时间。此时,源文件的时间是比可执行程序老的。
这时候,我们对源文件再次进行编译操作,是不允许的。
我们再给源文件增加几行代码。
此时,我们再次查看,源文件和可执行程序这两个文件的时间。可以很明显的看到,源文件的时间比可执行程序的时间新。
这时候,我们再进行编译操作就被允许了。
一个列子不能说明问题,我们接着往下看。我们通过cat对源文件进行了访问,查看了里面的内容。可是,源文件的访问时间并没有修改。
如果我们想让文件的时间立马更新,可以使用touch指令,touch这个文件。在文件不存在的情况下,touch用来创建文件。文件已经存在,touch的作用就变成了更新文件的时间。
touch源文件之前,源文件的时间是比可执行程序老的,make不被允许。touch之后,源文件的时间比可执行程序新,可以正常进行make了。
通过上面的两个例子,我们也就明白了。make是如何实现,禁止多次编译的原理了。它只需要比较,可执行程序的最近修改时间和源文件的最近修改时间,就可以确定需不需要进行重新编译了。
如果我们就是想让我们的指令,总是被执行呢?我们需要在相应的指令前加上.PHONY+为目标进行声明。
现在,我们希望make 总是可以被执行,对源文件进行编译。只需要按照".PHONY:伪目标“的格式,在相应的指令前面加上这样的字样即可。
加上.PHONY:mycode这样的字样后,我们想编译多少次就可以进行多次编译。
如果我们想make clean指令不被限制,就在clean前面加上“.PHONY:clean"的内容。
到这里,关于make/Makefile的内容,我们就了解得差不多了。最后,我们用一张图来总结一下刚刚所分享得内容。
下面,我们利用make/Makefile工具,来编写一个小程序。
时间倒计时
在编写代码之前,我们先了解两个背景知识。
第一个背景知识,什么是回车换行?
假设我们现在有一张纸,我们从左端依次向右写字,写到第一行红褐色五角星的位置。然后,我们将笔尖由第一行红褐色五角星的位置下移到第二行红褐色五角星的位置,这个过程称之为换行。而我们的笔尖由第二行红褐色五角星的位置左移到这一行的开头(图中用绿色五角星表示),这个过程,我们称之为回车,用\r表示。我们之前认识的\n,则包含了这两个动作。
第二个背景知识,缓冲区。
我们先简单实现一段倒计时的代码看看效果。sleep不认识,我们可以查man手册第三章的sleep函数。
简单运行一下。
结果能够正常输出,但哪有倒计时是这样的。我们把\n删除掉,重新进行编译。
编译后,我们运行程序发现,输出结果是过了一段时间后,才一起打印出来的。
这是什么原因呢?难到程序先执行了sleep吗?并不是,程序是严格自上而下逐步执行的,先执行printf,再执行sleep的。一到九的数据,最后,能完整输出,说明它们并没有丢失。这些数据被保存在了一段内存空间里,而这段内存空间称之为缓冲区。这个缓冲区由C语言来提供,也有C来进行维护。
我们没去掉\n时,数字能逐个输出,是因为\n有强制刷新缓冲区数据的功能。如果我们想让数据逐个输出,就需要对缓冲区进行刷新。这里我们要认识一个新的函数,fflush函数,它同样在man手册第三章中有介绍。它的功能就是将缓冲区中的数据刷新出来。
问题又来了,缓冲区的数据,我们往哪里刷。在我们运行一个程序时,会默认打开三个文件,标准输入,标准输出(显示屏,stdout),标准错误。我们这里需要将数据往输出流stdout刷,也就是向显示屏打印。
经过改进,我们的程序能够及时的打印数据了。可是,倒计时,都是在一个位置不断变换数字的。哪有打印一排数字的倒计时。还记得我们刚刚说到的\r吗?\r会将我们的笔尖放到一行的最开始位置。我们可以通过使用\r将我们的光标,移动到最开始的位置。
此时程序就可以达到我们预期的效果了。但如果我们把9增加到10。又会出现新的问题。为了方便演示结果,在代码的最后增加了一个\n打印。
我们想要打印的是8,可是后面多了个0。
这是为什么?这是因为显示器是字符文件,我们向显示器打印的是一个个字符,而不是数字。在打印十的时候,实际上是打印了1和0这两个字符,只不过它们是挨在一起的,让你误以为是数字10。当我们将光标移回最右边时,打印数字8时,只把数字10的1覆盖掉了。于是,就有了显示器中的80。那我们怎么解决呢?我们只需要调整一下格式,将数字10的两个字符都覆盖掉即可。
好了,到这里,我们一个正常的时间倒计时就完成了,下一章我们实现一个进度条程序。不知道我的分享,有没有说明白,给予你一点点收获。如果你有所收获,别忘了给我点个赞,这是对我最好的回馈,当然你也可以在评论发表一下你的收获和心得,亦或者指出我的不足之处。如果喜欢我的分享,别忘了给我点关注噢。