Linux快速入门之 make命令及Makefile编写 (8)

8.Makefile


首先需要先了解什么是make工具 , make命令工具是用来解释makefile文件的命令工具

当编译的项目中的文件过多时,如果使用GCC 编译一个个编译是十分繁琐的过程 ,修改某一个源文件就要全部重新编译是十分浪费时间的,因此在使用make命令 ,按照提前编写在makefile 中的编译规则,会十分方便的编译项目常见的IDE都有相对于的命令 :例如:visual stdio 系列中的 nmake 、 QtCreater 中的qmake等。

一个项目中的源文件 、头文件很多 , 需要按类型、功能分别放置在不同的目录中 。使用make命令,需要先建立一个makefile 的文件 ,makefile其中记录着项目的编译顺序 ,(先编译A ,后编译B。。。),makefile像一个Shell 脚本 可以执行操作系统的命令 ,构建项目时在那个目录中执行 make 命令 ,便会加载对于目录中的makefile文件(一个项目可以存在多个makefile文件)

makefile 带来的好处就是 ——“自动化编译”,一旦写好,只需要一个 make 命令,整个工程完全自动编译,极大的提高了软件开发的效率。

使用时直接使用make ,会自动查找当前目录中的makefile文件 ;

如果只想执行makefile中的某一个规则可以使用 make 某规则目标文件 (make c.o)

8.1 makefile语法

make命令在执行前,会先在makefile文件中查找各种规则,对各个规则进行解析后运行相应规则;

makefile文件格式:

#每个规则的语法格式:
target 1 , target2 , target3  ....: depend 1 ,depend2 ,depend3 .....
          command
          ..........

每条规则由三个部分组成分别是 :目标(target)、依赖(depend) 、命令(command)

命令(command): 当前这条规则的动作,一般情况下这个动作就是一个 shell 命令

  • 例如:通过某个命令编译文件、生成库文件、进入目录等。

  • 动作可以是多个,每个命令前必须有一个Tab缩进并且独占占一行

依赖(depend): 规则所必需的依赖条件,在规则的命令中可以使用这些依赖。

  • 例如:生成可执行文件的目标文件(*.o)可以作为依赖使用

  • 如果规则的命令中不需要任何依赖,那么规则的依赖可以为空

  • 当前规则中的依赖可以是其他规则中的某个目标,这样就形成了规则之间的嵌套
    依赖可以根据要执行的命令的实际需求,指定很多个

目标(target): 规则中的目标,这个目标和规则中的命令是对应的

  • 通过执行规则中的命令,可以生成一个和目标同名的文件
  • 规则中可以有多个命令,因此可以通过这多条命令来生成多个目标,所有目标也可以有很多个
  • 通过执行规则中的命令,可以只执行一个动作,不生成任何文件,这样的目标被称为伪目标

举例:

# 举例: 有源文件 a.c b.c c.c head.h, 需要生成可执行程序 app
################# 例1 #################
app:a.c b.c c.c
	gcc a.c b.c c.c -o app

################# 例2 #################
# 有多个目标, 多个依赖, 多个命令
app,app1:a.c b.c c.c d.c
	gcc a.c b.c -o app
	gcc c.c d.c -o app1
	
################# 例3 #################	
# 规则之间的嵌套
app:a.o b.o c.o
	gcc a.o b.o c.o -o app
# a.o 是第一条规则中的依赖
a.o:a.c
	gcc -c a.c
# b.o 是第一条规则中的依赖
b.o:b.c
	gcc -c b.c
# c.o 是第一条规则中的依赖
c.o:c.c
	gcc -c c.c

8.2 运行原理
8.2.1 规则的执行顺序

每次使用make命令 自动编译项目时,并不是每次就将源文件全部重新编译,make构建工具会判定源文件的更新时间判定源文件是否修改,来选择选择更新项目中的几个文件,或者全部更新

在调用 make 命令编译程序的时候,make 会首先找到 当前文件夹中makefile 文件中的第 1 个规则,分析并执行相关的动作。但好多时候要执行的动作(命令)中使用的依赖是不存在的,如果使用的依赖不存在,这个动作也就不会被执行。

对应的解决方案是先将需要的依赖生成出来,我们就可以在 makefile 中添加新的规则,将不存在的依赖作为这个新的规则中的目标,当这条新的规则对应的命令执行完毕,对应的目标就被生成了,同时另一条规则中需要的依赖也就存在了。

这样,makefile 中的某一条规则在需要的时候,就会被其他的规则调用,直到 makefile 中的第一条规则中的所有的依赖全部被生成,第一条规则中的命令就可以基于这些依赖生成对应的目标,make 的任务也就完成了。

案例 1

#规则之间嵌套
#规则1
app : a.o b.o c.o 
	gcc a.o b.o c.o -o app
#规则2
a.o : a.c
	gcc a.c -c 
#规则3
b.o : b.c
	gcc b.c -c
#规则4
c.o :c.c
	gcc c.c -c
	

make命令会首先查找第一个规则,当发现第一条规则中的依赖不存在时, 会暂停该规则,make会查找其他命令,查询到能生成该命令的规则执行(规则2、3、4),当第一条规则中的依赖全部存在时,执行规则一。

8.2.2文件的时间戳

由上文提及的make命令 的时间戳判定makefile文件中对于规则是否执行!

  • 通常情况下:目标时间戳 > 所有依赖的时间戳 ,执行make命令时,检查当规则和依赖满足相应规则时,那就规则中的命令就不会执行(不会重新再编译一遍源文件)

  • 当依赖文件更行之后,此时,目标文件的时间戳 < 依赖文件的时间戳 ,此时目标文件会通过规则重新生成(改了源文件 ,该源文件和目标文件会重新编译)

  • 当没有目标文件时,当然会执行规则中的命令

#规则之间嵌套
#规则1
app : a.o b.o c.o 
	gcc a.o b.o c.o -o app
#规则2
a.o : a.c
	gcc a.c -c 
#规则3
b.o : b.c
	gcc b.c -c
#规则4
c.o :c.c
	gcc c.c -c

先执行make命令,生成了目标文件app ,随后更新了 a.c的源码 ,再次执行make命令时,会先执行规则2 ,更新目标文件a.o , 然后执行规则1 更新目标文件生成app

8.2.3 自动推导

make虽然根据makefile文件中的指定规则编译源文件 ,但是我们在使用过程中往往会漏写一些规则 ,也会编译成功。是因为make 有自动推导的功能make并不完全依赖makefile文件规则

举例

#一个完整的makefile文件
myNum : add.o   sub.o   mult.o   divi.o main.o
	g++ add.o  sub.o  mult.o  divi.o main.o -o myNum

通过 make 构建项目

$  make
cc	-c add.o  add.c
cc	-c sub.o  sub.c
cc	-c mult.o  mult.c
cc	-c divi.o   divi.c
cc    -c main.o  main.c
gcc   add.o   sub.o   mult.o   divi.o main.o -o myNum

当 makefile 文件中只有一条规则,依赖中所有的 .o 文件在本地项目目录中是不存在的,并且也没有其他的规则用来生成这些依赖文件,这时候 make 会使用内部默认的构造规则先将这些依赖文件生成出来,然后在执行规则中的命令,最后生成目标文件

8.3 变量

为了写 makefile 文件 更加灵活 , 可以使用变量替代 关键字

makefile 文件变量分为: 自定义变量 、 预定义变量 、 自动变量

8.3.1 自定义变量

用户可自定义变量 ,makefile 文件中的变量没有类型 , 直接创建变量赋值

#语法
变量名 = 变量值

#举例使用
#自定义变量
targrt = myNum
obj = add.o  div.o main.o mult.o  sub.o
#取出变量值
$(obj)

#使用变量可简化 makefile写法
$(target) : $(obj)
	gcc $(obj)  -o $(target)

8.3.2 预定义变量

makefile 中有一些已经定义的变量 , 用户可以使用这些变量,不用进行定义

变量名含义默认值
AR生成静态库库文件的程序名称ar
AS汇编编译器的名称as
CCC 语言编译器的名称cc
CPPC 语言预编译器的名称$(CC) -E
CXXC++ 语言编译器的名称g++
FCFORTRAN 语言编译器的名称f77
RM删除文件程序的名称rm -f
ARFLAGS生成静态库库文件程序的选项无默认值
ASFLAGS汇编语言编译器的编译选项无默认值
CFLAGSC 语言编译器的编译选项无默认值
CPPFLAGSC 语言预编译的编译选无默认值
CXXFLAGSC++ 语言编译器的编译选项无默认值
FFLAGSFORTRAN 语言编译器的编译选项无默认
# 这是一个规则,普通写法
calc:add.o  div.o  main.o  mult.o  sub.o
        gcc  add.o  div.o  main.o  mult.o  sub.o -o calc
        
# 这是一个规则,里边使用了自定义变量和预定义变量
obj=add.o  div.o  main.o  mult.o  sub.o
target=calc
CFLAGS=-O3 # 代码优化
$(target):$(obj)
        $(CC)  $(obj) -o $(target) $(CFLAGS)

8.3.3 自动变量

makefile 中的规则语句中经常出现目标文件和依赖文件 , 自动变量用来代表这些规则中的目标文件和依赖文件,并且它们只能在规则的命令中使用

变 量含 义
$*表示目标文件的名称,不包含目标文件的扩展名
$+表示所有的依赖文件,这些依赖文件之间以空格分开,按照出现的先后为顺序,其中可能 包含重复的依赖文件
$<表示依赖项中第一个依赖文件的名称
$?依赖项中,所有比目标文件时间戳晚的依赖文件,依赖文件之间以空格分开
$@表示目标文件的名称,包含文件扩展名
$^依赖项中,所有不重复的依赖文件,这些文件之间以空格分开
# 这是一个规则,普通写法
calc:add.o  div.o  main.o  mult.o  sub.o
        gcc  add.o  div.o  main.o  mult.o  sub.o -o calc
        
# 这是一个规则,里边使用了自定义变量
# 使用自动变量, 替换相关的内容
calc:add.o  div.o  main.o  mult.o  sub.o
	gcc $^ -o $@ 			# 自动变量只能在规则的命令中使用

8.4 模式匹配
calc:add.o  div.o  main.o  mult.o  sub.o
        gcc  add.o  div.o  main.o  mult.o  sub.o -o calc
# 语法格式重复的规则, 将 .c -> .o, 使用的命令都是一样的 gcc *.c -c
add.o:add.c
        gcc add.c -c
div.o:div.c
        gcc div.c -c
main.o:main.c
        gcc main.c -c
sub.o:sub.c
        gcc sub.c -c
mult.o:mult.c
        gcc mult.c -c

第二个规则到第六个规则都是做类似的事情 ,但是文件名不同所有写出多个规则,看起来很冗余。

我们可以将一系列相同的操作整理为一个模板,类似的操作通过模板匹配使 makefile 会精简不少

#模式匹配   : 通过一个公式 ,代表若干个满足条件的规则
#依赖有一个 ,后缀为 .c 生成的目标是一个 .o 的文件 ,% 是一个通配符 ,匹配的是文件名
%.o : %.c
	gcc $<  -c

在这里插入图片描述

8.5 函数

makefile 中的函数时为了让我们快速方便得到函数的返回值;

写法: $(函数名 参数1 , 参数2 , 参数3 , …)

makefile 中使用频率最高的两函数 : wildcardpatsubst

8.5.1 wildcard 函数

此函数是为了获取指定目录下指定类型的文件名其返回值以空格分隔的、指定目录下的所有符合条件的文件名列表

#此函数只有一个参数 ,但是该参数可以以空格间隔分成若干个
$ (wildcard PATTERN...)
	参数:      指定某目录,搜索该目录下指定类型文件 ,例 : *.c

参数功能:

  • PATTERN 指的是某个或多个目录下的对应的某种类型的文件,比如当前目录下的.c 文件可以写成 *.c
    可以指定多个目录,每个路径之间使用空格间隔

返回值:

  • 得到的若干个文件的文件列表, 文件名之间使用空格间隔
  • 示例:$(wildcard *.c ./sub/*.c)

返回值格式: a.c b.c c.c d.c e.c f.c ./sub/aa.c ./sub/bb.c

# 使用举例: 分别搜索三个不同目录下的 .c 格式的源文件
src = $(wildcard /home/robin/a/*.c /home/robin/b/*.c *.c)  # *.c == ./*.c
# 返回值: 得到一个大的字符串, 里边有若干个满足条件的文件名, 文件名之间使用空格间隔
/home/robin/a/a.c /home/robin/a/b.c /home/robin/b/c.c /home/robin/b/d.c e.c f.c

8.5.2 patsubst 函数

此函数按指定的模式替换指定的文件名的后缀

#函数有三个参数 , 参数之间使用逗号间隔
$ (patsubst <pattern> , <replacement> , <text>

参数功能:
pattern: 这是一个模式字符串,需要指定出要被替换的文件名中的后缀是什么

  • 文件名和路径不需要关心,因此使用 % 表示即可 [通配符是 %]
  • 在通配符后边指定出要被替换的后缀,比如: %.c, 意味着 .c 的后缀要被替换掉

replacement: 这是一个模式字符串,指定参数 pattern 中的后缀最终要被替换为什么

  • 还是使用 % 来表示参数 pattern 中文件的路径和名字
  • 在通配符 % 后边指定出新的后缀名,比如: %.o 这表示原来的后缀被替换为 .o

text: 该参数中存储这要被替换的原始数据

返回值:
函数返回被替换过后的字符串。

src = a.cpp b.cpp c.cpp e.cpp
# 把变量 src 中的所有文件名的后缀从 .cpp 替换为 .o
obj = $(patsubst %.cpp, %.o, $(src)) 
# obj 的值为: a.o b.o c.o e.o

需要关心,因此使用 % 表示即可 [通配符是 %]

  • 在通配符后边指定出要被替换的后缀,比如: %.c, 意味着 .c 的后缀要被替换掉

replacement: 这是一个模式字符串,指定参数 pattern 中的后缀最终要被替换为什么

  • 还是使用 % 来表示参数 pattern 中文件的路径和名字
  • 在通配符 % 后边指定出新的后缀名,比如: %.o 这表示原来的后缀被替换为 .o

text: 该参数中存储这要被替换的原始数据

返回值:
函数返回被替换过后的字符串。

src = a.cpp b.cpp c.cpp e.cpp
# 把变量 src 中的所有文件名的后缀从 .cpp 替换为 .o
obj = $(patsubst %.cpp, %.o, $(src)) 
# obj 的值为: a.o b.o c.o e.o

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值