本文主要为陈皓的《跟我一起写Makefile》读书笔记。
目录
一,前言
笔者在初学C语言时使用的开发工具是VC++6.0,后来学习C++时,使用的是DEV-C++,再后来使用Visual Studio做软件开发。这些Windows下的IDE足够智能,编辑好代码后只需点一下Build && RUN,程序就生成好可以运行了。这会让我们误以为一个Hello.c到一个Hello.exe只需一步就可以完成。事实上不是这样,在Linux系统下,使用gcc或g++编译器能直观地体会到C/C++的目标代码文件到可执行文件的过程。
关于C/C++编译和链接的详细过程,可以参考博客:
C/C++预编译、编译、汇编和链接四个过程_罗三泡泡的博客-CSDN博客_编译链接四个步骤
C语言基础-从源代码到可执行文件的转换过程(编译和链接)_罗三泡泡的博客-CSDN博客_简述编辑、编译、连接、运行一个c语言程序的步骤
简而言之,C/C++源文件首先会生成中间目标文件,再由中间目标文件生成执行文件。
在编译时,编译器只检测程序语法,和函数、变量是否被声明。如果函数未被声明,编译器会给出一个警告,但可以生成 Object File。
而在链接程序时,链接器会在所有的 Object File 中找寻函数的实现,如果找不到,那到就会报链接错误码(Linker Error),在 VC 下,这种错误一般是: Link 2001 错误,意思说是说,链接器未能找到函数的实现。你需要指定函数的 Object File。
----《跟我一起学Makefile》
二,Makefile简介
make才是自动化编译工具,makefile是一个文件,相当于一个编译手册,make按照makefile这个操作手册,相应地执行编译和链接工作。此外,当项目编译好后,若修改了其中的某个文件,make命令会自动智能地根据当前的文件修改的情况来确定哪些文件需要重编译,从而自己编译所需要的文件和链接目标程序,而不是重新编译整个项目。当工程量大时,这一优点尤其重要。其具体的规则如下:
1)如果这个工程没有编译过,那么我们的所有 C 文件都要编译并被链接。
2)如果这个工程的某几个 C 文件被修改,那么我们只编译被修改的 C 文件,并链接目
标程序。
3)如果这个工程的头文件被改变了,那么我们需要编译引用了这几个头文件的 C 文件,
并链接目标程序。----《跟我一起学Makefile》
三,Makefile简单规则
Makefile的规则主要包含三部分:
target(目标文件) : prerequisites(依赖文件)
command (依赖文件到目标文件的生成命令)
Makefile规则 = 目标 + 依赖 + 命令
关于Makefile文件的简单编写,可以参考:Makefile文件的简单编写_wangjl~的博客-CSDN博客_makefile文件编写
比如现在有main.c和printHi.c两个c文件和printHi.h头文件:
//printHi.h
#include<stdio.h>
void printHi();
//PrintHi.c
#include"printHi.h"
void printHi()
{
printf("Hi C \n");
}
//main.c
#include"printHi.h"
#include<stdio.h>
int main()
{
printHi();
return 0;
}
在Linux系统下,使用gcc采用命令行的方式进行编译:
首先分别将c文件编译成目标文件(.o):
gcc -c main.c
gcc -c printHi.c
然后再将两个目标文件链接成可执行文件:
gcc main.o printHi.o -o main
执行main文件可得:
# ./main
Hi C
上述过程可以总结为:将main.c和printHi.c两个源文件 编译链接成 一个名为 main 的可执行文件,其中main为最终的目标。根据 Makefile规则 = 目标 + 依赖 + 命令 的规则,我们的makefile文件可以这样写:
#makefile
# main 是最终目标, 它依赖于main.o printHi.o两个文件
main: main.o printHi.o
#下面是生成目标main的具体命令:
gcc main.o printHi.o -o main
#make检测到生成 main需要main.o printHi.o,但是这两个.o文件实际并不存在,所以还需继续生成:
# 需要生成 main.o,把它当中目标,它依赖于main.c
main.o: main.c
gcc -c main.c
# 需要生成 printHi.o,把它当中目标,它依赖于printHi.c ,printHi.h头文件可加可不加,会自动识别加入
printHi.o: printHi.c
gcc -c printHi.c
最后在命令行中输入 make 命令:
可以看到它依次执行了makefile规则中的三条命令。通过命令的执行顺序可以发现,它是根据:
main: main.o printHi.o
这条目标与依赖的关系顺序执行的:main 是最终目标,它依赖于main.o和printHi.o两个文件,所以它先去生成main.o,再去生成printHi.o。
最后的包含源文件的文件夹中共有以下七个文件:
四,清空目标文件的规则
上面的makefile文件其实是不完整的,每个 Makefile 中都应该写一个清空目标文件(.o 和执行文件)的规则,这不仅便于重新编译,也很利于保持文件的整洁。所以我们的makefile可以这么写:
#makefile
# main 是最终目标, 它依赖于main.o printHi.o两个文件
main: main.o printHi.o
#下面是生成目标main的具体命令:
gcc main.o printHi.o -o main
#make检测到生成 main需要main.o printHi.o,但是这两个.o文件实际并不存在,所以还需继续生成:
# 需要生成 main.o,把它当中目标,它依赖于main.c
main.o: main.c
gcc -c main.c
# 需要生成 printHi.o,把它当中目标,它依赖于printHi.c ,printHi.h头文件可加可不加,会自动识别加入
printHi.o: printHi.c
gcc -c printHi.c
#清理目标文件和生成的可执行文件
.PHONY: clean
clean:
rm main.o printHi.o main
在makefile的末尾增加了几行:
.PHONY: clean
clean:
rm main.o printHi.o main
其中.PHONY表示clean是一个“伪目标”,“伪目标”并不是一个文件,只是一个标签,由于“伪目标”不是文件,所以 make 无法生成它的依赖关系和决定它是否要执行。我们可以通过显示的命令:
make clean
让其命令生效。此外,文件删除命令还可以简化成:
rm *.o main
意为删除所有以o为后缀的文件以及main文件。
此外还有一种更稳健的写法:
.PHONY: clean
clean:
-rm *.o main
在 rm 命令前面加了一个小减号的意思就是,也许某些文件出现问题,但不要管,继续做后面的事。还有一个不成文的规矩就是:“clean 从来都是放在文件的最后”。所以我们的makefile文件可以写成:
#makefile
# main 是最终目标, 它依赖于main.o printHi.o两个文件
main: main.o printHi.o
#下面是生成目标main的具体命令:
gcc main.o printHi.o -o main
#make检测到生成 main需要main.o printHi.o,但是这两个.o文件实际并不存在,所以还需继续生成:
# 需要生成 main.o,把它当中目标,它依赖于main.c
main.o: main.c
gcc -c main.c
# 需要生成 printHi.o,把它当中目标,它依赖于printHi.c ,printHi.h头文件可加可不加,会自动识别加入
printHi.o: printHi.c
gcc -c printHi.c
#清理目标文件和生成的可执行文件
.PHONY: clean
clean:
-rm *.o main
五,makefile使用变量
在上述makefile中“main.o printHi.o”重复出现了多次,当前只有两个工程文件,所以多敲几次“main.o printHi.o”也没什么,但是当自己的项目越写越大,目标文件越来越多,每增加一个c文件,就要在各个出现过目标文件的地方添加一个目标文件。如果忘掉一个需要加入的地方,就可能导致编译失败。所以,为了 makefile 的易维护,在 makefile 中我们可以使用变量。
makefile 的变量也就是一个字符串,可以理解成 C语言中的宏,就是简单的字符替换。具体写法为:
#makefile
# 定义变量 OBJ
OBJ = printHi.o main.o
# main 是最终目标, 它依赖于main.o printHi.o两个文件
# 使用 $( )解引用
main: $(OBJ) ,相当于字符替换
#下面是生成目标main的具体命令:
gcc $(OBJ) -o main
#make检测到生成 main需要main.o printHi.o,但是这两个.o文件实际并不存在,所以还需继续生成:
# 需要生成 main.o,把它当中目标,它依赖于main.c
main.o: main.c
gcc -c main.c
# 需要生成 printHi.o,把它当中目标,它依赖于printHi.c ,printHi.h头文件可加可不加,会自动识别加入
printHi.o: printHi.c
gcc -c printHi.c
#清理目标文件和生成的可执行文件
.PHONY: clean
clean:
-rm $(OBJ) main
其中,我们定义了一个 OBJ 的变量:
# 定义变量 OBJ
OBJ = printHi.o main.o
# 使用 $( )解引用,相当于字符替换
main: $(OBJ)
其中使用 $() 符号来解引用,相当于字符替换。当项目中的c源文件增加时,只需要改动 OBJ 变量,在其后面增加新的目标文件。比如新增加一个名为 Hello.c文件,其makefile的写法如下:
#makefile
# 定义变量 OBJ
OBJ = printHi.o main.o hello.o
# main 是最终目标, 它依赖于main.o printHi.o两个文件
# 使用 $( )解引用
main: $(OBJ) ,相当于字符替换
#下面是生成目标main的具体命令:
gcc $(OBJ) -o main
#make检测到生成 main需要main.o printHi.o,但是这两个.o文件实际并不存在,所以还需继续生成:
# 需要生成 main.o,把它当中目标,它依赖于main.c
main.o: main.c
gcc -c main.c
# 需要生成 printHi.o,把它当中目标,它依赖于printHi.c ,printHi.h头文件可加可不加,会自动识别加入
printHi.o: printHi.c
gcc -c printHi.c
# 新增的hello.c文件
hello.o: hello.c
gcc -c hello.c
#清理目标文件和生成的可执行文件
.PHONY: clean
clean:
-rm $(OBJ) main
五,make自动推导
上面的makefile文件还是不够简约,虽然可以定义变量来解决新增文件,需要到处加添加目标文件的问题,但还是需要一个一个写重复性的生成目标文件的规则:
main.o: main.c
gcc -c main.c
printHi.o: printHi.c
gcc -c printHi.c
hello.o: hello.c
gcc -c hello.c
这些规则重复性高,写起来不厌其烦。事实上make的功能很强大,它可以自动推导文件以及文件依赖关系后面的命令,于是我们就没必要去在每一个[.o]文件后都写上类似的命令,因为,我们的 make 会自动识别,并自己推导命令。
只要 make 看到一个[.o]文件,它就会自动的把[.c]文件加在依赖关系中,如果 make 找到一个 hello.o,那么 hello.c 就会是 hello.o 的依赖文件。并且命令 gcc -c hello.c 也会被推导出来,于是,我们的 makefile 再也不用写得这么复杂:
#makefile
OBJ = printHi.o main.o hello.o
main: $(OBJ)
gcc $(OBJ) -o main
.PHONY: clean
clean:
-rm $(OBJ) main
以上就是makefile的简单使用的最终版,使用了make的变量和自动推导功能,极大简化了makefile的编写过程。