姊妹篇 《Makefile 入门》 中已经介绍过隐含规则、自动推导。要想在 make & Makefile 上精进,必须了解其隐含规则。它可以让你省去很多繁琐、重复的细节,快速高效地完成项目的编译和链接。
隐含规则依赖同名(同名源文件-同名目标文件-同名可执行文件),一般而言设置好编译器属性、给出必要的头文件目录,就可以使用隐含规则生成同名的目标文件(.o 文件);但实际项目中很少出现可执行文件由单独一个源文件/目标文件生成的情况,所以生成可执行文件(包括链接库)时一般不能只使用隐含规则达到目的。
只有一个源文件
假设目录下只有 test.c 一个源文件。
最简单的方式:(只使用隐含规则)
不写 Makefile 文件,直接执行 make test
。可以看到: make(其实是默认生成的 Makefile 文件) 默认使用 cc 编译器(不是 gcc)。
1 | vimer@debian8light:~/see-the-world/code/make_learn$ ls |
当目录下存在 test.o 文件时直接执行 make test
命令
1 | vimer@debian8light:~/see-the-world/code/make_learn$ ls |
可以看到 Makefile 隐含规则在生成可执行文件时倾向于将可执行文件放在命令的最后,即 ${CC} $^ -o $@
的形式。
扩展:我们看到上文中使用的都是 cc 编译器,我们更习惯 gcc/g++ 对不?而且有时候还需要给编译器指定参数,比如使用 c99 标准,怎么做呢?很简单,只需要在当前目录下新建以下 Makefile 文件:
1 | # 指定编译器和选项 |
再次执行 make test
看看效果吧
1 | vimer@debian8light:~/see-the-world/code/make_learn$ ls |
可以看到:Makefile 隐含规则会自动使用 CC
和 CFLAGS
变量,针对 .cpp
文件则自动使用 CXX
和 CXXFLAGS
变量。
也可以看出,直接使用源文件生成可执行文件时,更准确的格式是 ${CC} ${CFLAGS} $^ -o $@
。
最简单的 Makefile 文件:
其实最简单的 Makefile 文件就是没有 Makefile文件,其次就是上一节中“只指定编译器及选项”的 Makefile 文件。但也由于其太过简单,在实际生产中并不具备实用性。下面来看一个“麻雀虽小,五脏俱全”的例子:
1 | # 可执行文件 |
执行 make
命令。可以看到:Makefile 隐含规则在生成目标文件时倾向于将源文件放在命令的最后,即 ${CC} ${CLFAGS} -c -o $@ $<
的形式。
1 | vimer@debian8light:~/see-the-world/code/make_learn$ make |
多个源文件
比如说三个:test.c test-add.c test-sub.c。这种情况下,我们只需要将上述 Makefile 文件中的“依赖目标”稍作修改即可(修改后的 Makefile 依旧适用之前的案例)
1 | # 可执行文件 |
make
执行结果:
1 | vimer@debian8light:~/see-the-world/code/make_learn$ ls |
目录分级 & 头文件
目录结构如下:
1 | vimer@debian8light:~/see-the-world/code/make_learn$ tree |
其中 test.c 文件中如下引入头文件:
1 |
预处理错误
如果我们直接 make test
那么一定会报错 test.c:2:22: fatal error: test-add.h: 没有那个文件或目录
找不到头文件。我们可以通过以下方式解决这个错误:
在
make
命令中指定参数:make test CPPFLAGS='-Itest-add -Itest-sub'
,因为参数中包含空格,所以必须用引号括起来。此时使用的仍然是 cc 编译器,我们可以在命令行中指定CC=gcc
参数等等。使用 Makefile 文件:我们延用第一节中的例子
1
2
3
4
5
6
7
8
9
10
11# 指定编译器和选项
CC=gcc
CFLAGS=-Wall -std=c99
CXX=g++
CXXFLAGS=-Wall -std=c++11
# 给预处理器传参
CPPFLAGS=-I'./test-add' -I'./test-sub'
# CPPFLAGS=-I./test-add -I./test-sub # 这么写也可以
# CPPFLAGS='-I./test-add -I./test-sub' # 这么写报错,为什么呢?上述的执行结果:
1
2
3
4
5
6
7
8
9
10
11
12vimer@debian8light:~/see-the-world/code/make_learn$ ls
Makefile test-add test.c test-sub
vimer@debian8light:~/see-the-world/code/make_learn$ make test
gcc -Wall -std=c99 -I'./test-add' -I'./test-sub' test.c -o test
(作者备注:以下报错链接错误)
/tmp/ccQlkvYC.o:在函数‘main’中:
test.c:(.text+0x49):对‘add’未定义的引用
test.c:(.text+0x69):对‘sub’未定义的引用
collect2: error: ld returned 1 exit status
<builtin>: recipe for target 'test' failed
make: *** [test] Error 1
vimer@debian8light:~/see-the-world/code/make_learn$链接错误
我们使用第二节中的 Makefile,对“源文件”做相应修改就可:(当然也需要新添 CPPFLAGS
变量)
1 | # 可执行文件 |
成功执行:
1 | vimer@debian8light:~/see-the-world/code/make_learn$ make |
共享库
系统共享库
如果使用到系统共享库又该怎么做呢?我们知道共享库是用在链接阶段的,参考第一节,在隐含规则中 CPPFLAGS
是传给预处理器的,CFLAGS
是传给编译器的,相应的传给链接器的变量是 LDLIBS
。
假设 test.c 源文件中调用了 #include <math.h>
的 sin()
函数,那么只需要在链接时指定 LDLIBS=-lm
即可:
1 | vimer@debian8light:~/see-the-world/code/make_learn$ ls |
自定义共享库
在使用 make & Makefile 文件生成可执行文件过程中,使用系统共享库和自定义共享库的区别在于:后者需要使用 LDFLAGS
变量指定路径。
运行可执行文件又会有一些区别,关于链接库更多的知识请移步 《共享库 & 静态库》。
参考
整篇笔记的结构以及用到源码参考自 例说makefile。但此系列笔记有两个不足之处:
原文中的
DLIBS
INC
其实就是LDLIBS
和CPPFLAGS
变量。- 虽说如果不使用隐含规则,只是显式地使用,这些变量随便起什么名字都可以;
- 但既然使用 Makefile & make,那么完全放弃使用隐含规则有“大器小用”之嫌,和单纯使用 shell 脚本还有区别吗?
- 使用 Makefile 的隐含规则,就应该使用 其隐含规则用到的变量(限于 GNU make);
- 在 Why LD_LIBRARY_PATH is bad 中提到的
LD_RUN_PATH
变量,在隐含规则中是否生效? - DLIBS INC 是其他 make (非 GNU make)的预定义变量吗?
原文中讲到自定义共享库时并没有细致划分 soname、linker name、real name。这个只是小瑕疵