1. 什么是Makefile?
Makefile 是一个存放编译规则的文件,它的核心作用是根据文件的时间戳,判断当前文件是否需要参与本次编译,从而实现高效编译。
Makefile 主要解决三个核心问题:
-
只需编写一次编译规则,后续无需重复输入复杂的编译命令;
-
实现增量编译:改了哪个文件,就只重新编译哪个文件,避免重复编译所有文件,节省时间;
-
一条 make 命令即可完成整个工程的编译,简化操作。
2. 什么是make?
make 是一个可执行程序,通常存放在 /usr/bin/ 路径下。当在终端执行 make 命令时,它会自动解析当前目录下的 Makefile 文件,并按照文件中定义的规则,编译当前目录下的工程。
3. 直接上手写一个简单的Makefile
先准备一个简单的C文件 test.c
#include <stdio.h>
int main(int argc, const char * argv[])
{
printf("hello world\\n");
return 0;
}
在同一目录下新建 Makefile 文件,内容如下:
test:test.c
gcc test.c -o test
clean: # clean是伪目标(不会生成名为clean的文件),执行 make clean 即可触发
rm test
看不懂没关系,下面我们逐步拆解基础知识,再一步步优化这个 Makefile。
4. 基础知识讲解
Makefile 的核心是「规则(rule)」,每个规则的基本格式如下:
target: dependencies
command
-
target(目标):可以是要生成的目标文件(如 test、main.o),也可以是一个动作名称(如 all、clean);
-
dependencies(依赖):生成当前目标所需要的文件或其他目标(如生成 test 依赖test.c);
-
command(命令):构建当前目标的具体命令(如 gcc test.c -o test),必须以 Tab 键开头(绝对不能用4个或8个空格代替,否则会报错)。
5. 逐步优化Makefile(从“看得懂”到“看不懂”)
先准备3个文件:add.c、add.h、main.c(模拟多文件工程),先写一个简单的 Makefile:
main:main.c add.c add.h
gcc main.c add.c add.h -o main
clean:
rm main
这个写法很简单,但存在一个问题:无法实现增量编译——只要修改了其中任意一个文件,所有文件都会被重新编译,效率很低。
因此我们优化一下,将每个 .c 文件单独编译成 .o 目标文件,再链接成可执行文件:
main:main.o add.o
c main.o add.o -o main # 注意:原代码多写了sub.o,此处修正(无sub相关文件)
main.o:main.c
main.c -o main.o # -c 表示仅编译,不链接,生成.o文件
add.o:add.c
-c add.c -o add.o
clean:
main rm *.o gcc gcc -c gc
注意:无需在依赖中写.h 文件,编译器会自动查找并包含头文件。
这样修改后,就能实现增量编译了:改了哪个 .c 文件,就只重新编译对应的 .o 文件,再链接生成可执行文件。但这样写还是太直观,我们继续优化,让它更“专业”。
5.1 引入Makefile变量
Makefile 中的变量无需定义类型,随用随写即可。
-
变量赋值:var=456;
-
变量使用:通过$(变量名) 或 ${变量名} 获取变量值(前面必须加 $);
-
命令回显:命令前加 @,可以取消命令执行时的回显(避免终端显示冗余的命令本身);
-
注释:用 # 开头,后面的内容为注释。
示例:
var1=123
all:
echo $(var1) # 终端只显示123,不显示echo $(var1)
# 这是一条注释,不会被执行 @ @
5.2 变量的4种赋值方式
(1)延迟赋值 =
赋值时不立即确定变量值,而是在 Makefile 解析到最后一次赋值时,才确定变量的最终值。
var1=123
var2=$(var1) # 此时不确定var2的值,等待var1的最后一次赋值
var1=456
all:
@echo $(var2) # 输出456(取var1最后一次赋值的结果)
@echo $(var1) # 输出456
(2)立即赋值 :=
赋值时立即确定变量值,后续修改原变量,不会影响已赋值的变量。
var1=123
var2:=$(var1) # 立即赋值,var2的值固定为当前var1的值(123)
var1=456
all:
echo $(var2) # 输出123(不受var1后续修改影响)
cho $(var1) # 输出456 @e @
(3)附加赋值 +=
在原有变量值的末尾追加新内容,类似 C 语言中的 strcat 函数。
var1=123
var2=hello
var2 += $(var1) # 在var2末尾追加var1的值
all:
ho $(var2) # 输出hello 123 @ec
(4)询问赋值 ?=
询问变量是否已经赋值:如果之前已经赋值,本次赋值无效;如果之前未赋值,本次赋值生效。
var1=123
# var2=hello # 注释掉,模拟var2未赋值
var2 ?= $(var1) # var2未赋值,本次赋值生效
all:
cho $(var2) # 输出123 @e
(5)命令行赋值
除了在 Makefile 中赋值,还可以在执行 make 命令时,通过命令行直接给变量赋值,优先级高于 Makefile 中的赋值。
var2 ?= 123
all:
echo $(var2) @
执行命令及结果:
-
make → 输出 123(Makefile 中赋值生效);
-
make var2=345 → 输出 345(命令行赋值覆盖 Makefile 中的赋值)。
5.3 用变量优化Makefile
结合变量赋值,我们优化之前的 Makefile,让它更简洁、更灵活:
TARGET?=main # 可执行文件名,支持命令行覆盖
OBJs:= main.o add.o # 目标文件列表,立即赋值
CC?=gcc # 编译器,支持命令行覆盖(如 make CC=clang)
Cflags:= -c -o # 编译选项(-c:仅编译,-o:指定输出文件)
$(TARGET):$(OBJs)
) $(OBJs) -o $(TARGET) # 链接所有.o文件,生成可执行文件
main.o:main.c
(CC) main.c $(Cflags) main.o # 编译main.c生成main.o
add.o:add.c
CC) add.c $(Cflags) add.o # 编译add.c生成add.o
clean:
BJs) $(TARGET) # 清理生成的.o文件和可执行文件 rm $(O $( $ $(CC
这样修改后,Makefile 已经开始“看不懂”了,但我们还能继续优化!
5.4 引入自动变量
Makefile 提供了几个常用的自动变量,可替代重复的目标名、依赖名,让代码更简洁(仅在规则中生效):
-
$@:当前规则的目标名;
-
$<:当前规则的第一个依赖名;
-
$^:当前规则的所有依赖名(去重);
-
$*:仅在模式规则中生效,匹配% 对应的文件名主干(等价于“去掉扩展名的目标”,但普通规则中使用为空)。
示例(直观理解自动变量):
main.o:main.c add.c
cho $@ # 输出 main.o(目标名)
o $* # 输出 main(模式规则中,去掉扩展名的目标主干)
$< # 输出 main.c(第一个依赖)
@echo $^ # 输出 main.c add.c(所有依赖) @echo @ech @e
用自动变量优化 Makefile,去掉重复的文件名:
TARGET?=main
OBJs:= main.o add.o sub.o
CC?=gcc
Cflags:= -c -o
$(TARGET):$(OBJs)
$(CC) $^ -o $@
main.o:main.c
$(CC) $< $(Cflags) $@
add.o:add.c
$(CC) $< $(Cflags) $@
clean:
rm $(OBJs) $(TARGET)
现在的 Makefile 已经和最开始的版本完全不一样了,但我们还能继续优化!
5.5 引入模式通配符 %
% 是 Makefile 特有的模式通配符,核心作用是:替代重复的单个规则,批量匹配“相同主干、不同后缀”的文件,实现批量编译。
比如,所有 .c 文件编译成对应的 .o 文件,无需为每个 .c 文件单独写规则,用 %.o:%.c 即可批量匹配。
用 % 继续优化 Makefile,去掉重复的 .o 编译规则:
TARGET?=main
OBJs:= main.o add.o sub.o
CC?=gcc
Cflags:= -c -o
$(TARGET):$(OBJs)
$(CC) $^ -o $@
%.o:%.c
$(CC) $< $(Cflags) $@
clean:
rm $(OBJs) $(TARGET)
到这里,Makefile 已经足够“专业”(看不懂)了,但我们还要加一个知识点,让它更健壮!
5.6 伪目标声明 .PHONY
.PHONY 是 Makefile 的伪目标声明指令,用于标记“不是实际文件,仅用于执行命令”的目标(如 clean、all)。
Makefile 的核心设计是「构建文件」,默认认为所有目标都是“要生成的文件”,执行 make 目标 时,会先检查:
该目标文件是否存在;
目标文件的修改时间是否比依赖文件新。
如果满足“文件存在且最新”,make 会直接提示 目标 is up to date,跳过目标对应的命令——这会导致 clean 这类“执行命令”的目标失效。
比如:如果当前目录下误创建了一个名为 clean 的文件,执行 make clean 时,make 会把 clean 当成“要检查的文件”,发现文件存在且无依赖,直接跳过 rm 命令,导致清理失败。
此时,加上 .PHONY 声明,就能避免这种问题——它会告诉 make:“这个目标不是文件,忽略文件检查,直接执行命令”。
最终优化版(最健壮、最专业):
TARGET?=main
OBJs:= main.o add.o sub.o
CC?=gcc
Cflags:= -c -o
$(TARGET):$(OBJs)
$(CC) $^ -o $@
%.o:%.c
$(CC) $< $(Cflags) $@
.PHONY: clean
clean:
rm $(OBJs) $(TARGET)
6. 新手必记注意事项
-
命令必须以 Tab 键 开头,不能用空格代替,否则会报错;
-
变量赋值的4种方式:=(延迟)、:=(立即)、+=(附加)、?=(询问),根据需求选择;
-
自动变量仅在规则中生效,$* 仅在模式规则中有效;
-
% 是 Makefile 特有通配符,用于批量匹配,需在目标和依赖中配对使用;
-
clean、all 等“命令型目标”,必须加 .PHONY 声明;
-
清理命令 rm 建议加 -f,避免文件不存在时报错,更健壮。




