设为首页 加入收藏

TOP

21.4 自动处理头文件的依赖关系
2013-10-12 06:59:01 来源: 作者: 【 】 浏览:110
Tags:21.4 自动 处理 文件 依赖 关系

21.4  自动处理头文件的依赖关系

现在我们的Makefile写成这样:

  1. all: main  
  2.  
  3. main: main.o stack.o maze.o  
  4.         gcc $^ -o $@  
  5.  
  6. main.o: main.h stack.h maze.h  
  7. stack.o: stack.h main.h  
  8. maze.o: maze.h main.h  
  9.  
  10. clean:  
  11.         -rm main *.o  
  12.  
  13. .PHONY: clean 

按照惯例,用all做缺省目标。还有一个问题比较麻烦,在写main.o、stack.o和maze.o这三个目标的规则时要查看源代码,找出它们依赖于哪些头文件,这很容易出错,一是因为有的头文件包含在另一个头文件中,在写规则时很容易遗漏,二是如果以后修改源代码改变了依赖关系,很可能忘记修改Makefile的规则。

在源代码中已经包含了目标文件和头文件之间的依赖关系,这种依赖关系是以#include的形式描述的,如果在Makefile中以规则的形式再描述一遍,就存在重复信息了。手工维护重复信息很容易出错,应该想办法自动维护。以前我们讲过定义一个宏然后在代码中多处引用它可以避免硬编码,讲过写一个头文件然后包含在很多.c文件中可以避免在这些.c文件中重复声明,道理都是一样的:如果某信息在多处重复出现,要修改它应该只在一个地方手工修改,而在其他地方自动获得更新后的信息,这称为DRY(Don't Repeat Yourself)原则。现在我们要想办法把源代码中的依赖关系信息抽取出来自动转换成Makefile中的规则。第一步,用gcc的-M选项自动分析目标文件和源文件的依赖关系,以Makefile规则的格式输出:

  1. $ gcc -M main.c  
  2. main.o: main.c /usr/include/stdio.h /usr/include/features.h \  
  3.  /usr/include/bits/predefs.h /usr/include/sys/cdefs.h \  
  4.  /usr/include/bits/wordsize.h /usr/include/gnu/stubs.h \  
  5.  /usr/include/gnu/stubs-32.h \  
  6.  /usr/lib/gcc/i486-linux-gnu/4.4.3/include/stddef.h \  
  7.  /usr/include/bits/types.h /usr/include/bits/typesizes.h \  
  8.  /usr/include/libio.h /usr/include/_G_config.h /usr/include/wchar.h \  
  9.  /usr/lib/gcc/i486-linux-gnu/4.4.3/include/stdarg.h \  
  10.  /usr/include/bits/stdio_lim.h /usr/include/bits/sys_errlist.h main.h \  
  11.  stack.h maze.h 

注意在Makefile的规则中也可以使用类似C语言的续行符\。gcc -M的输出结果不仅包括我们自己写的头文件main.h、stack.h和maze.h,还包括stdio.h和其他系统头文件,因为我们的程序中包含了stdio.h,而后者又包含了其他系统头文件。系统头文件通常不需要随我们的程序一起维护,所以通常不用gcc的-M选项而是用-MM选项,输出结果中只包括我们自己写的头文件:

  1. $ gcc -MM *.c  
  2. main.o: main.c main.h stack.h maze.h  
  3. maze.o: maze.c maze.h main.h  
  4. stack.o: stack.c stack.h main.h 

接下来的问题是怎么把这些规则添加到Makefile中,Scott McPeak提供了一个很好的解决方案(http://scottmcpeak.com/autodepend/autodepend.html):

  1. all: main  
  2.  
  3. main: main.o stack.o maze.o  
  4.         gcc $^ -o $@  
  5.  
  6. clean:  
  7.         -rm main *.o *.d  
  8.  
  9. .PHONY: clean  
  10.  
  11. OBJS = main.o stack.o maze.o  
  12.  
  13. -include $(OBJS:.o=.d)  
  14.  
  15. %.o: %.c  
  16.         gcc -c $(CFLAGS) $*.c -o $*.o  
  17.         gcc -MM $(CPPFLAGS) $*.c > $*.d  
  18.         mv -f $*.d $*.d.tmp  
  19.         sed -e 's|.*:|$*.o:|' < $*.d.tmp > $*.d  
  20.         sed -e 's/.*://' -e 's/\\$$//' < $*.d.tmp | fmt -1 | \  
  21.         sed -e 's/^ *//' -e 's/$$/:/' >> $*.d  
  22.       rm -f $*.d.tmp 

OBJS变量的值是我们要编译生成的.o文件的列表,$(OBJS:.o=.d)是变量值的替换语法,把OBJS变量中每一项的.o后缀替换成.d后缀,所以include这一句相当于:

  1. -include main.d stack.d maze.d 

类似于C语言的#include预处理指示,在Makefile中include main.d stack.d maze.d表示把这三个文件包含到当前的Makefile中,这三个文件也应该符合Makefile的语法。我们在include前面加了一个-,表示如果它要包含的文件列表中有的文件不存在,则忽略不存在的文件,只包含存在的文件。如果include前面没有-,而它要包含的文件又不存在,则make会报错。

事实上,include不只是把文件包含进来这么简单,还要做一个特殊处理:对于要包含进来的每一个文件,make会把文件名当做目标来尝试更新(如果有规则匹配该目标则执行该规则下的命令,如果没有规则能匹配该目标就算了),如果make检查完所有要包含进来的文件后,其中确实有些文件被更新了,则make会从头开始执行,重新包含更新后的文件。详见参考文献[27]的3.7节。对于我们这个例子不必考虑这种复杂的情况,因为我们没有规则能匹配以.d为后缀的目标,不会更新main.d、stack.d或maze.d。

下面我们分几个不同的场景来分析当.c和.h文件的依赖关系发生变化时make如何更新规则来适应这些变化。

1.如果你的工作目录是干净的,只有.c文件、.h文件和Makefile文件,执行make命令的结果是:

  1. $ make  
  2. gcc -c  main.c -o main.o  
  3. gcc -MM  main.c > main.d  
  4. mv -f main.d main.d.tmp  
  5. sed -e 's|.*:|main.o:|' < main.d.tmp > main.d  
  6. sed -e 's/.*://' -e 's/\\$//' < main.d.tmp | fmt -1 | \  
  7.           sed -e 's/^ *//' -e 's/$/:/' >> main.d  
  8. rm -f main.d.tmp  
  9. gcc -c  stack.c -o stack.o  
  10. gcc -MM  stack.c > stack.d  
  11. mv -f stack.d stack.d.tmp  
  12. sed -e 's|.*:|stack.o:|' < stack.d.tmp > stack.d  
  13. sed -e 's/.*://' -e 's/\\$//' < stack.d.tmp | fmt -1 | \  
  14.           sed -e 's/^ *//' -e 's/$/:/' >> stack.d  
  15. rm -f stack.d.tmp  
  16. gcc -c  maze.c -o maze.o  
  17. gcc -MM  maze.c > maze.d  
  18. mv -f maze.d maze.d.tmp  
  19. sed -e 's|.*:|maze.o:|' < maze.d.tmp > maze.d  
  20. sed -e 's/.*://' -e 's/\\$//' < maze.d.tmp | fmt -1 | \  
  21.           sed -e 's/^ *//' -e 's/$/:/' >> maze.d  
  22. rm -f maze.d.tmp  
  23. gcc main.o stack.o maze.o -o main 

由于main.d、stack.d和maze.d一个都不存在,所以include忽略它们,不包含任何文件。然后根据%.o: %.c规则,我们要更新目标main.o、stack.o和maze.o,它们的处理过程是一样的,我们以main.o为例解释一下具体的步骤:

执行gcc -c main.c -o main.o,由main.c编译出main.o。

执行gcc -MM main.c > main.d,把main.c的依赖关系写入文件main.d,其内容是main.o: main.c main.h stack.h maze.h。这条规则在本次make的处理过程中不起作用,但下次执行make时会把这个main.d包含进来,这条规则就会起作用了。上一条命令生成main.o,而这一条命令生成main.d,也就是说:只要生成一个.o文件,就要配合它生成一个.d文件,以便下次make时可以根据.d文件中的规则检查这个.o文件需不需要更新。

由gcc -MM生成的main.d还不能完全满足我们的要求,接下来的几条命令把main.d中的规则改成这样:

  1. main.o: main.c main.h stack.h maze.h  
  2. main.c:  
  3. main.h:  
  4. stack.h:  
  5. maze.h: 

我们稍后会分析为什么要改成这样。sed和fmt命令的用法是另一个复杂的话题,在这里就不细讲了,sed的作用是在文件中做编辑、查找、替换,fmt的作用是段落排版。

2.如果修改了头文件stack.h再重新make,执行结果是:

  1. $ make  
  2. gcc -c  main.c -o main.o  
  3. gcc -MM  main.c > main.d  
  4. mv -f main.d main.d.tmp  
  5. sed -e 's|.*:|main.o:|' < main.d.tmp > main.d  
  6. sed -e 's/.*://' -e 's/\\$//' < main.d.tmp | fmt -1 | \  
  7.           sed -e 's/^ *//' -e 's/$/:/' >> main.d  
  8. rm -f main.d.tmp  
  9. gcc -c  stack.c -o stack.o  
  10. gcc -MM  stack.c > stack.d  
  11. mv -f stack.d stack.d.tmp  
  12. sed -e 's|.*:|stack.o:|' < stack.d.tmp > stack.d  
  13. sed -e 's/.*://' -e 's/\\$//' < stack.d.tmp | fmt -1 | \  
  14.           sed -e 's/^ *//' -e 's/$/:/' >> stack.d  
  15. rm -f stack.d.tmp  
  16. gcc main.o stack.o maze.o -o main 

由于Makefile中包含了main.d和stack.d,也就是包含了规则main.o: main.c main.h stack.h maze.h和stack.o: stack.c stack.h main.h,所以修改stack.h将导致main.o和stack.o被更新,但这两条规则没有命令列表,怎么更新呢?注意到它们和%.o: %.c规则属于"一个目标拆开写多条规则"的情况,所以会执行%.o: %.c规则的命令列表。最后,main.o和stack.o被更新又导致可执行文件main被更新。

3.如果添加了一个头文件foo.h,并且在main.c中加了一行#include "foo.h",再重新make,执行结果是:

  1. $ make  
  2. gcc -c  main.c -o main.o  
  3. gcc -MM  main.c > main.d  
  4. mv -f main.d main.d.tmp  
  5. sed -e 's|.*:|main.o:|' < main.d.tmp > main.d  
  6. sed -e 's/.*://' -e 's/\\$//' < main.d.tmp | fmt -1 | \  
  7.       sed -e 's/^ *//' -e 's/$/:/' >> main.d  
  8. rm -f main.d.tmp  
  9. gcc main.o stack.o maze.o -o main 

根据规则%.o: %.c,修改main.c导致main.o被更新,在执行命令列表时重新生成了main.d,其内容为:

  1. main.o: main.c main.h stack.h maze.h foo.h  
  2. main.c:  
  3. main.h:  
  4. stack.h:  
  5. maze.h:  
  6. foo.h: 

把foo.h也包含到规则里了,下次如果修改foo.h并重新make,也会导致main.o被更新。

4.如果删除头文件foo.h再重新make,执行结果是:

  1. $ make  
  2. gcc -c  main.c -o main.o  
  3. main.c:6:17: error: foo.h: No such file or directory  
  4. make: *** [main.o] Error 1 

我们知道Makefile中包含了规则main.o: main.c main.h stack.h maze.h foo.h,但foo.h已经不存在了,make不是应该报错吗?为什么还会执行%.o: %.c的命令列表呢?

在这种情况下另外一条规则foo.h:起作用了,像这种既没有依赖条件也没有命令列表的规则是这样处理的:如果以该目标作为文件名的文件不存在,则认为该目标需要更新,由于没有命令列表,执行该规则并不会生成文件,但会认为该目标已被更新,因此依赖该目标的其他目标也要被更新。详见参考文献[27]的4.6节。因此,foo.h不存在将导致main.o被更新。

】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
分享到: 
上一篇一站式学习C编程 目录 下一篇22.8 函数类型和函数指针类型

评论

帐  号: 密码: (新用户注册)
验 证 码:
表  情:
内  容: