文章目录
- 1.什么是makefile
- 2.为什么说makefile正好适用于编译工程呢?
- 3.makfile操练
1.什么是makefile
- makefile语法很简单,核心思想就是执行对应的指令而已
A :
cmd a
B : C D
cmd b
C :
cmd c
D :
cmd d
#注意cmd前面必须要有tab键
你可以把A,B看作是一个指令标签,也可以用于make指令的参数,如make A 就会执行对应的指令cmd a。
cmd只是一条linux shell命令而已,可以执行任何合法的命令,如gcc, rm等。
- B后面还跟着一个C和D,他们是干嘛的呢?
可以把C和D看作B命令的子命令,简单来讲也就是说执行B的时候先执行C标签的指令,再执行D标签指令。
例如上面的指令,如果执行make B,最终的执行过程是cmd c, cmd d, cmd b。
但是执行B命令的时候不一定会执行C和D命令,这个下面要讲到。由于make程序主要用于编译项目工程,还是举个编译程序的例子:
P : test.o
gcc -o test test.o
test.o : test.c
gcc -c test.c
(1)执行上面的make,我们发现第一次执行时,两条指令都被执行了。可是再次执行时,只有第一条指令被执行。这是为什么呢?
为了讲明白这个,我们引入两个概念,目标和依赖。
即命令标签为目标,子命令为依赖。
上面的makefile中目标P依赖test.o,目标test.o依赖test.c。
(2)makefile命令被执行时有这样的一个规则:如果目标比依赖新,就不用执行该目标对应的命令,当然如果目标文件不存在的话
肯定是要执行对应的命令的。
(3)当执行make时,如果没有参数,默认是执行第一个指令,即P。
由于目标P依赖test.o,他就先去执行test.o命令。刚开始的时候是没有test.o的,于是执行test.o对应的指令gcc -c test.c,
生成了test.o。
第二次执行test.o时,由于test.o已经生成,且生成时间比test.c要晚,所以不用再次执行对应的命令。
(4)我们看到这种规则是正好适用于编译工程的。当目标文件生成后,再次执行目标命令时是不会执行对应的命令的,这样有利于
节省编译的时间。
只有当依赖文件变动后,目标文件变的比依赖文件早,这时就需要且必须重新执行对应的命令了。
(5)当我们去掉依赖文件时,例如上面的test.o后面没有test.c,虽然仍然可以执行下面对应的命令,生成test.o。
如果已经生成了test.o,当我们改动test.c文件后,再也不会重新生成test.o了,最终导致我们抓狂的找为什么改动不生效的bug。
所以我们编写makefile时一定要记得加上依赖文件。
(6)以上就是makefile文件的核心,我们发现makefile文件其实就是来执行shell 命令的,只不过他的语法规则正好适用于编译工程。
或者说为了编译工程发明了make程序及其对应的makefile脚本。
2.为什么说makefile正好适用于编译工程呢?
- 因为一般编译一个工程正好需要两步:编译和链接,而且这两步有依赖关系,也就是说先编译生成中间文件,然后才能链接成可执行文件。
如下所示:
Make_Execute_Progaram : Make_Obj_File
gcc xxx.c -o target.x
Make_Obj_File:
gcc -c xxx.c -o xxx.o
从上面的例子来看,makefile好像和shell脚本没什么差别,如果真的是这样直接用shell脚本就好了。
事实上makefile有更强大的功能,他能像编程语言一样有相应的语法和变量,在编译大型工程,特别是跨平台编译时会很高效。
- makefile变量名的声明和引用:
src = http.c #变量名和值之间可以有空格
target = http.o
A : $(src)
gcc -c $(src) -o $(target)
$()符号就是引用变量名了,以上替换后变为:
src = http.c #可以有空格
target = http.o
A : http.c
gcc -c http.c -o http.o
执行make A 就会执行gcc -c http.c -o http.o,A后面的http.c是他的依赖文件,其实这里可以不写。
但是一般写上是为了检测http.c文件是否存在,更重要的是判断文件是否有更新。
3.makfile操练
- 有这样一个需求,先将文件下的所有c文件编译,然后链接成可执行文件
(1)初级写法:笨蛋写法
app: a.o b.o c.o ....
gcc -o app a.o b.o c.o
a.o : a.c
gcc -c -o a.o a.c
b.o : b.c
gcc -c -o b.o b.c
...
(2)聪明的写法:
gcc至少支持通配符,这样就可以解决问题:gcc *.c -o app。
我们先介绍makefile通配符匹配所有文件的函数--wildcard,用法如下:
查找文件夹下所有的c文件:
src = $(wildcard *c)
#然后我们可以这样:
app : $(src)
gcc -o app $(src)
wildcard就是一个makefile函数,调用时的规则如下:
$(<function> <arguments> )
或是
${<function> <arguments>}
<function>就是函数名,make支持的函数不多。
<arguments>是函数的参数,参数间以逗号“,”分 隔,而函数名和参数之间以“空格”分隔。
函数调用以“$”开头,以圆括号或花括号把函数名和参数括起。
感觉很像一个变量,是不是?
(3)为了更精准的把控整个项目,我们希望对每个c文件分开编译,然后链接,又该怎么办呢?
我们再介绍两个函数subst与patsubst,他们的作用都是字符串处理函数。
字符串替换函数1:
$(subst <from>,<to>,<text> )
名称:字符串替换函数——subst。
功能:把字串<text>中的<from>字符串替换成<to>。
返回:函数返回被替换过后的字符串。
eg:
x = $(subst .c,.o, a.c b.c )
将a.c b.c中的.c替换为.o,即x = a.o b.o
字符串替换函数2:
$(patsubst <pattern>,<replacement>,<text> )
名称:模式字符串替换函数——patsubst。
功能:
查找<text>中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式<pattern>,
如果匹配的话,则以<replacement>替换。
这里,<pattern>可以包括通配符“%”,表示任意长度的字串。
如果<replacement>中也包含“%”,那么,<replacement>中的这个“%”将是<pattern>中的那个“%”所代表的字串。
(可以用“/”来转义,以“/%”来表示真实含义的“%”字符)
返回:函数返回被替换过后的字符串。
eg:
x = $(patsubst %.c, %.o, a.c b.c )
将a.c b.c中的.c替换为.o,即x = a.o b.o
substr和patsubset的区别如下:subst不是那么的智能,连空格也会替换,
eg:
subst .c, .o, a.c 替换成了a. o,而patsubst则不会
subst .c,.o, a.cc 替换成了a.oc,而patsbust则不会替,保留a.cc
字符串替换函数3:
再介绍一种将变量替换成字符串的方法,eg:
x = a.c b.c
y = $(x:.c=.cpp) #y将变成a.cpp b.cpp注意=两边都不能有空格
y = $(x:.c=) #y将变成a b,注意=两边都不能有空格,一般用这个得到可执行文件
- 但是,是否所有的.o文件都要单独写编译指令呢?
当然也不是,这里可以用makefile的模式规则,eg:
src = $(wildcard *.c)
objs = $(patsubst %.c, %.o, $(src))
app : $(objs)
gcc $^ -o $@ #$^表示所有的依赖文件,生成最终目标文件时,肯定要依赖所有的中间文件,$@表示目标文件
%.o : %.c #模式规则,也可以写成.c.o:,不是.o.c:
gcc -c $< -o $@ #$<表示第一个依赖文件,因为在编译的时候,我们往往只编译一个c文件,其余可能都是.h文件,他们不参与编译
为什么这里可以用模式规则呢,因为gcc在生成可执行目标文件的时候,其实隐含有一个编译为中间代码的步骤,
eg:
objs = $(patsubst %.c, %.o, $(src))
app : $(objs)
gcc -o app $(objs) #这里会先调用cc -c -o x.o x.c生成中间文件
我们可以通过模式规则自定义执行命令来替换隐含的那个过程,达到想要的效果。
参考:
https://seisman.github.io/how-to-write-makefile/introduction.html#id2