https://edu.csdn.net/course/detail/39264
通过前面知识点对Makefile的语法还有框架做出了简要介绍后,通过实战一步一步写一个简单的计算器项目。实现通过makefile来管理编译代码,包括生成静态库和动态库,多目录管理文件等等一系列架构组织,完成一个通用的Makefile模板。
首先先来构建一个加减乘除的一个简单的计算器项目工程。新建一个目录叫operation,在此新建目录下创建add.c add.h operation.c为加法模块,运算功能。 定义一个计算器计算的主文件operation.c,在这个主文件中会调用加法模块。
代码如下:
add.c
#include
void add_init()
{
printf("add operation init\n");
}
add.h
#ifndef _ADD_H
#define _ADD_H
void add init();
#endif
operation.c
#include
#include "add.h"
#include "sub.h"
int main()
{
printf("operation beginning\n");
add_init();
sub_init();
return 0;
}
那么,makefile该怎么写呢,可以这样写:
Makefile:
PHONY:al clean
TARGET = test
0BJ = add.o operation.o
all:$(TARGET)
$(TARGET):$(OBJ)
gcc -o s@ $^
%.o:%.c
gcc -o s@ -c S^
clean:
rm -rf $(OBJ) S(TARGET)
TARGET:计算器应用程序的可执行文件test
OBJ:包含需要生成以创建可执行文件的所有目标文件的名称。
%.c: 表示任何以 .c 为后缀的源文件。
%.o: 表示任何以 .o 为后缀的目标文件。
$@和$^是我们之前讲过的自动变量,$@表示目标,$^表示所有目标依赖
clean:伪目标用于删除所有生成的目标文件和可执行文件。
%.o:%.c
gcc -o s@ -c S^
这个规则是为了生成从源文件到目标文件的编译规则。
先定义了一个伪目标all,然后让all去依赖可执行程序test。即使没有all,我也能通过make直接生成test. 为什么要多此一举呢?之前在讲依赖树的时候我们注意到,树顶一般只有一个,但是如果我想直接make生成多个可执行程序时比如test1,test2,如果没有一个all作为树顶。我就无法直接make生成两个程序,而是只能make test1, make test2,这其实非常破坏依赖树的结构的,所以我们约定伪目标all是第一个目标,它通常依赖于构建整个项目所需的所有子目标,比如对于上面提到的test1,test2,我就让它们依赖于all,这样我直接make就能用时构建出这两个程序。
当执行 "test" 目标时,它首先检查目标文件是否存在。如果目标文件不存在,它使用 Makefile 中定义的规则编译相应的源文件。一旦所有目标文件生成,目标将它们链接起来创建可执行文件 "test"。
当我们再添加减法模块时,将operation.c修改:
sub.c
#include
void sub init()
{
printf("sub operation init\n");
}
sub.h
#ifndef _SUB_H
#define _SUB_H
void sub_init();
#endif
operation.c
#include
#include "add.h"
#include "sub.h"
int main()
{
printf("operation beginning\n");
add_init();
sub_init();
return 0;
}
此时,makefile只需要添加sub.o目标文件在变量OBJ上即可,添加好之后,我们直接make,可以看见sub.o已经生成了。运行一下test,我们也可以看见sub减法运算模块也已经正常使用了。
Makefile:
PHONY:all clean
TARGET = test
oBJ = add.o sub.o operation.o
all:$(TARGET)
$(TARGET):$(OBJ)
gcc -o $@ $^
%.o:%.c
gcc -o $@ -c $^
clean:
rm -rf $(OBJ) $(TARGET)
当继续添加其他模块的时候,是否直接再在OBJ变量上添加目标文件,那如果一个工程里面有几百个甚至更多.c和.h文件时,这样写就非常麻烦,此时为了使makefile更富有通用性,更加简便,因此我们可以使用前面课程中makefile函数中的讲解过的一个函数叫做获取匹配模式文件名函数----wildcard,这个函数在这里可以发挥特别的作用。
因此我们可以通过这个函数升级前面写的makefile ,定义一个变量:
SRCS = $(wildcard *c)
SRCS就是当前目录下所有的.c文件
OBJ = $(SRCS:.c=.o)
把所有的.c替换成.o
则OBJ就变成了当前目录下的所有.o文件,此时,OBJ变量的含义其实跟上述内容所实现的含义一样。这样优化后,不管添加多少个.c文件,这个makefile都是适用的,不需要进行修改。
Makefile如下:
PHONY:all clean
TARGET = test
SRCS = $(wildcard *.c)
0BJ = $(SRCS:.c=.o)
all:$(TARGET)
$(TARGET):$(OBJ)
gcc -o $@ $^
%.o:%.c
gcc -o $@ -c $^
clean:
rm -rf $(OBJ) $(TARGET)
可见此时执行结果是一样的,在用函数设置变量时,我们也可以打印一下看看,这两个变量是否跟我们预想的一样,只需要添加打印语句:
@echo "SRCS = $(SRCS)
@echo "OBJ = $(OBJ)
以上规则中,每个.o文件只依赖对应的.c文件,并没有依赖对应的.h文件,那会造成什么样的现象呢?
我们举个例子说说:
修改sub.h头文件,宏定义一个变量math为8,例如:
sub.h
#ifndef _SUB_H
#define _SUB_H
void sub_init();
#define math 8
#endif
在sub.c的函数中打印math的值,编辑sub.c,包含sub.h ,增加打印语句
sub.c
#include
#include "sub.h"
void sub init()
{
printf("sub operation init\n");
printf("number=%d\n",math);
}
修改完后,make执行一下,此时math=8,这个时候改一下这个宏定义的值,将math改为9,然后再进行编译make,出现如下
make: Nothing to be done for all'
说明执行make命令时,所有需要的文件已经是最新时就会出现这个消息,这意味着刚才修改了sub.h文件并没有成功重新编译。
可以编译可执行程序test查看:这时发现了,明明已经修改了将math的值改为9,此时math怎么依然值为8。
这是因为在上面的规则中,每个.o文件只依赖了对应的.c文件,并没有依赖对应的.h文件。 所以当.h文件修改了之后,不会再去重新编译所有依赖该.h文件的目标文件,需要在Makefile中添加目标对应.h文件的依赖关系!
因此我们就需要添加对头文件的依赖,此时sub.o就得将它设置为依赖于sub.h和sub.c,为以下格式:
sub.o:sub.c sub.h
这个规则可不用写生成命令,因为前面讲过多规则目标,当多个规则是同一个目标时,会将目标依赖进行合并成此目标的依赖文件列表,并且只使用最后一个规则中定义的命令即可。 则此时其实内部是这样的:
sub.o:sub.c usb.h
gcc -o sub.o -c usb.c usb.h
修改后如下:
.PHONY:all clean
TARGET = test
SRCS = $(wildcard *.c)
0BJ = $(SRCS:.c=.o)
all:$(TARGET)
$(TARGET):$(OBJ)
gcc -o $@ $^
sub.o:sub.c sub.h
%.o:%.c
gcc -o $@ -c $^
clean:
rm -rf $(OBJ) $(TARGET)
加入sub.h的依赖关系后执行make
gcc: fatal error: cannotspecify -o with s or -E with multiple files compilation terminated.
可见执行的命令与预想一致,但是又出现这个错误是怎么回事 ?错误显示makefile的语法错误,使用选项-c时不能与多个文件一起使用,我们得将.c文件过滤出来,这时我们可以使用filter过滤函数,让其更加简便。
此时如果我们要保留下.c文件,可以这样写
$(filter %.c,$^)
编译可执行程序如下:此时math已经为9,可以修改再进行实验!
其实,在讲基础部分讲过,在makefile中,可以自动生成头文件依赖关系,使用gcc -MM xx.c 命令会自动生成此.c文件头文件依赖关系:
gcc -MM sub.c>sub.d
并将依赖关系重定位到.d文件中,执行命令语句为 : gcc -MM XX.c>XX.d ,则.d文件中就是其依赖关系。
生成sub.d文件后,于是我们可以将makefile再次改写一下:
.PHONY:all clean
TARGET = test
SRCS = $(wildcard *.c)
0BJ = $(SRCS:.c=.o)
all:$(TARGET)
$(TARGET):$(OBJ)
gcc -o $@ $^
include sub.d
%.o:%.c
gcc -o $@ -c $(filter %.c,$^)
clean:
rm -rf $(OBJ) $(TARGET)
同样我们也能达到前面的效果,但是一个一个手动生成.d然后include太麻烦了 ,可以再次修改makefile,实现用makefile就可以直接生成每个.c的.d文件。 添加一条规则:
%d:%c
gcc -MM $^>$@
然后make执行一下发现不能生成.d文件,这是为什么呢?这是因为我们呢make执行的实际上是make all,但是.d文件并没有在all的依赖树中。所以我们要生成.d文件就需要让all依赖于.d文件。
因此我们可以定义一个变量DEP,将当前目录下的.c文件都转换为.d文件。然后让all去依赖于这个变量。如果直接include,.d文件可能还没有创建,就会有一个警告。
所以我们可以增加一个判断语句如果.d文件存在,再将其包含进来: 别忘了在clean下面要加上DEP。
.PHONY:all clean
TARGET = test
SRCS = $(wildcard *.c)
0BJ = $(SRCS:.c=.o)
DEP = $(SRCS:.c=.d)
all:$(TARGET) $(DEP)
ifneq ("$(wildcard $(DEP))","")
include $(DEP)
endif
$(TARGET):$(OBJ)
gcc -o $@ $^
%.o:%.c
gcc -o $@ -c $(filter %.c,$^)
%d:%c
gcc -MM $^>$@
clean:
rm -rf $(OBJ) $(TARGET) $(DEP)
此时make一下,发现没有什么问题了。
将不同的模块都放入不同的目录下,这样便于我们更好的去管理我们的代码架构。这里我们可以看到我们已经有一个主程序operation.c,还有两个功能模块,分别为加法和减法模块。因此新建两个目录add和sub,将add.c和add.h放入add目录中,sub也同样如此。再新建一个目录calculator用来存放主程序operation.c文件。放好之后,我们先编译看看,例如进入到add目录下,如果想编译这个模块,我们还得需要一个makefile文件,先把上节的Makefile放到add目录下:
所以此时make发现会有错误。makefile需要修改一下,在add这个目录下没有main函数,所以要把TARGET的定义去掉。 TARGET为空的话下面的关于它的规则就不会生效,那.o文件就不会在依赖关系树里面,所以我们要在all后面加上OBJ,同时要调整一下位置,把OBJ和DEP放到前面,因为先生成依赖文件再生成依赖目标才是合理的。
.PHONY:all clean
TARGET =
SRCS = $(wildcard *.c)
0BJ = $(SRCS:.c