什么是makefile
在我们以后的工作环境中,一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作
Linux中提供了自动化构建工具–makefile来帮我们解决这个问题,makefifile带来的好处就是——“自动化编译”,makefile一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率
此外,会不会写makefifile,从一个侧面说明了一个人是否具备完成大型工程的能力
什么是make
make是一个命令工具,是一个解释makefifile中指令的命令工具,一般来说,大多数的IDE都有这个命令,比如:Delphi的make,Visual C++的nmake,Linux下GNU的make。可见,makefifile都成为了一种在工程方面的编译方法
总结:make是一条命令,makefifile是一个文件,两个搭配使用,完成项目自动化构建
makefile的编写重要的是编写依赖关系和依赖方法,依赖关系是指一个文件依赖于另一个文件,即我们想到得到一个文件,在目录下我们必须先有的另外一个文件,依赖方法是指如何依赖文件来得到对应的目标文件
我们在编写makefile的时候需要注意一下几点:
1.makefile文件名必须是makefile/Makefile,不能是其他的文件名,否则make无法识别
2.依赖文件可以有多个,也可以没有
3.依赖方法必须以Tab键开头,不能是四个空格
我们以下面的例子来说明如何编写makefile文件
#include
int main()
{
printf("hello makefile\n");
return 0;
}
makefile文件:
test.out:test.c // 依赖关系
gcc test.c -o test.out // 依赖方法
.PHONY:clean //伪目标
clean:
rm -f test.out
如上,在makefile文件中,test.out 依赖于test.c ,我们以冒号作为分隔符,依赖方法是gcc 编译执行,clear不依赖于任何文件,依赖方法为rm -f指令,其中,.PHONY修饰的clear表示其是一个伪目标,表示它总是被执行
在Linux在,我们输入make命令之后,make会在当前目录下找名字叫“Makefifile”或“makefifile”的文件,如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到“test.c”这个文件,并把这个文件作为最终的目标文件,如果找不到,就会打印提示信息
在我们上面的例子中,makefile中一共有两个目标文件,test.out和clear,如下,我们输入make它会默认执行第一个目标文件,此外,我们也可以指定多个目标文件来让它执行多个目标文件
1.没有编写makefile文件:
2.使用一个指令:
3.同时使用多个指令
我们将上面例子的makefile修改成如下内容:
test.out:test.o
gcc test.o -o test.out
test.o:test.s
gcc -c test.s -o test.o
test.s:test.i
gcc -S test.i -o test.s
test.i:test.c
gcc -E test.c -o test.i
.PHONT:clean
clean:
rm -f test.i test.o test.s test.out
我们在输入make指令之后,make会在当前目录找名字叫“Makefifile”或“makefifile”的文件,如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到“test.out”这个文件,并把这个文件作为最终的目标文件,如果test.out所依赖的test.o文件不存在,那么make会在当前文件中找目标为test.o文件的依赖性,如果
找到则再根据那一个规则生成test.o文件(l类似于栈–后进先出)
如果test.o依赖的文件也不存在,则继续执行该规则,知道找到存在依赖文件的目标文件,得到目标文件之后层次返回形成路径上的其他目标文件,或者最后被依赖的文件找不到,直接退出并报错
这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件
在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make就会直接退出,并报错,而对于所定义的命令的错误,或是编译不成功,make根本不理
对于我们上面的例子,test.out依赖的文件test.o不存在,make会去寻找以test.o为目标文件的依赖关系,test.o依赖的test.s也不存在,make会去找以test.s为目标文件的依赖关系,然后test.s依赖test.i,test.i依赖test.c,test.c文件存在,此时,make就会根据test.i的依赖方法形成test.i,层层向上返回,依次形成test.s,test.o,test.out
工程是需要被清理的,在makefile中,我们使用clean作为项目清理的目标文件,由于项目清理不需要依赖于其他文件,所以clean 也不需要依赖关系
像clean这种,没有被第一个目标文件直接或间接关联,那么它后面所定义的命令将不会被自动执行,不过,我们可以显示要make执行。即命令——“make clean”,以此来清除所有的目标文件,以便重编译
但是一般我们这种clean的目标文件,我们将它设置为伪目标,用 .PHONY 修饰,伪目标的特性是,总是被执行的
当我们对同一个源文件多次make的时候,我们就会发现第一次程序正常编译,但是第二次之后它就不会再进行编译了,而是提示“make :‘test.out’ is up to date.":
但是如果我们将源文件进行修改再进行make的时候,第一次正常进行编译,第二次之后情况还是和上述情况一样
实际上,Linux是为了防止我们对已经编译好的并且没有经过修改的文件进行重复编译,从而造成时间的浪费,有人可能会说,我编译一个程序最多几秒中,浪费这一点时间问题不大,但是我们在工作的时候,编译一个程序可能需要几十分钟甚至更久,这时对已经编译好的文件不再进行编译,就会减少很多的时间。对于上面我们的代码,如果test.c已经编译得到了test.out,并且我们对test.c文件没有进行修改,那么我们再次make的时候make就不再执行,即不进行重复编译。
那么make是如何判断源程序不需要重新编译呢?也就是如何判断源文件没有被修改呢?
make会通过比较源文件和目标文件的时间戳来判断是否需要重新编译。如果源文件的时间戳比目标文件的时间戳早,说明源文件没有被修改过,此时make就不需要重新编译。否则,make会重新编译源文件生成新的目标文件
具体来说,make会根据每个源文件生成一个对应的目标文件,然后比较它们的时间戳。如果目标文件不存在,或者其时间戳比源文件早,说明源文件已经被修改过,此时make就需要重新编译。否则,make就认为源文件没有被修改过,不需要重新编译
需要注意的是,如果源文件的修改只是微小的变化,例如修改了注释或空格等,此时make可能无法正确判断是否需要重新编译。为了避免这种情况,可以使用一些工具来生成文件的摘要或哈希值,并将其与目标文件的哈希值进行比较,这样可以更加准确地判断是否需要重新编译
在Linux中,文件一共有三种时间:
1.访问时间(Access):当我们查看文件内容后该时间改变,比如cat,vim,less等等
2.修改时间(Modify):当我们修改文件的内容后该时间改变
3.改变时间(Change):当我们修改文件的属性或者权限的时候该时间改变,比如vim/nano(文件大小改变),chmod/chown/chgrp(文件的权限改变)
但是实际上,我们访问文件的内容不一定会改变文件的访问时间,其原因如下:
1.在Linux下,访问文件内容的操作十分频繁,而修改文件的访问时间需要对文件进行IO操作,如果我们每次访问都修改文件的访问时间,会增大系统的负担
2.一个文件是否能被读取是由文件的权限决定的,而既然文件是可读的,那么说明文件的拥有者/所属组是并不在意我们对文件进行读取,所以也没有必要每次都修改文件的访问时间
3.有些文件系统可能会使用一些优化策略,例如延迟写入(delayed write)或写时复制(copy-on-write),这些策略可能会导致文件的访问时间不及时更新。此外,在某些情况下,文件的访问时间可能会被关闭或禁用
4.为了提高系统性能,Linux内核使用了一些优化策略来减少对文件访问时间戳的更新。例如,内核会将文件的访问时间戳的更新延迟到一定时间后再更新,或者只在文件被关闭时才更新访问时间戳。这样可以减少对文件系统的读写操作,提高系统性能
5.在某些情况下,文件系统可能会禁用访问时间戳的更新,例如使用noatime或nodiratime选项挂载文件系统时,访问时间戳不会被更新。这些选项可以提高文件系统的性能,但会牺牲文件访问时间戳的准确性
访问文件内容不一定会改变文件的访问时间戳,这取决于文件系统的优化策略和挂载选项。但是,在大多数情况下,访问文件内容会导致访问时间戳的更新。
综上所述,Linux下并不会每次访问文件的内容都会更新文件的访问时间,而是积累到一定的访问次数或者积累一段时间才更新
而make则是根据可执行程序的修改时间(Modify time)与源文件的修改时间进行对比来判断是否需要重新进行编译
make判断源文件是否需要重新编译只与源文件的修改时间有关,与源文件的内容改动无关,我们可以通过touch命令来进行验证,(touch file:如果file已经存在,则更新file的所有时间)
我们在了解make是如何判断是否需要重新执行依赖方法形成目标文件之后,.PHONY的原理和作用就显而易见了:被.PHONY修饰的目标文件不根据文件的修改时间来判断是否需要重新执行,从而达到总是被执行的效果
我们也同样可以使用.PHONY来修饰test.out,使得test.out每次都被重新编译
对于\n相信大家一定会熟悉,因为在C语言中我们经常使用/n来进行换行,但是实际上我们C语言的‘\n’是’\r’+’\n’
‘\r’:回车,即将光标移动到当前行首
‘\n’:换行,即将光标移动到下一行
所以我们C语言的’\n’的作用是回车+换行,而不仅仅是换行
我们知道,我们从键盘输入的字符以及向显示器输出的内容,并不是直接的读入或者输出,而是会被存放到输入缓冲区和输出缓冲区中,待缓冲区刷新时数据才会被读入或输出
而行缓冲是缓冲区的一种,在行缓冲下,当输入和输出中遇到换行符时,才执行真正的I/O操作,即我们输入的字符会先存放到缓冲区,等按下回车键时才进行真正的I/O操作。我们可以使用两份不同的代码来验证上诉结论:
我们可以看到,test1.c的数据printf后并没有直接打印显示到终端上,而是等待程序结束后缓冲区刷新后才显示,而test2.c中的数据由于’\n’可以刷新行缓冲,所以直接打印到了终端
此时,我们就可以运用回车换行的特性以及我们所学了C语言知识来编写一个进度条小程序了
#pragma once
#include
#include
#include
#define NUM 101
//#define STYLE '#'
#define S_NUM 5
// 函数声明
extern void ProcessOn();
#include "process.h"
// 进度条字符数组
const char style[S_NUM]={'#','$','*','>','-'};
// 函数的定义
void ProcessOn()
{
int cnt=0;
// 定义字符数组,用于存储进度条显示的字符
char bar[NUM];
memset(bar,'\0',sizeof(bar));
// 旋转光标
const char* lable = "|\\-/";
// 循环101次
while(cnt<=100)
{
printf("[%-100s][%d%%][%c]\r",bar,cnt,lable[cnt%4];)
// printf("\033[42;34m[%-100s][%d%%][%c]\033[0m\r",bar,cnt,lable[cnt%4]);
// 缓冲区刷新
fflush(stdout);
// bar[cnt++] = STYLE;
bar[cnt++] = style[N];
usleep(50000);
}
printf("\n");
}
#include "process.h"
int main()
{
ProcessOn(); return 0;
}
ProcessOn:process.c test.c
gcc process.c test.c -o ProcessOn -DN=0 .PHONT:clean
clean:
rm -f ProcessOn
在process.c中,我们每次打印数据之后使用’\r’让光标回到行首,然后刷新缓冲区,再增加bar数组里面的标识字符,这样就可以使我们下一次打印数据时可以直接覆盖之前的数据,并且会增加一个字符的内容,就有就可以实现进度条的效果
同时,为了使进度条更加的真实,我们还增加了一个显示进度的百分比和一个旋转光标,每次打印打印完成之后休眠0.05秒,使得进度条在5秒中之后打印完毕,这样就使得进度条就有了一个加载进度以及一个旋转的符号
usleep函数
usleep函数的一个休眠函数,传入参数的单位是纳秒,而sleep传入参数的单位是秒
最后我们了为了丰富进度条字符的样式,我们把进度条字符设置为一个字符数组,我们可以根据我们想要的样式来进行选择,选择时调整N的值即可
此外,我们还可以给输出字符加上颜色。控制颜色的基本格式为:
printf("\033[字背景颜色;字体颜色m字符串\033[0m");
// 比如下面的例子
printf("\033[47;31mhello world\033[5m");
这里有一篇相关的文章,大家有兴趣的可以看一看C语言控制台下printf设置文字颜色和背景色以及实现简单的文字选择菜单
注意:在printf函数中,%具有特殊的意义,所以我们需要输入%%对其进行转义,同样,在lable数组中,字符’\‘也是特殊字符,我们需要输入’\\’
最终我们实现的进度条演示如下: