以前对 makefile 的编写, 限于刚开始接触, 我都比较局限一些死板的格式, 有时候就会显得有些繁琐. 在进一步了解一些系统编译和链接的知识后, 对 makefile 编写流程有了一些新的认识, 所以来此梳理梳理, 方便更灵活地编写 makefile.
限于 makefile 认识不足, 这里参考了一篇比较好博文: makefile https://blog.csdn.net/haoel/article/details/2886/
关于 makefile
makefile 带来直接好处就是 --"自动化编译". 一旦写好, 只需要一个 make 命令, 整个工程完全自动编译, 所以十分方便. 而 Makefile 文件就是告诉 make 命令怎么样地去编译和链接程序. 但是想要比较灵活的运用它, 还是先要熟悉一些关于系统对程序编译和链接的知识.
一般来说, 对 C,C++ 程序, 先把源文件编译成中间代码文件. Linux 下是 .o 文件即 Object File, 在 Windows 下也就是 .obj 文件, 这个动作叫做编译(compile). 然后再把大量的. O 文件合成执行文件, 这个动作叫作链接(link)
编译时, 编译器需要的是语法的正确, 函数与变量的声明的正确. 对于后者, 通常是让我们告诉编译器头文件的所在位置(头文件中放声明, 而定义放在 C/C++ 文件中), 只要所有的语法正确, 编译器就可以编译出中间目标文件. 一般来说, 每个源文件都应该对应于一个中间目标文件(.O 文件或是 OBJ 文件).
链接时, 主要是链接函数和全局变量, 所以, 我们可以使用这些中间目标文件 (.O 文件或. OBJ 文件) 来链接我们的应用程序. 链接器并不管函数所在的源文件, 只管函数的中间目标文件. 在大多数时候, 由于源文件太多, 编译生成的中间目标文件太多, 而在链接时需要明显地指出中间目标文件名, 这对于编译很不方便, 所以, 我们要给中间目标文件打个包, 在 Windows 下这种包叫 "库文件"(Library File), 也就是 .lib 文件, 在 Linux 下, 是 Archive File, 也就是 .a 文件
总的来说就是, 首先源文件 -> .o 文件, 再由. o 文件 ->可执行文件. 在编译时, 编译器只检测程序语法, 和函数, 变量是否被声明. 如果函数未被声明, 编译器会给出一个警告, 但可以生成 Object File. 而在链接程序时, 链接器会在所有的. o 文件中找寻函数的实现, 如果找不到, 那到就会报链接错误码(Linker Error)
直白点说, 最后生成的可执行文件就是靠着这种 "各各依赖关系" 逐步得到的.
来个例子感受一下,
- hello: hello.o
- hello.o: hello.c
gcc -c hello.c -o hello.o
这里 make, 便会自动编译了. 这当中生成可执行文件 hello 依赖于 hello.o,hello.o 依赖于 hello.c; 最后找到了 hello.c 便可以 gcc 生成 hello.o 这样往后'带', 目标文件的 hello 便链接上. o 文件去执行了. 这里值得注意的是写 gcc 命令时需要添上 -c 选项, 用来保证得到的. o 文件可重链接, 不然基本会 make 报错(某些情况如直接 gcc hello.c -o hello 例外).
还有注意一点就是在 Makefile 中的命令 (如 gcc ..), 必须要以[Tab] 键开始, 不然你很可能就会 make 出错哦~.
上面例子直接链接一个中间目标文件, 显得比较简单, 当遇到源文件需要多个链接多个中间目标文件时是怎么个样子呢?
比如 分别创建一个加法的 add.c 和 add.h , 一个减法 sub.c 和 sub.h 最后 main.c 来调用 add 和 sub 实现加减法. 此时 Makefile 会像这样
main: main.o add.o sub.o
main.o: main.c
gcc -c main.c -o main.o
add.o: add.c
gcc -c add.c -o add.o #加 - c 指定生成为可重链接. o 文件
sub.o: sub.c
gcc -c sub.c -o sub.o
- .PHONY:clean
- clean:
- -rm -rf *.o
使用看看
从上面注意几个地方:
当最终目标文件依赖多个. o 时, 将依赖的多个. o 一起写到最前面. 然后依次以 目标: 依赖文件 gcc... 的格式, 罗列所有依赖关系
由于在上面的过程中生成了多个中间. o 文件 (实际工程中肯定是比较多的), 所以每次编译完成, 需要进行一定的清理工作, 这时候就用上一个 "clean" (后面细说一下) 来清理.
.PHONY 意思表示 clean 是一个 "伪目标". 也即是无论 clean 是否最新, 一定执行它. rm 命令前面加了一个小减号的意思就是, 也许某些文件出现问题, 但并不理睬. 当然, clean 的规则不要放在文件的开头, 否则这就会变成 make 的默认目标, 相信谁也不愿意这样. 不成文的规矩是 --"clean 从来都是放在文件的最后"
关于 clean:
它只不过是一个动作名字, 有点像 c 语言中的 lable 一样, 其冒号后什么也没有, 那么, make 就不会自动去找它的依赖性, 也就不会自动执行其后所定义的命令. 要执行其后的命令(不仅用于 clean, 其他 lable 同样适用), 就要在 make 命令后明显得指出这个 lable 的名字. 这样的方法非常有用, 我们可以在一个 Makefile 中定义不用的编译或是和编译无关的命令, 比如程序的打包, 程序的备份, 等等.
到这, 大致可以了解了 makefile, 以及大致怎么实现 makefile. 好, 那么 make 又是怎么用 makefile 进行执行的呢?
make 怎么执行
1,make 会在当前目录下找名字叫 "Makefile" 或 "makefile" 的文件.
2, 如果找到, 它会找文件中的第一个目标文件(target), 在上面的例子中, 他会找到 "main" 这个文件, 并把这个文件作为最终的目标文件.
3, 如果 main 文件不存在, 或是 main 所依赖的后面的 .o 文件的文件修改时间要比 main 这个文件新, 那么, 它就会执行后面所定义的命令来生成 main 这个文件.
4, 如果 main 所依赖的. o 文件也不存在, 那么 make 会在当前文件中找目标为. o 文件的依赖性, 如果找到则再根据那一个规则生成. o 文件.(这有点像一个堆栈的过程)
5, 当然, 你的 C 文件和 H 文件是存在的啦, 于是 make 会生成 .o 文件, 然后再用 .o 文件生命 make 的终极任务, 也就是执行文件 main 了.
这就是整个 make 的依赖性, make 会一层又一层地去找文件的依赖关系, 直到最终编译出第一个目标文件. 在找寻的过程中, 如果出现错误, 比如最后被依赖的文件找不到, 那么 make 就会直接退出, 并报错, 而对于所定义的命令的错误, 或是编译不成功, make 根本不理. make 只管文件的依赖性, 即如果在我找了依赖关系之后, 冒号后面的文件还是不在, 那么对不起, 我就不工作啦.
灵活编写 makefile
从前面的 makefile 编写来看, 当中我们每写一个依赖关系就需要写一个形如 gcc X.c -o X.o 生成命令, 这里还好, 若是较大的工程, 这样难免就太繁琐了, 所以据了解, 一般在公司专门编写 makefile 的人是不会那样写的. 还有写着更简洁方式, 就是利用下面这几个符号:
$^ 代表所有的依赖文件
$@ 代表所有的目标文件
$< 代表第一个依赖文件
于是便可以将上面的 makefile 改写成
.PHONY:clean
main: main.o add.o sub.o
main.o: main.c
gcc -c $< -o $@
add.o: add.c
gcc -c $^ -o $@
sub.o: sub.c
gcc -c $^ -o $@
- clean:
- rm -rf *.o
由于依赖的 都是中间目标文件. o , 如果 makefile 变得复杂, 那么我们就有可能会忘掉一个需要加入的地方, 而导致编译失败. 所以, 为了 makefile 的易维护, 在 makefile 中我们可以使用常量 (这里看了好多人都把它说成变量, 个人认为 它在后面并没有被改变, 因次叫常量更好) 那么还可以定义一个常量来表示所有的. o 文件, 于是便还可将它们写成这样
- .PHONY:clean
- OBJS = main.o\ //\ 转义字符
- add.o\
- sub.o
- main: $(OBJS)
- %.o : %.c
gcc -c $^ -o $@
- clean:
- -rm -rf $(OBJS)
这里的 %.o : %.c 想必都可以猜出来, 这代表的意思就是所有的. o 文件依赖相应的. C 文件, 这样便又省去好几步.
到这, 相信聪明的你, 可以更灵活编写 makefile 了. 最后, 再补充补充关于 Makefile 的东西
Makefile 还有什么
显式规则. 显式规则说明了, 如何生成一个或多个目标文件. 这是由 Makefile 的书写者明显指出, 要生成的文件, 文件的依赖文件, 生成的命令.
隐晦规则. 由于我们的 make 有自动推导的功能, 所以隐晦的规则可以让我们比较简略地书写 Makefile, 这是由 make 所支持的.
变量的定义. 在 Makefile 中我们要定义一系列的变量, 变量一般都是字符串, 这个有点像你 C 语言中的宏, 当 Makefile 被执行时, 其中的变量都会被扩展到相应的引用位置上.
文件指示. 其包括了三个部分, 一个是在一个 Makefile 中引用另一个 Makefile, 就像 C 语言中的 include 一样; 另一个是指根据某些情况指定 Makefile 中的有效部分, 就像 C 语言中的预编译 #if 一样; 还有就是定义一个多行的命令. 有关这一部分的内容, 我会在后续的部分中讲述.
注释. Makefile 中只有行注释, 和 UNIX 的 Shell 脚本一样, 其注释是用 "#" 字符, 这个就像 C/C++ 中的 "//" 一样. 如果你要在你的 Makefile 中使用 "#" 字符, 可以用反斜杠进行转义, 如:"\#".
在工程应用时, 我们的规则一般这样:
如果这个工程没有编译过, 那么我们的所有 c 文件都要编译并被链接.
如果这个工程的某几个 c 文件被修改, 那么我们只编译被修改的 c 文件, 并链接目标程序.
如果这个工程的头文件被改变了, 那么我们需要编译引用了这几个头文件的 c 文件, 并链接目标程序.
所以只要我们的 makefile 写得够好, 所有的这一切, 我们只用一个 make 命令就可以完成, make 命令会自动智能地根据当前的文件修改的情况来确定哪些文件需要重编译, 从而自己编译所需要的文件和链接目标程序.
来源: https://www.cnblogs.com/tp-16b/p/8955462.html