当工程中的文件逐渐增多,使用 GCC 命令编译每个源文件就会变得力不从心。此时我们需要借助项目构造工具 make 帮助我们完成这个任务。

本文创建于 2024-11-25;修改于 2024-11-25

1. make

make工具在构造项目的时候需要加载一个叫做makefile的文件,makefile关系到了整个工程的编译规则。一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令。

makefile 文件须命名为: Makefile or makefile

在哪个目录下执行构建命令,哪个目录下的 makefile 文件就会被加载。

2. 语法格式

# 每条规则的语法格式:
target1,target2...: depend1, depend2, ...
    command1
    command2
    ...

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

  • 命令(command):编译目标的 shell 命令
    • 比如,通过 shell 命令 编译文件,库文件,进入文件夹等
    • 注意,每行命令前都要有缩进
  • 依赖(depend):规则所需的依赖文件
    • 比如 .c .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

3. 工作原理

3.1 时间戳

make 命令执行时,会根据文件的时间戳来判断是否执行 makefile 规则中的相关命令。

make 会先判断目标是否存在,如果不存在,则命令一定会执行;如果目标已经存在,则会判断时间戳:

  • 正常情况下,目标时间戳 > 所有依赖的时间戳,如果满足这个条件,则意味着相关依赖并没有任何更新或变化,自然也就无需重新编译目标。
  • 当依赖文件被更新了,则 目标时间戳 < 某些依赖的时间戳,此时,make 会根据命令重新生成目标。

3.2 自动推导

$ tree
.
├── add.c
├── div.c
├── head.h
├── main.c
├── makefile
├── mult.c
└── sub.c

# makefile 文件:
# 这是一个完整的 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

此时执行 make 进行构建会发现,项目构建成功了,这说明 make 进行了自动推导,在我们的目录下并没有 add.o div.o main.o mult.o sub.o这些依赖,make 根据同名的 .c 文件进行了自动编译,最终生成我们需要的目标文件。

4. 变量

我们可以在 makefile 文件中使用变量来进行规则的定义,makefile 文件中的变量分为三种:自定义变量、预定义变量、自动变量。

4.1 自定义变量

# 创建一个变量并赋值,不能只创建变量名而不赋值
variable=value

# 用 $(name) 的形式取出变量值
# 定义变量 variable
variable=a.o
# 对 variable 进行取值
$(variable)

# 举例:
obj=a.c b.c c.c d.c
target=app
$(target):$(obj)
    gcc $(obj) -o $(target)

4.2 预定义变量

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

变 量 名 含 义 默 认 值
AR 生成静态库库文件的程序名称 ar
AS 汇编编译器的名称 as
CC C 语言编译器的名称 cc
CPP C 语言预编译器的名称 $(CC) -E
CXX C++语言编译器的名称 g++
FC FORTRAN 语言编译器的名称 f77
RM 删除文件程序的名称 rm -f
ARFLAGS 生成静态库库文件程序的选项 无默认值
ASFLAGS 汇编语言编译器的编译选项 无默认值
CFLAGS C 语言编译器的编译选项 无默认值
CPPFLAGS C 语言预编译的编译选项 无默认值
CXXFLAGS C++语言编译器的编译选项 无默认值
FFLAGS FORTRAN 语言编译器的编译选项 无默认

示例:

obj=a.c b.c c.c d.c
target=app
# 代码优化参数,注意是字母 O 不是数字 0
CFLAGS=-O3
$(target):$(obj)
    $(CC) $(obj) -o $(target) $(CFLAGS)

4.3 自动变量

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

变 量 含 义
$* 表示目标文件的名称,不包含目标文件的扩展名
$+ 表示所有的依赖文件,这些依赖文件之间以空格分开,按照出现的先后为顺序,其中可能 包含重复的依赖文件
$< 表示依赖项中第一个依赖文件的名称
$? 依赖项中,所有比目标文件时间戳晚的依赖文件,依赖文件之间以空格分开
$@ 表示目标文件的名称,包含文件扩展名
$^ 依赖项中,所有不重复的依赖文件,这些文件之间以空格分开

示例:

# 使用自动变量,替换命令中的相关内容
app:a.c b.c c.c d.c
    gcc $^ -o $@

5. 模式匹配

对于makefile文件中可能出现的大量重复性规则,我们可以将重复的动作化为一个模版,类似的操作都通过模版去匹配,从而简化了我们的makfile文件。

示例:

# 每一个 .o 目标,如果它对应的 .c 文件存在,则执行下面的命令。% 是一个通配符,匹配任意字符串。
%.o:%.c
    gcc $< -c

6. 函数

makefile 中的所有函数都是有返回值的。函数的调用形式如:$(函数名 参数1, 参数2, …)

6.1 wildcard 函数

wildcard 函数的主要作用是搜索并获取指定目录下指定类型的文件名,其返回值是以空格分割的、指定目录下的所有符合条件的文件名列表。

使用示例:

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

6.2 patsubst 函数

patsubst 函数的功能是按照指定的模式替换指定的文件名的后缀

使用示例:

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

7. 练习

根据以下目录结构编写 makefile 文件:

# 目录结构
.
├── include
│   └── head.h	==> 头文件, 声明了加减乘除四个函数
├── main.c		==> 测试程序, 调用了head.h中的函数
└── src
    ├── add.c	==> 加法运算
    ├── div.c	==> 除法运算
    ├── mult.c  ==> 乘法运算
    └── sub.c   ==> 减法运算

makefile 文件:

# 最终的目标名 app
target=app
# 搜索当前目标目录下的源文件
src=$(wildcard *.c ./src/*.c)
# 将 $(src) 中文件拓展名替换为 .o
obj=$(patsubst *.c, *.o, $(src))
# 头文件目录
include=./include

# 执行链接操作得到最终目标
$(target):$(obj)
    $(CC) $^ -o $@

# 模式匹配,将每一个 .c 文件汇编为 .o 文件
%.o:%.c
    $(CC) $< -c -I $(include) -o $@

# .PHONY 说明 clean 是一个伪目标
.PHONY:clean
clean:
    rm $(obj) $(target) -f


本文资料来源 爱编程的大丙,看了这位老师B站的很多课程,受益匪浅,感谢!