c语言 函数依赖关系生成,自动生成依赖关系(十)

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

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

makefile 源码OBJS := func.o main.o

hello.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;

}

我们来看看编译结果

78efb55d173ef1426528831f67359c5c.png

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

10194585bd9fd5f7ad565870532176f2.png

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

c16d1a13cf247ec37d80db3aabf45c2d.png

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

89d0ec09bc4bfb39eba26c4da0ff4fc0.png

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

7eab8b759acae381119896dbe6600832.png

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

453792a5b9b1b30907aab94b80006c9b.png

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

c37e44ad260974617d851d280081bc4c.png

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

test :

gcc -M main.c

编译结果如下

2b5f47bf0c69ec08f8f7af862b76ff4a.png

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

547fdf00a6a7a9c89a2073af1f170e30.png

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

4957dae648d418b2587cffd8a6647621.png

我们来做个实验.PHONY : a b c

test : a b

test : b c

test :

@echo "$^"

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

9fe3a1ac930e98fe4e27f64d41a0493f.png

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

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

7c899ee9065fb29ad348b5cd5e45e08c.png

下来还是以代码为例来进行说明.PHONY : test

include test.txt

all :

@echo "this is $@"

test.txt :

@echo "test.txt"

@touch test.txt

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

c6eefb15c262f7829cb51d3b966167d5.png

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

all :

mkdir test

cd test

mkdir subtest

我们来看看编译结果

e524d115426b5e538c6fb82de248174c.png

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

all :

set -e; \

mkdir test; \

cd test; \

mkdir subtest

看看编译结果

4182ea19879c185c4a83c4bc9658c86f.png

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

MKDIR := mkdir

RM := rm -fr

CC := gcc

SRCS := $(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)

我们来看看编译结果

ead4e20166c3392408cf31b34c82147a.png

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

05cc00075388e66a4a94688e72f30a80.png

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

我们下来看看初步的代码设计是怎样的.PHONY : all clean

MKDIR := mkdir

RM := rm -rf

CC := gcc

DIR_DEPS := deps

SRCS := $(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 文件中

ba35ccfba0a66ba94d69792f478db6e9.png

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

MKDIR := mkdir

RM := rm -fr

CC := gcc

DIR_DEPS := deps

SRCS := $(wildcard *.c)

DEPS := $(SRCS:.c=.dep)

DEPS := $(addprefix $(DIR_DEPS)/, $(DEPS))

all :

@echo "all"

ifeq ("$(MAKECMDGOALS)", "all")

-include $(DEPS)

endif

ifeq ("$(MAKECMDGOALS)", "")

-include $(DEPS)

endif

$(DIR_DEPS) :

$(MKDIR) $@

ifeq ("$(wildcard $(DIR_DEPS))", "")

$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c

else

$(DIR_DEPS)/%.dep : %.c

endif

@echo "Creating $@ ..."

@set -e; \

$(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@

clean :

$(RM) $(DIR_DEPS)

我们再次编译看看

64f1b73a58ca25918c86a39185b46c84.png

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

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

include test.txt

all :

@echo "this is all"

test :

@echo "creating $@ ..."

@echo "other : ; @echo "this is other" " > test.txt

我们来编译看看

c182d6e771ec17c5fffe3ee83435e3fd.png

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

c5d2a2a64051e5e6dfe9a8b7d96333dc.png

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

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

include test.txt

all :

@echo "this is all"

test.txt :

@echo "creating $@ ..."

@echo "other : ; @echo "this is other" " > test.txt

看看编译结果

91007b9b66b28d5365a75081fbf5c33b.png

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

other :

@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.txt

all :

@echo "this is all"

test.txt : b.txt

@echo "this is $@"

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

177d16c4a7e0a7ca7e17e70138de537b.png

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

-include test.txt

all :

@echo "$@ : $^"

test.txt : b.txt

@echo "creating $@ ..."

@echo "all : c.txt" > test.txt

看看结果

0821c37443b90b60e2195c2364ba8976.png

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

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

51937690d5a67606413a856e4e2662dd.png

下面我们就根据这个来编写相关的 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 clean

MKDIR := mkdir

RM := rm -rf

CC := gcc

DIR_DEPS := deps

DIR_OBJS := objs

DIR_EXES := exes

DIRS := $(DIR_DEPS) $(DIR_EXES) $(DIR_OBJS)

EXE := app.out

EXE := $(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)

endif

ifeq ("$(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) %.c

else

$(DIR_DEPS)/%.dep : %.c

endif

@echo "Creating $@ ..."

@set -e; \

$(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o  $@ : ,g' > $@

clean :

$(RM) $(DIRS)

编译结果如下

a5c16d27c01993f49c917f06e3aef53d.png

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

ae87f598ba2770b3f1784e212289b889.png

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

1455b2a24d652983ab66211f6d818029.png

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

e31af538c45faca0c905fc16b97a55be.png

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

ee94476ce2bf4455d9fafb88656b5c17.png

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

942f1b6588bb78eed4ffa40d6d07ac46.png

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

aaf41ebfa598a46cecdf0406bf32e606.png

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

990fd72a890c8e4675e0af63d06df7b1.png

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值