在写完我们的C语言程序之后,我们通常可以使用shell
脚本来编译和链接C语言程序的源文件,但是这种方式有一个缺点:当我们更改了几个源文件的名称之后,shell脚本就会编译失败,为了解决这种问题,我们可以使用Makefile,自动有选择的执行编译链接。
一个make文件由一系列目标项、依赖项、规则组成。
下面是makefile的格式:
目标项 | 依赖项列表 |
---|---|
target: | file1 file2 … fileN |
规则 | |
<tab> | command1 |
<tab> | command2 |
<tab> | other command |
目标项通常是需要创建或者更新的文件,但他也可能是make程序要引用的指令和标签。
依赖项是目标项的提前执行项,也就是说如果一个依赖项比目标项新(也就是说,依赖项的修改时间在目标项之后),或者目标项不存在,那么make就会执行对应的命令来生成或者更新目标项。这种机制可以确保当你修改了一部分源代码后,只有依赖于这部分源代码的目标会被重新编译,而其他不受影响的目标则不会被重新编译。这样可以大大提高编译的效率。
假定有三个文件:type.h
mysum.c
t.c
我们通过以下的方式使用makefile去编译链接这些文件:
myt: type.h t.c mysum.c #依赖项列表,目标项名称为myt,依赖项为type.h, t.c, mysum.c
gcc -o myt t.c mysum.c #链接规则
生成的可执行文件名(在例子中是myt)通常与目标项名称匹配。这样可以使得make通过目标项时间戳与依赖项时间戳之间的比较,来决定稍后是否再次构建目标项。
使用mk1作为makefile文件,并且使用make指令去运行该文件:
make -f mk1
make将会构建目标文件(.o文件),并且将命令执行显示为:
gcc -o myt t.c mysum.c
如果此时再次运行make,将会看到信息:
make: 'myt' is up to date
这将会意味着make程序不会执行,因为根据makefile文件中目标项和依赖项的时间戳的比较,目标项myt是时间戳中最后一个更改的,因此说明myt已经是根据其他依赖项编译成功的了,无需重新编译。
如果我们从依赖项列表中删除一些文件名称,即使这些文件有更改,make也不会执行rule命令。
原因仍然是时间戳的关系,因为依赖项中的文件少了=当这些文件的时间戳不能被检测
在makefile中,宏定义的符号——$被替换为他们的值。
CC = gcc #将CC替换为GCC
CFLAGS = -Wall #将CFLAGS替换为-Wall
OBJS = t.o mysum.o #将OBJS替换为t.o mysum.o
INCLUDE = -Ipath #将将INCLUDE替换为-Ipath,这里其实是告诉编译器在path路径下查找头文件的路径
myt: type.h $(OBJS) #创建了一个目标文件type.h,依赖项包括t.o mysum.o
$(CC) $(CFLAGS) -o t $(OBJS) $(INCLUDE) #gcc -Wall -o t t.o mysum.o path
执行之后出现下面的效果:
gcc -Wall -c -o solve.o solve.c
gcc -Wall -c -o myt.o myt.c
gcc -Wall -o t solve.o myt.o -Ipath
对于依赖项列表中的每个.o文件,make首先会将相应的.c文件编译成.o文件(这里你可能会有疑问,这个问题放到本段最后去讨论),但是!这只会适用于.c文件,因为所有的.c文件都依赖于.h文件,所以必须在依赖项列表中显式地包含type.h(当然,其他.h文件也是这样的),不过也有其他解决办法,如下所示
t.o: t.c type.h
gcc -c t.c
mysum.o: mysum.c type.h
gcc -c mysum.c
如果我们将上述目标项添加到makefile文件中,.c文件或者type.h中的任何更改都将触发make重新编译.c文件。
但是这种方法在工程量巨大的时候会非常繁琐,因此还有其他办法。
在上面的文章的示例中,我们写了这样的代码:
CC = gcc #将CC替换为GCC
CFLAGS = -Wall #将CFLAGS替换为-Wall
OBJS = t.o mysum.o #将OBJS替换为t.o mysum.o
INCLUDE = -Ipath #将将INCLUDE替换为-Ipath,这里其实是告诉编译器在path路径下查找头文件的路径
myt: type.h $(OBJS) #创建了一个目标文件type.h,依赖项包括t.o mysum.o
$(CC) $(CFLAGS) -o t $(OBJS) $(INCLUDE) #gcc -Wall -o t t.o mysum.o path
但是你可能会有一个问题:我的目录中没有.o文件,我也没有去写对应的规则,为什么它会知道自己去编译同名的.c文件呢?
这是因为Makefile中的规则和隐含规则。在Makefile中,指定了myt
依赖于mysum.o
和t.o
。当你运行make myt
时,make
会检查mysum.o
和t.o
是否存在,如果不存在,它会查找能够生成这些文件的规则。
在我们的Makefile中并没有明确的规则来生成solve.o
和myt.o
,但是make
有一套默认的隐含规则。其中一个隐含规则是,如果一个.o
文件不存在,make
会查找同名的.c
文件,然后使用C编译器(在你的Makefile中定义为gcc
)来编译这个.c
文件,生成对应的.o
文件。
所以,即使你的Makefile中并没有明确指定如何从.c
文件生成.o
文件,make
也能通过它的隐含规则自动完成这个过程。
什么是按名称编译目标呢?这里的名称并不是指的.c
文件或者.o
文件的名称,而是指的目标项的名称,以如下程序举例:
假设有下面三个文件:
solve.c
:
#include <stdio.h>
int solve(int a, int b) {
return (a - b) * b;
}
myso.h
:
#ifndef _MYSO_H
#define _MYSO_H
#include <stdio.h>
int solve(int a, int b);
#endif
myt
:包含main函数的文件
#include <stdio.h>
#include "myso.h"
int main() {
int a, b;
scanf("%d %d", &a, &b);
printf("%d", solve(a, b));
return 0;
}
那么关于上面三个文件的makefile如下所示:
#makefile
CC = gcc
CFLAGS = -Wall
OBJSC = solve.o myt.o
INCLUDE = -Ipath
all: myt install
myt: myso.h $(OBJSC)
$(CC) $(CFLAGS) -o myt $(OBJSC) $(INCLUDE)
solve.o: solve.c myso.h
gcc -c solve.c
myt.o: myt.c myso.h
gcc -c myt.c
install: myt
echo install myt to /usr/local/bin
sudo mv myt /usr/local/bin/
run: install
echo run executable image myt
myt || /bin/true
clean:
rm -f *.o 2> /dev/null
sudo rm -f ./usr/local/bin/myt
接下来对上面的程序中的目标项做出分析:
all
:这个目标项包括两个依赖项myt
和install
,当运行这个目标项时make all makefile
),会直接执行myt
和install
两个目标项myt
:正常编译链接指令,生成的文件名称为myt
,这里不再解释solve.o
和myt.o
:可有可无,对myt
中的依赖项的补充,其实可以由make程序自带的隐式规则自动执行install
:将生成的myt
文件转移至/usr/local/bin/
目录,这个目录中通常放置着用户安装的可执行文件,在系统变量PATH
中,该目录地址被包含,因此放在这个目录中的可执行文件可以在terminal中直接执行run
:只有install
执行之后才会执行,执行myt
指令,如果执行成功则结束,如果执行失败就执行/bin/true
确保命令的返回值为0,这样可以防止myt
运行失败导致整个makefile运行失败clean
:删除掉所有.o文件还有可执行文件,包括系统目录中(/usr/local/bin/
)对应名称的可执行文件执行上面的程序时有以下几种方式:
其功能详细读者已经能够心领神会,这里不再多做解释
makefile支持变量,在makefile中,%
是一个与sh
中的*
类似的通配符变量。
makefile还包含自动变量,这些变量在匹配规则后由make设置,自动变量规定了对目标和依赖项列表中元素的访问(也就是说可以通过自动变量在规则中来表示依赖项名称),下面是makefile的一些自动变量:
make也支持后缀规则,后缀规则并非目标,而是make程序的指令,我们通过一个例子来了解make变量和后缀规则:
DEPS = type.h
%.o: %.c $(DEPS)
$(CC) -c -o $@
在上面的程序中,%.o
代表所有.o文件
,$@
表示设置为当前目标名称,这样可以避免为单个.o
文件定义单独的目标。
接下来再举一个后缀规则的例子:
CC = gcc #编译器
CFLAGS = -I. #表示搜索路径包含当前目录
OBJS = myt.o solve.o
AS = as #汇编器
DEPS = type.h
.s.o:
$(AS) -o $< -o $@ #.s->.o的规则
.c.o:
$(CC) -c $< -o $@ #.c->.o的规则
%.o: %.c $(DEPS)
$(CC) -c -o $@ $<
myt: $(OBJS)
$(CC) $(CFLAGS) -o $@ $^
其中需要解释的有以下几点:
AS = as
:设置变量AS的值为as,表示使用as作为汇编器DEPS = type.h
设置变量DEPS的值为type.h,表示依赖的头文件然后定义了几个规则:
.s.o
:这是一个后缀规则,表示如何从.s文件生成.o文件。规则的内容是$(AS) -o $< -o $@,表示使用汇编器as将源文件(<)编译为目标文件(@)。.c.o
:这是另一个后缀规则,表示如何从.c文件生成.o文件。规则的内容是$(CC) -c $< -o $@,表示使用C编译器gcc将源文件(<)编译为目标文件(@)。%.o: %.c $(DEPS)
:这是一个模式规则,表示如何从.c文件和依赖的头文件生成.o文件。规则的内容是$(CC) -c -o $@ $<,表示使用C编译器gcc将源文件(<)编译为目标文件(@)。这些规则指定:对于每一个
.o
文件,如果他们的时间戳不同,即.s
或者.o
文件已经修改,则应当立即创建一个与之对应的.s
或者.o
文件。
最后定义了一个目标:
myt: $(OBJS)
:这是一个目标,表示如何从目标文件生成可执行文件myt
。规则的内容是$(CC) $(CFLAGS) -o $@ $^,表示使用C编译器gcc
和编译选项-I
.将所有的目标文件链接为可执行文件myt(@)。大型的C语言变成项目通常由数十到数百个源文件组成。为了便于维护,源文件通常被放到不同级别的目录中,每个目录都有自己的makefile。make很容易进入子目录以通过命令执行该目录中的本地makefile:
(cd DIR; $(MAKE)) OR cd DIR && $(MAKE)
关于这个指令,具有如下值得在意的解释:
(cd DIR; $(MAKE))
:这个命令会在一个子shell中执行。首先,它会切换到DIR目录,然后在该目录中执行Makefile文件。执行完毕后,它会返回到原来的目录。这个命令的优点是不会改变当前shell的工作目录,但是如果cd DIR命令失败,它仍然会尝试执行$(MAKE)命令。
cd DIR && $(MAKE)
:这个命令会在当前的shell中执行。它首先会切换到DIR目录,如果成功,然后在该目录中执行Makefile文件。如果cd DIR命令失败,它不会执行$(MAKE)命令。这个命令的优点是如果cd DIR命令失败,它不会尝试执行$(MAKE)命令,从而避免可能的错误。但是,它会改变当前shell的工作目录。
废话不多说,上例子:
假设你有一个项目,它的目录结构如下:
/myproject
Makefile
/src
Makefile
main.c
/lib
Makefile
mylib.c
在这个项目中,src
目录和lib
目录都有自己的Makefile文件。你可以在顶级目录的Makefile文件中使用make
命令来调用子目录中的Makefile文件,如下所示:
# /myproject/Makefile
all:
make -C src
make -C lib
在这个Makefile文件中,make -C src
命令会切换到src
目录,并调用该目录中的Makefile文件。同样,make -C lib
命令会切换到lib
目录,并调用该目录中的Makefile文件。
当然还有其他的两种写法:
# /myproject/Makefile
all:
(cd src; $(MAKE))
(cd lib; $(MAKE))
或者
# /myproject/Makefile
all:
cd src && $(MAKE)
cd lib && $(MAKE)
如果子文件中的makefile文件是其他名称怎么办呢?可以这样写:
make -C DIR -f MyMakefile
(cd DIR; $(MAKE) -f MyMakefile)
cd DIR && $(MAKE) -f MyMakefile
这三种方法都可以用来在子目录中执行具有特定名称的Makefile文件。希望这个答案能帮助你。
然后,在src
目录和lib
目录中的Makefile文件中,你可以定义如何编译该目录中的源文件,例如:
# /myproject/src/Makefile
CC = gcc
CFLAGS = -I../lib
OBJS = main.o
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
main: $(OBJS)
$(CC) $(CFLAGS) -o $@ $^
# /myproject/lib/Makefile
CC = gcc
CFLAGS = -I.
OBJS = mylib.o
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
mylib: $(OBJS)
$(CC) $(CFLAGS) -o $@ $^
在这两个Makefile文件中,我们定义了如何从.c文件生成.o文件,以及如何从.o文件生成可执行文件或库文件。
在顶级目录中运行make
命令时,make工具会自动编译和链接所有的源文件,无论它们在哪个子目录中。
以上就是本文章的所有内容啦~~~创作不易,希望多多点赞、收藏、关注!!!!
欢迎评论区提问!!有交流才会有进步!!!