首先,假设当前工程目录为prj/,该目录下有6个文件,分别是:main.c、abc.c、xyz.c、abc.h、xyz.h和Makefile。其中main.c包含头文件abc.h和xyz.h,abc.c包含头文件abc.h,xyz.c包含头文件xyz.h,而abc.h又包含了xyz.h。它们的依赖关系如图。
第一次使用Makefile应该写成这个样子(假设生成目标main):
1. main:main.o abc.o xyz.o
2. gcc main.o abc.o xyz.o -o main
3. main.o:main.c abc.h xyz.h
4. gcc -c main.c –o main.o -g
5. abc.o:abc.c abc.h xyz.h
6. gcc -c abc.c –o abc.o -g
7. xyz.o:xyz.c xyz.h
8. gcc -c xyz.c -o xyz.o -g
9. clean:
10. rm main main.o abc.o xyz.o -f
虽然这样Makefile完全符合Makefile的书写规则,但是当代码文件再增加几倍后,再管理这些命令将会是一个噩梦!!!因此Makefile提供了默认规则和自动推导帮我们完成一些常用功能。然后,我们将Makefile修改如下:
1. EXE=main
2. CC=gcc
3. OBJ=main.o abc.o xyz.o
4. CFLAGS=-g
5. $(EXE):$(OBJ)
6. $(CC) $^ -o $@
7. clean:
8. rm $(EXE) $(OBJ) -f
变量EXE,CC,OBJ分别代指目标程序名,编译器名,目标文件名。CFLAGS是C编译选项,它会附加在每条编译命令(gcc -c)之后。$(EXE)是对变量的引用,$^代指所有的依赖项——即$(OBJ),$@代指目标项——即$(EXE)。
该命令等价于:$(CC) $(OBJ) -o $(EXE)。
这个Makefile只有目标文件链接的命令,源文件的编译命令都被忽略了!这正是Makefile的自动推导功能——它可以将目标文件自动依赖于同名的源文件,即:
1. main.o:main.c
2. gcc -c main.c -o main.o
3. abc.o:abc.c
4. gcc -c abc.c -o abc.o
5. xyz.o:xyz.c
6. gcc -c xyz.c -o xyz.o
按照上述方式,只要工程下增加了源文件后,只需要在OBJ初始化处增加一个*.o即可。但是这种方式是有问题的,Makefile的自动推导功能只会推导出目标文件对源文件的依赖关系,而不会在依赖关系中添加头文件!这导致的直接问题就是:当第一次执行make后,再次修改依赖的abc.h、xyz.h头文件的内容,自动推导功能只会去检测.c文件的修改时间戳,发现没有变化则不会再次编译生成main.o、abc.o、xyz.o文件,进一步导致不会进行重新make链接生成目标文件(因为检测到main.o、abc.o、xyz.o文件没有变化)!除非修改头文件后运行一次make clean,再运行make.
为了能让make自动包含头文件的依赖关系,gcc为我们提供了一个编译选项(gcc -M,对于g++是-MM),能输出目标文件的依赖关系!比如执行:
gcc -M main.c
会终端显示:
main.o:main.c abc.h xyz.h
注意:如果是不在当前路径下的头文件会显示出全路径!
如果将每个源文件的依赖关系包含到Makefile里,就可以使得目标文件自动依赖于头文件了!再次修改原先的Makefile:
1. EXE=main
2. CC=gcc
3. SRC=$(wildcard *.c)
4. OBJ=$(SRC:.c=.o)
5. CFLAGS=-g
6. all:depend $(EXE)
7. depend:
8. @(CC)−MM(SRC) > .depend
9. -include .depend
10. $(EXE):$(OBJ)
11. $(CC) $(OBJ) -o $(EXE)
12. clean:
13. @rm $(EXE) $(OBJ) .depend -f
创建了一个伪目标:all,它依赖于目标depend和实际的目标EXE。而depend正是将所有源文件对应的目标文件的依赖关系输入到.depend文件,并被包含在Makefile内!这里有几个细节需要说明:
1..depend文件是隐藏文件,避免和工程的文件混淆。
2.include命令之前增加符号'-',避免第一次make时由于.depend文件不存在报告错误信息。
3.SRC初始化为wildcard *.c表示当前目录下的所有.c源文件,这就省去了我们手动输入新增的源文件。
4.OBJ初始化为SRC:.c=.o,表示将SRC中所有.c结尾的文件名替换为.o结尾的,这样就自动生成了源文件的目标文件序列。
5.clean的rm命令钱@符号表示执行该命令时不回显命令。
这样,每次执行make时都会重新计算目标文件的依赖关系,并输出到.depend文件,然后包含到Makefile后进行编译工作,这样目标文件的依赖关系就不会出错了!而我们得到了一个能自动包含源文件和识别头文件依赖关系的Makefile,将该文件应用于任何单目录的C/C++工程(C++需要修改部分细节,不作赘述)都能正常工作。
但是,这种方式也有一定的不足,当头文件的依赖关系不发生变化时,每次make也会重新生成.depend文件。如果这样使得工程的编译变得不尽人意,那么我们可以尝试将依赖文件拆分,使得每个源文件独立拥有一个依赖文件,这样每次make时变化的只是一小部分文件的依赖关系。
1. EXE=main
2. CC=gcc
3. SRC=$(wildcard *.c)
4. OBJ=$(SRC:.c=.o)
5. DEP=(patsubst(SRC))
6. CFLAGS=-g
7. $(EXE):$(OBJ)
8. $(CC) $^ -o $@
9. $(DEP):.%.d:%.c
10. @set -e;
11. rm -f $@;
12. $(CC) −M $< > @.$$$;
13. sed 's,$∗\.o[ :]*,\1.o $@ : ,g' < $@.
14. > $@;
15. rm -f @.$$$
16. -include $(DEP)
17. clean:
18. @rm $(EXE) $(OBJ) $(DEP) -f
注意,上面10-15行其实是一条命令,make只会创建一个Shell进程执行这条命令,这条命令分为5个子命令,用;号隔开。执行步骤为:
1)set-e命令设置当前Shell进程为这样的状态:如果它执行的任何一条命令的退出状态非零则立刻终止,不再执行后续命令。@表示makefile执行这条命令时不显示出来
2)把原来的.d文件删掉。
3)$<依赖的目标集(即*.c), -M表示生成文件依赖关系, $@表示生成的目标文件(即*.d),$$表示本身的ProcessID。注意,在Makefile中$有特殊含义,如果要表示它的字面意思则需要写两个$,所以Makefile中的四个$传给Shell变成两个$,两个$在Shell中表示当前进程的id,一般用它给临时文件起名,以保证文件名唯一。
4)这个sed命令比较复杂,就不细讲了,主要作用是查找替换,并加入.d的依赖关系。
5)最后把临时文件删掉。
该Makefile增加了一个变量DEP,初始化为patsubst %.c,.%.d,$(SRC),表示将SRC中的以*.c结尾的源文件名替换为.*.d的形式,比如main.c对应着文件.main.d,这就是main.c的依赖关系文件,且是隐藏的。
为了生成每个源文件的依赖文件,建立了目标依赖关系$(DEP):.%.d:%.c,该关系表示,对于目标DEP,通过$@可以访问一个依赖文件,通过$>则访问对应的同名源文件。命令部分使用连接,表示当前命令作为一个整体在一个进程内执行。该组命令的含义是:将gcc -M生成的信息输出到一个临时文件,然后在:之前加上当前的文件名输出到依赖文件。比如对于main.c生成的临时文件信息为:
main.o:main.c abc.h xyz.h
处理后依赖文件信息是:
main.o .main.d:main.c abc.h xyz.h
这样的依赖关系表示main.o和它的依赖关系文件的依赖项是一致的,只要相关的源文件或头文件发生了改变,才会重新生成目标文件和依赖关系文件,也就达到了依赖关系文件单独更新的目的了。
虽然如此,但是这样的Makefile也不是完美的。现假设工程目录内新增一个源文件lmn.c,按照Makefile的指令make后会产生.lmn.d依赖关系文件。而如果我们再删除lmn.c源文件后,重新make后.lmn.d依然存在!尤其是当重复增删很多源文件后,工程目录下可能会存在很多无用的依赖文件,当然这些问题可以通过make clean解决。
通过前边的讨论,我们得到一个能在单目录工程下工作的通用Makefile,至于是实现为单独一个依赖文件的形式,还是每个源文件产生一个独立的依赖文件,要根据程序作者自己的喜恶来选择。虽然每种方法都有一些细微的瑕疵,但是不影响这个通用的Makefile的实用性,试想一下在工程目录下拷贝一份当前的Makefile,稍加修改便可以正确的编译开发,一定会令人心情大好。