千家信息网

自动生成依赖关系(十)

发表于:2025-12-02 作者:千家信息网编辑
千家信息网最后更新 2025年12月02日,我们在之前的 makefile 学习中,其目标文件(.o)只依赖于源文件(.c)。那么如果在源文件中还包含有头文件,此时编译器如何编译源文件和头文件呢?我们来看看编译行为带来的缺陷:1、预处理器将头文
千家信息网最后更新 2025年12月02日自动生成依赖关系(十)

我们在之前的 makefile 学习中,其目标文件(.o)只依赖于源文件(.c)。那么如果在源文件中还包含有头文件,此时编译器如何编译源文件和头文件呢?我们来看看编译行为带来的缺陷:1、预处理器将头文件中的代码直接插入源文件;2、编译器只通过预处理后的源文件产生目标文件;3、规则中以源文件为依赖,命令就可能无法执行。

我们来看看下面的 makefile 有没有问题


makefile 源码

OBJS := func.o main.ohello.out : $(OBJS)    @gcc -o $@ $^    @echo "Target File ==> $@"$(OBJS) : %.o : %.c    @gcc -o $@ -c $^


func.h 源码

#ifndef _FUNC_H_#define _FUNC_H_#define HELLO "Hello D.T."void foo();#endif


func.c 源码

#include #include "func.h"void foo(){    printf("void foo() : %s\n", HELLO);}


main.c 源码

#include #include "func.h"int main(){    foo();    return 0;}

我们来看看编译结果

我们看到已经正确实现了字符串的打印,那么我们接下来在 func.h 源文件中想要改掉这个字符串为 Software 呢?试试看能不能修改成功

我们看到在重新编译的时候,它并没有因为头文件的改变而改变,我们在 makefile 中又没有进行头文件的相关添加,改掉头文件中的内容肯定是不动的。下来我们在模式规则中加上头文件,在 %.c 后加上 func.h,再来看看编译结果

我们看到直接添加之后,编译出错了。因为 -c 后面的目标中含有头文件,所以不能直接进行编译。我们可以只编译 %.o 后面的第一依赖 %.c,这样就不会去编译 func.h 头文件了,将下面的 $^ 改为 $< ,我们来看看效果

我们看到已经正确改过来了。经过上面的实验,我们看到:头文件作为依赖条件出现于每个目标对应的规则中,当头文件改动时,任何源文件都将会被重新编译(编译低效);当项目中头文件巨大时,makefile 将很难维护。那么我们的头脑中不禁会冒出这么个想法:通过命令对自动生成对头文件的依赖;将生成的依赖自动包含进 makefile 中;当头文件改动后,自动确认需要重新编译的文件。那么此时我们还需要知道一个命令,Linux 中的 sed 命令。sed 是一个流编辑器,用于流文本的修改(增、删、查、改);它可用于流文本中的字符串替换,其字符串替换方式为:sed 's:src:des:g',具体格式如下

sed 同样也支持正则表达式,在 sed 中可以用正则表达式匹配替换目标,并且可以使用匹配的目标生成替换结果。格式如下

下来我们以代码为例来看看 sed 命令是如何使用的

再来看看 gcc 关键编译选项,获取目标的完整依赖关系:gcc -M test.c;获取目标的部分依赖关系:gcc -MM test.c。makefile 如下

.PHONY : testtest :    gcc -M main.c

编译结果如下

我们看到 -M 是获取了它的所有依赖关系,再来试试 -MM 呢

我们看到 -MM 后,它只依赖与 main.c func.h。我们可以拆分目标的依赖,即将目标的完整依赖差分为多个部分依赖。格式如下

我们来做个实验

.PHONY : a b ctest : a btest : b ctest :    @echo "$^"

我们来打印看看目标 test 的依赖都有哪些,编译结果如下

那么我们思考下:如何将 sed 和 gcc -MM 用于 makefile,并自动生成依赖关系呢?

我们再来看看 makefile 中的 include 关键字,它类似于 C 语言中的 include,是将其它文件的内容原封不动的搬入当前文件。make 对 include 关键字的处理方式是在当前目录下搜索或指定搜索目标文件。如果搜索一成功,便将文件内容搬入当前 makefile 中;如果搜索失败,将会产生警告,以文件名作为目标查找并执行对应规则,当文件名对应的规则不存在时,最终产生错误。格式如下

下来还是以代码为例来进行说明

.PHONY : testinclude test.txtall :    @echo "this is $@"test.txt :    @echo "test.txt"    @touch test.txt

我们在第 3 行包含 test.txt,可是当前目录下并没有 test.txt,然后触发 test.txt 的规则。因而会打印出 test.txt,然后再创建 test.txt,我们来看看编译结果

我们看到确实是创建了一个 test.txt 文件。那么在 makefile 中命令的执行是:1、规则中的每个命令默认是在一个新的进程中执行(Shell);2、可以通过接续符(;)将多个命令组合成一个命令;3、组合的命令依次在同一个进程中被执行;4、set -e 指定发生错误后立即退出执行。那么我们看看下面的代码会实现想要的功能吗?

.POHONY : allall :    mkdir test    cd test    mkdir subtest

我们来看看编译结果

我们看到在当前目录下创建了目录,但是 subtest 目录却不是在 test 目录下创建的,这是怎么回事呢?在第一条命令执行时创建了目录 test,此时这个进程已经关闭了;在第二条命令执行时,执行的是另一个进程,虽然它已经进入到目录 test 中,但是随着这个进程的关闭,又回到了当前目录;第三个进程是重新创建了目录 subtest。那么如何解决这个问题呢?直接利用 set -e 和 接续符来解决

.PHONY : testall :    set -e; \    mkdir test; \    cd test; \    mkdir subtest

看看编译结果


那么我们之前思考问题的初步思路是:1、通过 gcc -MM 和 sed 得到 .dep 依赖文件(目标的部分依赖),技术点是规则中命令的连续执行;2、通过 include 指令包含所有的 .dep 依赖文件。技术点是当 .dep 依赖文件不存在时,使用规则自动生成。下面我们来看看解决方案是怎样的

ONY : all cleanMKDIR := mkdirRM := rm -frCC := gccSRCS := $(wildcard *.c)DEPS := $(SRCS:.c=.dep)include $(DEPS)all :    @echo "all"        %.dep : %.c    @echo "Creating $@ ..."    @set -e; \    $(CC) -MM -E $^ | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@clean :    $(RM) $(DEPS)

我们来看看编译结果

我们先来分析下,在执行 make all 前,它先通过 include 包含 $(DEPS),通过 $(DEPS) 触发模式规则,进而创建文件夹。我们看到在前面出现两个没有文件夹的信息,其实这条信息是可以隐藏的。我们在 include 前面加上 - 就 OK,来看看效果

我们看到并没打印出前面的两条信息了。那么我们再来思考下:如何组织依赖文件相关的规则与源码编译相关的规则,进而形成功能完整的 makefile 程序呢?我们如何在 makefile 中组织 .dep 文件到指定目录呢?初步想法是当 include 发现 .dep 文件不存在时:1、通过规则和命令创建 deps 文件;2、将所有 .dep 文件创建到 deps 文件夹;3、.dep 文件中记录目标文件的依赖关系

我们下来看看初步的代码设计是怎样的

.PHONY : all cleanMKDIR := mkdirRM := rm -rfCC := gccDIR_DEPS := depsSRCS := $(wildcard *.c)DEPS := $(SRCS:.c=.dep)DEPS := $(addprefix $(DIR_DEPS)/, $(DEPS))include $(DEPS)all :    @echo "all"$(DIR_DEPS) :    $(MKDIR) $@$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c    @echo "Creating $@ ..."    @set -e; \    $(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@clean :    $(RM) $(DIR_DEPS)

我们来看看编译结果,是不是都将所有的 .dep 文件放入一个 deps 文件中

我们看到已经实现效果了。我们仔细看看 make 有一个警告,说 main.dep 被修改了,也就是说 main.dep 被重新创建了。那么我们来分析下,为什么一些 .dep 依赖文件会被重复创建多次呢?deps 文件夹的时间属性会因为依赖文件创建而发生改变,make 发现 deps 文件夹比对应的目标更新,于是乎就触发相应的规则重新解析和执行命令。那么我们知道了原因,此时这个方案该如何优化呢?我们可以使用 ifeq 动态决定 .dep 目标的依赖,具体 makefile 如下

.PHONY : all cleanMKDIR := mkdirRM := rm -frCC := gccDIR_DEPS := depsSRCS := $(wildcard *.c)DEPS := $(SRCS:.c=.dep)DEPS := $(addprefix $(DIR_DEPS)/, $(DEPS))all :     @echo "all"ifeq ("$(MAKECMDGOALS)", "all")-include $(DEPS)endififeq ("$(MAKECMDGOALS)", "")-include $(DEPS)endif$(DIR_DEPS) :    $(MKDIR) $@ifeq ("$(wildcard $(DIR_DEPS))", "")$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.celse$(DIR_DEPS)/%.dep : %.cendif    @echo "Creating $@ ..."    @set -e; \    $(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@    clean :    $(RM) $(DIR_DEPS)

我们再次编译看看

我们看到它还是报了这样的错误,有可能是编译器的优化造成的。思路是正确的。下来我们来看看 include 的一些鲜为人知的秘密。

A、 使用减号(-)不但关闭了 include 发出的警告,同时将关闭了错误;当错误发生时 make 将忽略这些错误! 以代码为例来进行分析说明

.PHONY : allinclude test.txtall :    @echo "this is all"test :    @echo "creating $@ ..."    @echo "other : ; @echo "this is other" " > test.txt

我们来编译看看

我们看到不但发出警告,而且报错了。下来我们来在 include 前面加上 - 试试

这样它也不报错了,直接就通过了,我们还以为 makefile 写的对着呢。这便是第一个暗黑操作。下来看看第二个暗黑操作

B、如果 include 触发规则创建了文件,之后还会发生什么?以代码为例来进行分析说明

.PHONY : allinclude test.txtall :    @echo "this is all"test.txt :    @echo "creating $@ ..."    @echo "other : ; @echo "this is other" " > test.txt

看看编译结果

我们进行直接 make 的时候,发现它输出的 this is other,并不是我们所期望的 this is all。这是为什么呢?因为在 include 的时候,直接将 test.txt 铺开在这,此时会触发规则。makefile 就变成了下面这样

.PHONY : allother :     @echo "creating $@ ..."    @echo "this is other"all :    @echo "this is all"

我们在直接 make 的时候,它默认执行的是第一个目标,因此便会输出 this is other,只有当我们 make all 的时候才会输出 this is all。这便是 include 的第二个暗黑操作了,下面继续看看第三个

C、如果 include 包含的文件存在,之后会发生什么呢?以代码为例来进行分析说明

.PHONY : all-include test.txtall :    @echo "this is all"test.txt : b.txt    @echo "this is $@"

在当前目录下创建一个 b.txt 文件,看看编译结果

我们看到同样也执行了 test.txt 的相应的规则。看看下面这个 makefile 将会输出什么

.PHONY : all-include test.txtall :     @echo "$@ : $^"    test.txt : b.txt    @echo "creating $@ ..."    @echo "all : c.txt" > test.txt

看看结果

我们看到它最后输出的 all 的依赖是 c.txt,不应该觉得奇怪吗?我们明明在 all 后面没有依赖啊。再来看看生成的 test.txt 文件,它的内容是 all : c.txt,因此输出的结果是我们意想不到的。那么我们关于 include 便有了这几条总结:1、当目标文件不存在时,以文件名查找规则并执行;2、当目标文件不存在时且查找到的规则中创建了目标文件,将创建成功的目标文件包含进当前的 makefile 中;3、当目标文件存在,将目标文件包含进当前 makefile,以目标文件名查找是否有相应规则,YES 的话则比较规则的依赖关系来决定是否执行规则的命令,NO 的话则 NULL(无操作)。4、当目标文件存在且目标名对应的规则被执行,规则中的命令更新了目标文件,make 重新包含目标文件,替换之前包含的内容。目标文件未被更新,便是 NULL(无操作)。

经过了这么多的知识点的探索,此时已经具备实现之前的想法的能力了。想要实现的具体格式如下

下面我们就根据这个来编写相关的 makefile。

func.h 源码

#ifndef FUNC_H#define FUNC_H#define HELLO "hello Makefile"#endif


func.c 源码

#include #include "func.h"void foo(){    printf("void foo() : %s\n", HELLO);}


main.c 源码

#include #include "func.h"int main(){    foo();        return 0;}


makefile 源码

.PHONY : all cleanMKDIR := mkdirRM := rm -rfCC := gccDIR_DEPS := depsDIR_OBJS := objsDIR_EXES := exesDIRS := $(DIR_DEPS) $(DIR_EXES) $(DIR_OBJS)EXE := app.outEXE := $(addprefix $(DIR_EXES)/, $(EXE))SRCS := $(wildcard *.c)OBJS := $(SRCS:.c=.o)OBJS := $(addprefix $(DIR_OBJS)/, $(OBJS))DEPS := $(SRCS:.c=.dep)DEPS := $(addprefix $(DIR_DEPS)/, $(DEPS))all : $(DIR_OBJS) $(DIR_EXES) $(EXE)ifeq ("$(MAKECMDGOALS)", "all")-include $(DEPS)endififeq ("$(MAKECMDGOALS)", "")-include $(DEPS)endif$(EXE) : $(OBJS)    $(CC) -o $@ $^    @echo "Success! Target => $@"$(DIR_OBJS)/%.o : %.c    $(CC) -o $@ -c $^$(DIRS) :    $(MKDIR) $@ifeq ("$(wildcard $(DIR_DEPS))", "")$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.celse$(DIR_DEPS)/%.dep : %.cendif    @echo "Creating $@ ..."    @set -e; \    $(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o  $@ : ,g' > $@                clean :    $(RM) $(DIRS)

编译结果如下

我们看到已经自动生成了,并且最后的结果也是我们想要的,那么我们如果在 func.h 中改变字符串,看看结果是否也会改变

我们看到在编译的时候报错了,原因是只能编译 .c 文件,.h 头文件不参与编译,这时我们便要用到预定义函数 filter 了。因此我们需要在 makefile 第37 行将它改为 $(CC) -o $@ -c $(filter %.c, $^);再来看看效果

我们看到也成功的替换掉了。这时我们基本上已经完成我们之前的想法了,那么在实际开发中,肯定需要时不时的添加头文件,我们再来在 func.h 中包含一个头文件 define.h,在 define.h 文件中定义字符串 hello-makefile,看看结果是否会跟着改变

我们看到字符串并没有发生改变,再来看看 func.dep 和 main.dep 中是否包含了 define.h

也没有包含,按理说不应该,因为我们在 func.h 中包含了 define.h,那么在 func.c 和 main.c 中肯定也就包含了 define.h。下来我们来分析下这个,当 .dep 文件生成后,如果动态的改变头文件间的依赖关系,那么 make 可能无法检测到这个改变,进而做出错误的编译决策。解决方案便是:1、将依赖文件名作为目标加入自动生成的依赖关系中;2、通过 include 加载依赖文件时判断是否执行规则;3、在规则执行时重新生成依赖关系文件;4、最后加载新的依赖文件。解决方法是在 sed 命令后加上 $@,看看编译效果,顺便我们再来加上 rebuild。

我们看到已经正确实现了,我们来看看在 deps 文件下的 .dep 文件是否包含 define.h 呢?

确实是包含了 define.h,我们再来加上 new.h,看看是否还会有效

我们看到 new.h 同样也包含进去了。通过对综合示例的学习,总结如下:1、makefile 中可以将目标的依赖拆分写到不同的地方;2、include 关键字能够触发相应规则的执行;3、如果规则的执行导致依赖更新,可能导致再次解释执行相应规则;4、依赖文件也需要依赖于源文件得到正确的编译决策;5、自动生成文件间的依赖关系能够提高 makefile 的移植性


欢迎大家一起来学习 makefile,可以加我QQ:243343083

文件 编译 目标 规则 命令 结果 目录 生成 源文件 源码 代码 字符 字符串 错误 自动生成 时候 进程 分析 输出 内容 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 南邮网络技术与应用知识点 远程服务器无法复制 小学信息技术课 网络安全 天津现代化软件开发诚信服务 计算机网络技术以后找啥工作 对数据库高级应用的认识 剪切音乐软件开发 表格里怎样查找不同的数据库 网络安全的要素 惠普磁盘阵列服务器管理 现有的计算机网络安全技术 数据库修改用户quota 游戏数据库密码一般在哪里 成华区绒霜网络技术工作室 山东省网络安全进校园 网页和数据库 知乎 从事软件开发都要交钱培训吗 注册网络安全工程师怎么报名 网络安全问题及危害 网络安全处置能力不足 严格遵守网络安全法的规定 中央网信办网络安全知识竞赛 郑州智能还款app软件开发 恢复exchange数据库 数据库集体死机先启动哪个 湖南梁音互联网科技专利产品 阴阳师进不去服务器 五子棋下载软件开发 机房服务器配置公网ip 软件开发系统需求分析模板
0