Makefile基础

Makefile简介

什么是makefile?

如果以前一直使用 IDE来编写 C/C++ 语言的话肯定没有听说过 Makefile 这个东西,因为这些 IDE对makefile文件进行了封装,提供给的是已经经过封装后的图形界面,我们在 IDE 中添加要编译的 C/C++文件,然后点击按钮就完成了编译。而在 Linux 下用的最多的是 GCC 编译器,这是个没有 UI的编译器,因此 Makefile 就需要我们自己来编写了。

Makefile:是描述哪些文件需要编译、哪些需要重新编译的文件; 而make就是工程编译的工具,相当于IDE中图形界面的按钮。

为什么要引入Makefile?

使用 GCC 的命令行进行程序编译在单个文件下是比较方便的,但当工程中的文件有几十、上百甚至上万个的时候,用终端输入 GCC 命令的方法显然是不现实的 。这种情况下我们需要借助项目构造工具 make 帮助我们完成这个艰巨的任务。

Makefile语法

Makefile规则

Makefile 的框架是由规则构成的。make 命令执行时先在 Makefile 文件中查找各种规则,对各种规则进行解析后运行规则。规则的基本格式为:

Makefile规则的语法格式:

1
2
3
4
target ... : depend ...
command1
command2
...

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

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

②、依赖(depend): 规则所必需的依赖条件,在规则的命令中可以使用这些依赖。
例如:生成可执行文件的目标文件(*.o)可以作为依赖使用
如果规则的命令中不需要任何依赖,那么规则的依赖可以为空
当前规则中的依赖可以是其他规则中的某个目标,这样就形成了规则之间的嵌套
依赖可以根据要执行的命令的实际需求,指定很多个

③、命令(command): 当前这条规则的动作,一般情况下这个动作就是一个 shell 命令
例如:通过某个命令编译文件、生成库文件、进入目录等。
动作可以是多个,命令列表中的每条命令必须以 TAB 键开始,不能使用空格! 并且独占占一行。

​ 如果命令太长,可以使用反斜框(‘\’)作为换行符。

例:通过键盘输入两个数字,计算出它们的乘积;

input.c

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include "input.h"

void input_int(int *a, int *b)
{
printf("input two num:");
scanf("%d %d", a, b);
printf("\r\n");
}

input.h

1
2
3
4
5
#ifndef _INPUT_H
#define _INPUT_H

void input_int(int *a, int *b);
#endif

multiply .c

1
2
3
4
int multiply(int a, int b)
{
return (a + b);
}

multiply .h

1
2
3
4
5
#ifndef _CALCU_H
#define _CALCU_H

int multiply (int a, int b);
#endif

main.c

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include "input.h"
#include "multiply .h"

int main(int argc, char *argv[])
{
int a, b, num;
input_int(&a, &b);
num = multiply (a, b);
printf("%d + %d = %d\r\n", a, b, num);
}

工程中有 main.c、 input.c 和 multiply .c 这三个 C 文件和 input.h、 multiply .h 这两个头文件

例1:Makefile 编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 规则1 main是通过编译 main.o input.o multiply.o
main: main.o input.o multiply.o
gcc -o main main.o input.o multiply.o
# 规则2 main.o是通过编译main.c生成的
main.o: main.c
gcc -c main.c

# 规则3 input.o是通过编译input.c生成的
input.o: input.c
gcc -c input.c

# 规则4 multiply.o是通过编译multiply.c生成的
multiply.o: multiply.c
gcc -c multiply.c

# 规则5 通过make clean 删除所有的.o和main文件
clean:
rm *.o
rm main

规则的执行顺序:

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

解决方案:将不存在的依赖作为这个新的规则中的目标,当这条新的规则对应的命令执行完毕,对应的目标就被生成了,同时另一条规则中需要的依赖也就存在了。这样,makefile 中的某一条规则在需要的时候,就会被其他的规则调用,直到 makefile 中的第一条规则中的所有的依赖全部被生成,第一条规则中的命令就可以基于这些依赖生成对应的目标,make 的任务也就完成了。

如上例中:规则1中main.o 、input.o、 multiply.o先没有存在,那么就先执行规则2、规则3、规则4,生成main.o 、input.o、 multiply.o后再执行规则1

Makefile变量

跟C/C++语言一样 Makefile 也支持变量 ;但不像 C/C++语言中的变量有 int、char等各种类型, Makefile 中的变量都是字符串。

变量在声明时需要给予初值,而在使用时,需要给在变量名前加上“$”符号,但最好用小括号“()”或是大号“{}”把变量给包括起来。如果你要使用真实的“$”字符,那么你需要用“$$”来表示。

1
2
3
objects = main.o input.o multiply.o
main: $(objects)
gcc -o main $(objects)

Makefile变量的赋值符还有其它两个“:=”和“?=”

1、赋值符“=” : 使用“=”在给变量的赋值的时候,不一定要用已经定义好的值,也可以使用后面定义的值;

2、赋值符“:=” :赋值符“:=”不会使用后面定义的变量,只能使用前面已经定义好的;相当于在C/C++ 变量前添加了const;

3、赋值符“?=” :如果变量没有被赋值,那就使用=号后的值,如果已经定义,那就不改变;

4、变量追加“+=” :Makefile 中的变量是字符串,有时候我们需要给前面已经定义好的变量添加一些字符串进去,此时就要使用到符号“+=”

3.2 预定义变量

在 Makefile 中有一些已经定义的变量,用户可以直接使用这些变量,不用进行定义。在进行编译的时候,某些条件下 Makefile 会使用这些预定义变量的值进行编译。这些预定义变量的名字一般都是大写的,经常采用的预定义变量如下表所示:

Makefile模式

模式规则中,至少在规则的目标定定义中要包涵“%”,否则就是一般规则,目标中的“%”表示对文件名的配,“%”表示长度任意的非空字符串,比如“%.c”就是所有的以.c 结尾的文件,类似与通配符, a.%.c 就表示以 a.开头,以.c 结束的所有文件。

Makefile自动化变量

Makefile 中的变量除了用户自定义变量和预定义变量外,还有一类自动变量。Makefile 中的规则语句中经常会出现目标文件和依赖文件,自动变量用来代表这些规则中的目标文件和依赖文件,并且它们只能在规则的命令中使用。

常用的三种: $@、 $<和$^

使用Makefile模式规则和自动化变量对例1中Makefile文件进行精简:

1
2
3
4
5
6
7
8
9
10
11
12
13
#规则1
objects = main.o input.o calcu.o
main: $(objects)
gcc -o main $(objects)

#规则2
%.o : %.c
gcc -c $< #使用自动化变量:$<,将所有的.c文件编译成.o文件

#规则3
clean:
rm *.o
rm main

Makefile伪目标

Makefile 有一种特殊的目标——伪目标,一般的目标名都是要生成的文件,而伪目标不代表真正的目标名,在执行 make 命令的时候通过指定这个伪目标来执行其所在规则的定义的命令。

使用伪目标的目的:主要是为了避免 Makefile 中定义的只执行命令的目标和工作目录下的实际文件出现名字冲突,有时候我们需要编写一个规则用来执行一些命令,但是这个规则不是用来创建文件的,如:

1
2
3
clean:
rm *.o
rm main

规则中并没有创建文件 clean 的命令,因此工作目录下永远都不会存在文件 clean,当我们输入“make clean”以后,后面的“rm *.o”和“rm main”总是会执行。但当出错在工作目录下创建一个名为“clean”的文件时,当执行“make clean”的时候,规则因为没有依赖文件,所以目标被认为是最新的,因此后面的 rm 命令也就不会执行,预先设想的清理工程的功能也就无法完成。为了避免这个问题,可以将 clean 声明为伪目标,声明方式如下:

1
.PHONY : clean  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#规则1
objects = main.o input.o calcu.o
main: $(objects)
gcc -o main $(objects)

#伪命令
.PHONY : clean

#规则2
%.o : %.c
gcc -c $< #使用自动化变量:$<,将所有的.c文件编译成.o文件

#规则3
clean:
rm *.o
rm main

clean 为伪目标,声明 clean 为伪目标以后不管当前目录下是否存在名为“clean”的文件,输入“make clean”的话规则后面的 rm 命令都会执行。

Makefile的条件判断

Makefile 也支持条件判断,语法有两种:
1
2
3
<条件关键字>
<条件为真时执行的语句>
endif

以及:

1
2
3
4
5
<条件关键字>
<条件为真时执行的语句>
else
<条件为假时执行的语句>
endif
条件关键字有 4 个:

ifeq、 ifneq、 ifdef 和 ifndef,这四个关键字其实分为两对、 ifeq 与ifneq、 ifdef 与 ifndef。

①、ifeq 和 ifneq

ifeq 用来判断是否相等, ifneq 就是判断是否不相等, ifeq 用法如下:

1
2
3
4
5
ifeq (<参数 1>, <参数 2>)
ifeq ‘<参数 1 >’ ,‘ <参数 2>’
ifeq “<参数 1>” , “<参数 2>”
ifeq “<参数 1>” , ‘<参数 2>’
ifeq ‘<参数 1>’ , “<参数 2>”

用来比较“参数 1”和“参数 2”是否相同,如果相同则为真,“参数 1”和“参数 2”可以为函数返回值。 ifneq 的用法类似,只不过 ifneq 是用来了比较“参数 1”和“参数 2”是否不相等,如果不相等的话就为真。

②、ifdef 和 ifndef

1
ifdef <变量名>

如果“变量名”的值非空,那么表示表达式为真,否则表达式为假。“变量名”同样可以是一个函数的返回值。 ifndef 用法类似,但是含义用户 ifdef 相反。

Makefile函数

Makefile 中的函数是已经定义好的,可以直接使用,但不支持自定义函数。

函数的用法:
1
$(函数名 参数集合)

或者

1
${函数名 参数集合}  

调用函数和调用普通变量一样,使用符号“$”来标识。参数集合是函数的多个参数,参数之间以逗号“,”隔开,函数名和参数之间以“空格”分隔开,函数的调用以“$”开头。

常用的Makefile函数:

①、函数 subst

函数 subst 用来完成字符串替换,调用形式如下:

1
$(subst <from>,<to>,<text>)

此函数的功能是将字符串中的内容替换为,函数返回被替换以后的字符串;

②、、函数 patsubst

函数 patsubst 用来完成模式字符串替换,使用方法如下:

1
$(patsubst <pattern>,<replacement>,<text>)

此函数查找字符串中的单词是否符合模式,如果匹配就用来替换掉,可以使用包括通配符“%”,表示任意长度的字符串,函数返回值就是替换后的字符串。如果中也包涵“%”,那么中的“%”将是中的那个“%”所代表的字符串 。

1
$(patsubst %.c,%.o,a.c b.c c.c)

将字符串“a.c b.c c.c”中的所有符合“%.c”的字符串,替换为“%.o”,替换完成以后的字符串为“a.o b.o c.o”。

③、、函数 dir
函数 dir 用来获取目录,使用方法如下:

1
$(dir <names…>)  

④、、函数 notdir
函数 notdir 看名字就是知道去除文件中的目录部分,也就是提取文件名,用法如下:

1
$(notdir <names…>)

此函数用与从文件名序列中提取出文件名非目录部分,比如:

1
$(notdir </src/a.c>)

提取文件“/src/a.c”中的非目录部分,也就是文件名“a.c”。

⑤、、函数 foreach
foreach 函数用来完成循环,用法如下:

1
$(foreach <var>, <list>,<text>)

此函数的意思就是把参数中的单词逐一取出来放到参数中,然后再执行
包含的表达式。每次都会返回一个字符串,循环的过程中, 中所包含的每个字符串
会以空格隔开,最后当整个循环结束时, 所返回的每个字符串所组成的整个字符串将会是
函数 foreach 函数的返回值。

⑥、函数 wildcard

通配符“%”只能用在规则中,只有在规则中它才会展开,如果在变量定义和函数使用时,
通配符不会自动展开,这个时候就要用到函数 wildcard,使用方法如下:

1
$(wildcard PATTERN…)

比如:

1
$(wildcard *.c)

上面的代码是用来获取当前目录下所有的.c 文件,类似“%”。