为什么要使用make工具和makefile文件
我们使用gcc命令编译一个helloWorld程序是非常简单的,因为他只有一个文件。但是当多个文件编译成一个可执行文件时,就需要链接器进行目标文件链接。但是当我们的工程非常庞大,设计的源文件非常多的时候,一个个编译不仅操作繁琐,也会导致没有改变的文件重复编译,浪费了大量的时间。
make辅助编译工具,在编译之前会比较所有文件的时间戳。它会选择性地编译被修改过的文件,而不是全部编译一遍,这极大地节省了编译花费的时间。对于没有被修改的文件的目标文件,由于链接器工作是最后将目标文件链接到一起,所以不会被其他文件影响。
怎么使用make工具
调用make命令即可调用make工具
make
#调用该命令之后,会报错
#make: *** No targets specified and no makefile found. Stop.
#原因是我们没有指定make工具如何去工作,而指导工作是通过makefile文件来实现的
什么是makefile
makefile是描述整个工程编译、链接等规则的文件。makefile文件的命名必须是makefile
或者是Makefile
。
touch makefile
make
#报错:make: *** No targets. Stop.
#原因:虽然找到了makefile文件,但是是空文件,没有包含任何规则。
makefile的语法
语法格式,注意所有的行前空隙都使用tab生成的!不能用空格:
目标:依赖
命令
举例:
all:
gcc hello.c -o hello
目标:all
依赖:空
命令:gcc hello.c -o hello
上面的例子也可以写成:
all:hello.o
gcc hello.o -o hello
hello.o:hello.c
gcc -c hello.c
目标:all
和hello.o
依赖:hello.o
和hello.c
命令:gcc hello.c -o hello.o
和gcc -c hello.c
因为all
依赖hello.o
文件,所以要先执行gcc hello.c -o hello.o
得到hello.o
文件,然后才可以执行gcc hello.c -o hello
。所以输入make
命令后执行顺序与上述依赖的相互关系相同,而不是makefile文件中从上到下的顺序。
利用makefile文件执行make命令
#假设已经写好makefile文件如上面的第二个例子
make all #制定编译目标all,也可以不指定获取的编译目标,就默认获取第一个编译目标
我们会发现,执行上述makefile后,会得到.o的中间文件,我们当然可以手动地删除它们(rm命令),但是每次都要操作是非常麻烦的。我们可以在makefile文件中加入如下命令:
clean:
rm -rf *.o
然后执行命令make clean
,就可以快速删掉.o文件。这里使用的是通配符结合rm命令删除匹配的文件。
makefile的伪目标
思考如下问题:如果在当前目录下,也有一个clean文件,那么执行make clean
会发生什么?
我们可以发现,当前路径下有与makefile目标名同名的文件时,make命令会报错,除了在调用make命令前删除相关文件外,还有什么方法呢?这就会用到makefile的伪目标。
伪目标模式:.PHONY:目标名
,在可能出现同名文件的目标名的上一行添加这个语句就能避免同名文件报错了。
-PHONY:clean
clean:
rm -rf *.o
Makefile变量与变量赋值
- 变量可以在很多地方使用,如目标,依赖,或者命令
- 变量的复制可以使用
:=
、?=
、+=
、:
、=
。 - 变量的使用:通过
$(变量名)
来完成变量的使用
示例1:
使用:=
来立刻赋值:
var:=aaa
var:=$(var1)bbb
var1:=ccc
all:
echo $(var2)
使用:=
来给变量赋值,是立刻赋值,在执行var:=aaa
的同时变量值已经确定,所以最后打印出来应该是aaabbb
而非cccbbb
。
示例2:
使用=
来延迟赋值:
var=aaa
var=$(var1)bbb
var1=ccc
all:
echo $(var2)
使用=
来给变量赋值,是延迟赋值,使用他来赋值的话引用变量时会引用这个被引用的变量最终被赋予的值,所以最后打印出来应该是cccbbb
而非aaabbb
。
示例3
使用?=
来缺省赋值
var:=aaa
var?=ccc
var:=$(var1)bbb
all:
echo &(var2)
示例4
使用+=
来追加赋值
var:=aaa
var=$(var1)bbb
var1+=ccc
var1+=ddd
all:
echo $(var2)
使用?=
来给变量赋值,是缺省赋值,意思是如果该变量在此之前没有被赋值,就将其缺省为赋予的值。所以会打印aaabbb
,而非cccbbb
。
此处,相当于直接:=
进行赋值(此处用到了\
符号作多行书写):
var:=aaa\
ccc\
bbb
makefile的自动化变量
不用定义且会随着上下文变化而变化的变量。
%@
:表示所有目标$<
:表示第一个依赖文件,如果依赖模式是%
,那么它就表示一系列文件。(%
为通配符,类似于Linux上的*
)$^
表示所有依赖。
问题引入
我们通过下面的程序来体会以下这些概念:
下面有如下的文件
//hello.c文件如下
#include<stdio.h>
#include"hello.h"
void hello()
{
printf("hello world!\n);
}
//hello.h文件如下
#ifndef _HELLO_
#define _HELLO_
void hello(void);
#endif
//main.c文件如下
#include<stdio.h>
#include"hello.h"
void main(void)
{
hello();
}
makefile文件如下
hello:hello.o main.o
gcc hello.o main.o -o hello
hello.o:hello.c
gcc -c hello.c -o hello.o
main.o:main.c
gcc -c main.c -o main.o
clean:
rm -rf *.o hello
通过make命令,我们可以编译得到可执行文件,但是如果我们要另外追加一些include的文件,那么我们就要在makefile文件中重新修改依赖和下方的命令,这在非常多文件的时候是非常麻烦的。
使用变量来优化makefile
变量来替代文件名
尝试如下的使用方法:
var:= hello.o main.o
hello:$(var)
gcc $(var) -o hello
hello.o:hello.c
gcc -c hello.c -o hello.o
main.o:main.c
gcc -c main.c -o main.o
可以发现,以后如果需要再增加目标文件依赖,可以直接修改var
变量而不是修改两次下方的语句,这减少了我们的工作量。
自动化变量$<
和$@
来替代同名依赖和目标
观察下述makefile文件:
var:= hello.o main.o
hello:$(var)
gcc $(var) -o hello
%.o:%.c
gcc -c $< -o $@
可以发现我们利用自动化变量,将两组分别同名的依赖和目标整合到了一个地方。
自动化变量$^
来替代所有的依赖
观察下述makefile文件:
var:= hello.o main.o
hello:$(var)
gcc $^ -o hello
%.o:%.c
gcc -c $< -o $@
有个疑问:直接使用$(var)
也差不多啊,为啥要引入$^
?
makefile的函数
wildcard函数
wildcard:通配符的意思
格式:$(wildcard 包含通配符的字符串)
功能:展开展开指定的目录
举例:
在/home/test
的目录下有一个a.c
的C源文件和一个test
的文件夹,在/home/test/test
文件夹下有一个b.c
文件。我们在/home/test
目录下创建makefile文件如下:
var:=$(wildcard ./*.c ./test/*.c)
all:
@echo $(var)
此处shell命令前加上@
后,使用make命令就不会再终端显示@
之后的shell命令。此处就不会显示echo
这个命令。我们执行这个makefile文件得到结果:
可以发现:
echo
命令没有被显示出来wildcard
关键字之后的通配符路径被展开成了匹配到的c源文件。
notdir函数
格式:$(notdir 路径字符串)
功能:提取路径字符串中的文件名
假设当前路径下只有test.c
的C源文件
dir:=$(wildcard ./*.c)
var2:=$(notdir $(dir))
var:=$(notdir ./test.o)
chartest:
echo $(dir)
echo $(var)
echo $(var2)
执行上述文件,会输出如下结果:
可以发现:var2
变量提取了通配符结果./test.c
中的文件名部分test.c
。
dir函数
格式:$(dir 文件名字符串)$
功能:提取文件名字符串中的路径名(也就是最后一个 /
之前的所有字符)
dir:=$(wildcard ./*.c)
#var2:=$(notdir $(dir))
var:=$(notdir ./test.o)
var3:=$(dir $(dir))
chartest:
echo $(dir)
echo $(var)
#echo $(var2)
echo $(var3)
执行上述文件,会输出如下结果:
可以发现:
#
后的makefile语句没有被执行,其实#
是makefile的注释符号echo
语句之前的#
并没有起作用,所以可以知道:缩进部分的语句会被完全当作shell语句处理,而不是makefile语句。var3
变量提取了通配符结果./test.c
中的路径部分./
。
patsubst函数
格式:$(patsubst 源字符,目标字符,字符列表
功能:替换字符列表中的源字符到目标字符
var2:=$(notdir $(dir))
var:=$(notdir ./test.o)
#var3:=$(dir $(dir))
var4:=$(patsubst %.c,%.o,$(var2))
chartest:
echo $(dir)
echo $(var)
echo $(var2)
@echo $(var3)
echo $(var4)
执行上述文件,会输出如下结果:
可以发现:var2
中的.c部分被替换成.o并存入到var4
中(var2没有被改变)。
使用$(var:源字符串=目标字符串)
来替换字符
格式:$(var:a=b)
功能:把字符串中的源字符串替换成目标字符串
dir:=$(wildcard ./*.c)
var2:=$(notdir $(dir))
var:=$(notdir ./test.o)
#var3:=$(dir $(dir))
#var4:=$(patsubst %.c,%.o,$(var2))
var5:=$(dir:%.c=%.o)
chartest:
echo $(dir)
echo $(var)
echo $(var2)
@echo $(var3)
@echo $(var4)
echo $(var5)
执行上述文件,会输出如下结果:
可以发现:变量dir
中的.c被替换成了.o
foreach函数
格式:$(foreach <var>,<list>,<text>)
功能:把参数<list>
中的单词逐一取出放到参数<var>
指定的变量中,然后在执行<text>
所包含的表达式。每一次<text>
会返回一个字符串。
dir:=$(wildcard ./*.c)
#var2:=$(notdir $(dir))
var:=$(notdir ./test.o)
var3:=$(dir $(dir))
#var4:=$(patsubst %.c,%.o,$(var2))
#var5:=$(dir:%.c=%.o)
var6:=$(foreach n,$(var3),$(wildcard $(n)*.c))
chartest:
echo $(dir)
echo $(var)
@echo $(var2)
echo $(var3)
@echo $(var4)
@echo $(var5)
echo $(var6)
执行上述文件,会输出如下结果:
可以发现:
- foreach执行的过程为从
var3
中取出一个值放入变量n中,执行语句$(wildcard $(n)*.c)
。然后再取出下一个值,执行相应语句直到穷尽var3
中的值。 - 可以发现,dir函数不会去除重复路径,如
./test2.c ./test.c
在dir函数处理后变成./ ./
,所以在此后的var6
中,会执行两次,每一次都会因为*.c
的通配符,匹配到两个test2.c test.c
文件名。故而总共输入两组重复的文件名。
makefile相关的资料手册
限于篇幅原因,本文只列出最基本的一些用法。读者可以在此基础上自行查找一些相关的资料手册进行进一步的学习。此处推荐一本《跟我一起写makefile》,写得很不错。
设置vim编辑器的tab缩进宽度
#先打开vim配置文件
vi /etc/vim/vimrc #etc文件夹放配置文件,rc结尾的文件一般是配置文件
再在最后一行加入语句(这里的配置原理同PATH的.bashrc文件原理类似):
set tabstop=4
这里我们设置tab缩进宽度为4。同样地,也可以加入语句set number
,就可以在打开的时候就设置显示每一行的行号了。set shiftwidth=4
表示换行的自动缩进长度设为4个空格。