make和makefile是linux系统里用于自动化编译和构建程序的工具。们通过定义一系列的规则来指定如何编译和链接程序,从而简化了编译过程,尤其是有多个源文件的时候。
下面我们来浅浅的了解一下make和makefile是如果进行自动化编译的。
一.make和makefile
简单来说,make是一个指令,而makefile是一个文件
make会读取名为makefile的文件,然后根据makefile文件里面的指令执行相应的命令。
下面写一个简单的makefile:
当前目录下,我们有一个main.cpp文件,和一个makefile文件,我们现在当然可以直接编译main.cpp文件g++ main.cpp -o main 然后生成一个名为main的可执行程序。
现在我们不用g++命令来编译,而是借助make和makefile来实现自动化编译
先看结论:
执行make命令之后,他会在makefile文件里面从上到下搜索,将第一个文件作为是目标文件即main。而目标文件是依赖于main.cpp这个文件的。有了依赖关系之后,还需要有依赖方法,即第二行的g++命令。
这段指令是什么意思呢?
文件的第一行 main:main.cpp表示一组依赖关系,即main文件依赖main.cpp文件。
第二行表示了它们的依赖方法,make的时候,就会执行该依赖方法。
第五行是clean目标,但其没有依赖关系,只有依赖方法。但一个makefile文件只能有一个目标文件,而它是从上到下扫描文件的,所以第一个文件就是目标文件。目标文件可以直接make,而其余的文件则需要借助make显式调用(make clean)。
.PHONY:后面的是伪目标,伪目标意味着总是会被执行。即后面的clean会总是被执行。
二.总是被执行?
什么是总是被执行呢?先观察下面这一现象:
我们make了之后产生了 可执行程序,我们如果再想make的话就不行了,这是为啥呢?
make是一个自动化的编译工具,它只会编译那些已经被更新了的程序。如果make发现之前已经编译了一次,且源代码并没有改变,此时就会拒绝编译。
而我们的make clean就可以被重复执行,这就是总是被执行。
所以我们可以用.PHONY修饰main,使其成为一个伪目标,这样就可以多次make了
那么make是怎么知道依赖的文件有没有被修改呢?
文件=内容+属性,而他的属性包含了有三个时间:
- Modify:表示文件内容被修改的时间
- Change:表示文件属性被修改的时间
- Access:表示文件的访问时间
前两种很好理解,且需要注意,文件内容修改了,则文件属性一般情况下也被修改了,因为size也是一个文件属性。
访问时间指的是你修改了文件或者cat了文件,但是访问时间不准,你刚才编辑了文件,访问时间修改,但是接下来cat文件,它的访问时间是不变的。这中间有冷却时间。
综上,make就是借助Modify time来确定文件的老旧的。如果main的Modify在main.cpp之前,表示main.cpp已经更新了,所以此时就可以重新编译了
三.makefile的推导过程
我们刚才是直接将.cpp编译成为可执行程序,那我们如果分布编译呢?
既然是分布编译,则我们最终的可执行程序main就是从.o文件来的。即main依赖于main.o,但是当前目录中并没有main.o,此时make就会继续向下寻找,make.o依赖于main.s,而目录中也没有main.s,所以就会继续向下搜索,最终发现,所有的依赖关系到了main.cpp,而该文件是有的,所以make就从这个位置开始向上执行。
其实make遇到依赖关系之后,如果找不到依赖关系,就会先将该依赖关系的依赖方法入栈,然后扫描下一个依赖关系,如果某个依赖关系的依赖文件存在,则从该位置开始执行,接着依次取出栈顶的方法执行。
下面观察运行结果:
如果make在扫描的过程中,最后找不到依赖文件,就会停止工作:
四. makefile的扩展语法
makefile里面可以定义变量:
一般将最终的可执行程序定义为BIN,SRC即为源文件,CC为编译器,FLAGS表示编译选项,RM则表示清理,变量所指代的东西是可以带空格。
接下来我们在写依赖关系和依赖方法时就可以使用变量来写,但是变量得用$()包围。对于依赖方法来说,可以用$@表示目标文件,$^表示依赖对象。它与用变量直接表示是一样的。
但是我们一般习惯于先将源文件编译成目标文件,最后在进行链接,那么makefile怎么写呢?
我们可以定义出链接选项和编译选项,然后根据上面的推导过程进行书写。 对于编译过程,不需要指定obj文件,-c选项会自动生成同名.o文件。
一个依赖关系下面可以有多个依赖方法,且依赖方法可以通过@实现不可视化:
那么如果有多个源文件呢?如果进行先编译后链接呢?
我们在定义变量时可以使用shell命令获取所有的.cpp文件,也可以将所有的.cpp生成同名.o
五.实现进度条
1.换行和回车的区别
我们在屏幕上打印信息时,可以通过\n来换行,但这里面不仅仅是一个换行解决的。
\r:回车,使光标回到最左端
\n:换行,使光标直接移动到下一行,并不回到下一行的开始位置。
所以我们以前printf("%d\n",a);其实这个\n被处理成了\r\n.
2.缓冲区刷新
我们先观察下面这段程序:
我们向屏幕打印hello world,然后让程序休眠。我们来观察打印结果:
我们看,我们明明先打印的信息,后进行休眠,为什么执行时而是先休眠后打印呢?
我们知道顺序语句,所以程序一定是先打印,后休眠的。但为什么会出现这个结果呢?
归根结底,是因为printf不会第一时间把内容输出到屏幕上,而是先放入一个缓冲区中,而我们并没有刷新缓冲区的行为,所以并不会进行打印。最后程序结束,会自动刷新缓冲区,使信息打印成功。
所以我们在printf之后,可以先用fflush函数刷新一下缓冲区:
而\n换行符就可以刷新缓冲区。
3.倒计时小程序
在实现进度条之前,我们先结合上面的小知识,实现一个倒计时小程序
%-2d可以控制输出的位数,以及让数字左对齐。/r则是控制光标回到起始位置打印。
4.实现进度条
我们采取声明和实现分离。分别用main.c来控制运行逻辑,progressbar.h包含头文件以及函数声明,progressbar.c包含函数实现。
下面是实现进度条的主要代码:
我们定义一个字符数组,用来表示当前的进度,每个字符都表示1个进度。即一共有100个字符。所以开101个空间。打印时有三个方括号,一个表示进度条本身,一个表示当前的进度百分比,一个是动态字符,用来显式进度条正在执行。每打印一个进度就休眠。
5.进度条与下载任务结合
进度条一般不会自己凭空执行,他会在下载软件或上传资源时显式。所以它一般是被下载任务所调用。
完~