Makefile 入门使用与深入解析(超详细!!!)
文章目录
1. 前言
在软件开发(尤其是 C/C++、嵌入式、Linux 应用)中,Makefile 是最经典的构建工具之一。它能自动化管理依赖、提高编译效率、支持跨平台和交叉编译,是任何工程化项目的必备工具。本文将结合实例,对 Makefile 进行详细讲解。
2. 为什么要用 Make / Makefile
- 当项目只有一个
hello.c,你可以gcc hello.c -o hello。 - 当项目有几十个
.c、.h文件时,手动管理编译命令不现实。 - 修改一个文件后,只编译受影响的部分,节省时间。
- 支持跨平台、交叉编译、安装、清理等完整工作流。
📌 简而言之:Makefile 能让你的项目 高效、可维护、自动化。
在项目的实际开发过程中就算你只是修改一个文件,也需要重新编译所有的文件,会浪费了很多开发时间。 要解决这个问题,最好的方式就是把工程的编译规则写下来,让编译器自动加载该规则进行编译。 解决方法就是使用make和Makefile,这两个工具是搭配使用的,下面给大家介绍一下:
-
make工具:它可以帮助我们找出项目里面修改变更过的文件,并根据依赖关系,找出受修改影响的其他相关文件,然后对这些文件按照规则进行单独的编译,这样一来,就能避免重新编译项目的所有的文件。
-
Makefile文件:上面提到的规则、依赖关系主要是定义在这个Makefile文件中的,我们在其中合理地定义好文件的依赖关系之后,make工具就能精准地进行编译工作。

我们管理一个项目工程,实质上就是管理项目文件间的依赖关系。 所以我们在学习和使用Makefile的时候,一定要牢牢抓住它这种面向依赖的思想, 心里一定要谨记,Makefile中所有的复杂、晦涩的语法都是更好地为解决依赖问题而存在的。 理解了它的本质目的之后,我们以后在学习它的过程中就不用死记硬背各种语法了, 只要想想这个本质目的,你会觉得一切都是那么地顺理成章。
在学习Makefile知识之前,要现在脑海中初步建立Makefile知识点的整体框架,以此来指导进一步的学习。 接下来我们先整体看一下要学习Makefile的知识点,如下图所示:

1、 基础语法– 描述目标和依赖的特定格式,Makefile的核心。
2、 变量– 记录特定的信息,避免重复输入原始信息。尤其是手动输入原始信息很长时,特别好用。
3、 分支判断– 灵活控制多个不同的编译过程,方便兼容不同属性。
4、 头文件依赖– 监控头文件的变化,头文件也是程序的关键内容。
5、 隐含规则– 利用Makefile的一些默认规则,可以减少编写Makefile的工作量。
6、 自动化变量– 利用Makefile的默认的自动化变量,可以减少编写Makefile的工作量。
7、 模式规则– 灵活使用正则表达式,可以减少编写Makefile的工作量。
8、 函数– 使用Makefile的各种函数,可以更方便地实现Makefile的功能。
了解完Makefile的知识点,从上面的分析可以知道,Makefile的核心在于基础语法,用来描述目标和依赖的关系。 其他语法的目的,是为了减少我们编写Makefile工作量,让我们能够以更加优雅、更加简洁、更好维护的方式来实现Makefile的功能。 这跟我们程序开发是很相似的,不止要实现功能,还要兼顾程序的可读性、拓展性、可维护性等等。
3. Makefile 的基本结构
target: prerequisites
<TAB>command
接下来,我们做一个小的实验让大家对Makefile有一个大致的认知:
我们新建一个Makefile文件,在文件里面写入以下内容:
#Makefile格式
#目标:依赖的文件或其它目标
#Tab 命令1
#Tab 命令2
#第一个目标,是最终目标及make的默认目标
#目标a,依赖于目标targetc和targetb
#目标要执行的shell命令 ls -lh,列出目录下的内容
targeta: targetc targetb
ls -lh
#目标b,无依赖
#目标要执行的shell命令,使用touch创建test.txt文件
targetb:
touch test.txt
#目标c,无依赖
#目标要执行的shell命令,pwd显示当前路径
targetc:
pwd
#目标d,无依赖
#由于abc目标都不依赖于目标d,所以直接make时目标d不会被执行
#可以使用make targetd命令执行
targetd:
rm -f test.txt
在这里需要注意的几个地方:出现的第一个目标,是最终目标及make的默认目标,在上面的例子中,targeta就是默认目标。
| 目标文件 | 说明 |
|---|---|
targeta | 这是Makefile中的第一个目标代号,在符号“:”后 面的内容表示它依赖于targetc和targetb目标,它自身的命令为“ls -lh”,列出当前目录下的内容。 |
targetb | 这个目标没有依赖其它内容,它要执行的命令为“touch test.txt”,即创建一个test.txt文件。 |
targetc | 这个目标同样也没有依赖其它内容,它要执行的命令为“pwd”,就是简单地显示当前的路径。 |
targetd | 这个目标无依赖其它内容,它要执行的命令为“rm -f test.txt”,删除目录下的test.txt文件。与targetb、c不同的是,targeta不依赖targetd,所以不会执行。 |

如果我们想要单独执行Makefile中的某个目标,可以使用”make 目标名“的语法,例如上图中分别执行了”make targetd“ 和”make targetc“指令,在执行”make targetd”目标时,可看到它的命令rm -f test.txt被执行,test.txt文件被删除。
从这个过程,可了解到make程序会根据Makefile中描述的目标与依赖关系,执行达成目标需要的shell命令。简单来说,Makefile就是用来指导make程序如何干某些事情的清单。
3.1. 目标与依赖
- 目标 (target):最终要生成的文件(如可执行程序、库、.o 文件)。指make要做的事情,可以是一个简单的代号,也可以是目标文件,需要顶格书写,前面不能有空格或Tab。一个Makefile可以有多个目标,写在最前面的第一 个目标,会被Make程序确立为 “默认目标”,例如前面的targeta。
- 依赖 (prerequisites):生成目标所依赖的文件。
3.2.命令(Recipe)
- 每条命令前必须用 Tab 开头!!! 一定不能使用空格进行代替,make不会识别。
- 命令执行时会显示在终端,加
@可以隐藏命令本身。
3.3 伪目标
.PHONY 声明伪目标,避免与真实文件冲突。例如我们的项目已经存在一个clean文件,可是在Makefile文件里又需要定义clean目标文件,此时make才知道我们需要执行的make clean是什么。只要我们不期待生成目标文件,就应该把它定义成伪目标,前面的演示代码修改如下:
示例:
#使用.PHONY表示targeta是个伪目标
.PHONY:targeta
#目标a,依赖于目标targetc和targetb
#目标要执行的shell命令 ls -lh,列出目录下的内容
targeta: targetc targetb
ls -lh
#使用.PHONY表示targetb是个伪目标
.PHONY:targetb
#目标b,无依赖
#目标要执行的shell命令,使用touch创建test.txt文件
targetb:
touch test.txt
#使用.PHONY表示targetc是个伪目标
.PHONY:targetc
#目标c,无依赖
#目标要执行的shell命令,pwd显示当前路径
targetc:
pwd
#使用.PHONY表示targetd是个伪目标
.PHONY:targetd #当然使用clean更加明了 也可以是.PHONY:clean 后面也需要进行相应的修改
#目标d,无依赖
#由于abc目标都不依赖于目标d,所以直接make时目标d不会被执行
#可以使用make targetd命令执行
targetd:
rm -f test.txt
GNU组织发布的软件工程代码的Makefile,常常会有类似以上代码中定义的clean伪目标,用于清 除编译的输出文件。常见 的还有**“all”、“install”、“print”、“tar”等分别用于编译所有内容、安装已编译好的程序、列出被修改的文件及打包成tar文件**。虽然并没有固定的要求伪目标必须用这些名字,但可以参考这些习惯来编写自己的Makefile。
4. Makefile 的编译过程
make 执行的过程大致分为以下几步:
- 读取 Makefile:加载规则、变量、函数。
- 构建依赖关系图:建立目标与依赖的有向图。
- 判断目标是否需要更新:比较时间戳,若目标比依赖旧或不存在,则需要重建。
- 执行命令:从依赖树底层开始,逐级执行命令。
- 生成最终目标:完成用户指定的目标。
📌 示例:
#Makefile格式
#目标文件:依赖的文件
#Tab 命令1
#Tab 命令2
hello_main: hello_main.o hello_func.o
gcc -o hello_main hello_main.o hello_func.o
#以下是make的默认规则,下面两行可以不写
#hello_main.o: hello_main.c
# gcc -c hello_main.c
#以下是make的默认规则,下面两行可以不写
#hello_func.o: hello_func.c
# gcc -c hello_func.c
在Makefile的实际应用中,通常会把编译和最终的链接过程分开。也就是说,我们的hello_main目标文件本质上并不是依赖hello_main.c和hello_func.c文件,而是依赖于hello_main.o和hello_func.o,把这两个文件链接起来就能得到我们最终想要的hello_main目标文件。另外,由于make有一条默认规则,当找不到xxx. o文件时,会查找目录下的同名xxx.c文件进行编译,也就是上面代码中被注释掉的地方。
我们在这里同样举例子说明:
hello_main.c:
#include "hello_func.h"
int main()
{
hello_func();
return 0;
}
hello_func.h
void hello_func(void);
hello_func.c
#include <stdio.h>
#include "hello_func.h"
void hello_func(void)
{
printf("hello, world! This is a C program.\n");
for (int i=0; i<10; i++ ) {
printf("hello world!!!\n",i);
}
}
同样我们执行Makefile文件:

5. 变量与赋值方式
使用C自动编译成.o的默认规则有个缺陷,由于没有显式地表示.o依赖于.h头文件,假如我们修改了头文件的内容,那么.o并不会更新,这是不可接受的。并且默认规则使用固定的“cc”进行编译,假如我们想使用ARM-GCC或者其他的进行交叉编译,那么系统默认的“cc”会导致编译错误。
要解决这些问题并且让Makefile变得更加通用,需要引入变量和分支进行处理。
基础语法:
| 变量定义 | 说明 |
|---|---|
= | 延时赋值,该变量只有在调用的时候,才会被赋值 |
:= | 直接赋值,与延时赋值相反,使用直接赋值的话,变量的值定义时就已经确定了。 |
?= | 若变量的值为空,则进行赋值,通常用于设置默认值。 |
+= | 追加赋值,可以往变量后面增加新的内容。 |
当我们想使用变量时,其语法如下:
$(变量名)
示例:
A = foo
B = $(A)
C := $(A)
A += bar
D ?= foo bar bar
all:
@echo A=$(A)
@echo B=$(B)
@echo C=$(C)
@echo D=$(D)
输出:

A = foo - 使用递归展开式定义变量 A
B = $(A) - B 延时赋值,会在使用时才赋值
C := $(A) - C 直接赋值,在定义时就立即赋值
A += bar - 追加操作,此时 A 变为 "foo bar"
D ?= foo bar bar - 条件赋值,如果 D 未定义才定义
6. 自动化变量
Makefile中还有其它自动化变量,此处仅列出方便以后使用到的时候进行查阅,见下表。
| 变量 | 含义 |
|---|---|
$@ | 当前目标 |
$< | 依赖中的第一个目标文件 |
$^ | 所有依赖,如果依赖中有重复的,只保留一份 |
$% | 与 @ 类似,但 @类似,但 @类似,但%仅匹配“库”类型的目标文件 |
$+ | 所有的依赖目标,即使依赖中有重复的也原样保留 |
$? | 所有比目标新的依赖 |
在后面我会给出相应的例子供大家学习。
7. 使用分支
就像前面所说,当我们想要切换编译器的时候,就可以使用条件分支语法。在Makefile中的条件分支语法如下:
ifeq(arg1, arg2)
分支1
else
分支2
endif
分支会比较括号内的参数“arg1”和“arg2”的值是否相同,如果相同,则为真,执行分支1的内容,否则的话,执行分支2 的内容,参数arg1和arg2可以是变量或者是常量。
# Makefile格式
# 目标文件:依赖的文件
# Tab 命令1
# Tab 命令2
# 定义变量
# ARCH默认为x86,使用gcc编译器,
# 否则使用arm编译器
ARCH ?= x86
TARGET = hello_main
CFLAGS = -I.
LDFLAGS =
DEPS = hello_func.h
OBJS = hello_main.o hello_func.o
# 根据输入的ARCH变量来选择编译器
# ARCH=x86,使用gcc
# ARCH=arm,使用arm-gcc
ifeq ($(ARCH),x86)
CC = gcc
else
CC = aarch64-linux-gnu-gcc
endif
# 默认目标
all: $(TARGET)
# 目标文件
$(TARGET): $(OBJS)
$(CC) -o $@ $^ $(LDFLAGS)
# *.o文件的生成规则
%.o: %.c $(DEPS)
$(CC) -c -o $@ $< $(CFLAGS)
# 伪目标
.PHONY: all clean run print
clean:
rm -f *.o $(TARGET)
run: $(TARGET)
./$(TARGET)
print:
@echo "Architecture: $(ARCH)"
@echo "Compiler: $(CC)"
@echo "Target: $(TARGET)"

8. 模式规则
在上面的例子中也是用到了很多模式规则,例如下面这个:”%”是一个通配符,功能类似”*”,如”%.o”表示所有以”.o”结尾的文件。所以”%.o : %.c”在本例子中等价 于”hello_main.o : hello_main.c”、”hello_func.o : hello_func.c”,即等价于o文件依赖于c文件的默认规则。不过这行代码后面的” ( D E P S ) ”表示它除了依赖 c 文件,还依赖于变量” (DEPS)”表示它除了依赖c文件,还依赖于变量” (DEPS)”表示它除了依赖c文件,还依赖于变量”(DEPS)”表示的头文件,所以当头文件修改的话,o文件也会被重新编译。
# *.o文件的生成规则
%.o: %.c $(DEPS)
$(CC) -c -o $@ $< $(CFLAGS)
可自动匹配所有 .c → .o 的规则,避免重复写。
9. Makefile 内置函数详解
在更复杂的工程中,头文件、源文件可能会放在二级目录,编译生成的*.o或 可执行文件也放到专门的编译输出目录方便整理,如下图所示。示例中*.h头文件 放在includes目录下,*.c文件放在sources目录下,编译输出存放在build中。
实现这些复杂的操作通常需要使用Makefile的函数。GNU Make 提供了大量函数,这里重点讲四个:notdir、patsubst、wildcard 、foreach 。
函数格式及示例
在Makefile中调用函数的方法跟变量的使用 类似,以“ ( ) ”或“ ()”或“ ()”或“{}”符号包含函数名和参数,具体语法如下:
$(函数名 参数)
#或者使用花括号
${函数名 参数}
9.1. notdir—— 去除目录函数
notdir函数用于去除文件路径中的目录部分。它的格式如下:
语法:
$(notdir 文件名)
示例:
# 示例 Makefile
files = src/main.c lib/utils.c include/header.h
files_no_dir = $(notdir $(files))
all:
@echo "原始文件列表: $(files)"
@echo "去掉目录后的文件列表: $(files_no_dir)"

9.2. patsubst —— 模式替换函数
当输入的字符串符合匹配规则,那么使用替换规则来替换字符串,当匹配规则中有“%”号时,替换规 则也可以例程“%”号来提取“%”匹配的内容加入到最后替换的字符串中。
语法:
$(patsubst 匹配规则, 替换规则, 输入的字符串)
示例:
# 示例 Makefile
src_files = src/main.c src/utils.c src/network.c
obj_files = $(patsubst %.c,%.o,$(src_files))
all:
@echo "src_files: $(src_files)"
@echo "obj_files: $(obj_files)"

9.3. wildcard —— 文件匹配函数
wildcard函数用于获取文件列表,并使用空格分隔开。它的格式如下:
语法:
$(wildcard 匹配规则)
示例:
# 示例 Makefile
# 假设当前目录下有 main.c, utils.c, network.c, test.c 文件
c_files = $(wildcard *.c)
h_files = $(wildcard *.h)
all:
@echo "c_files: $(c_files)"
@echo "h_files: $(h_files)"

常用于自动收集源文件。
9.4. foreach —— 循环函数
语法:
$(foreach var,list,text)
示例:
# 示例 Makefile
files = main.c utils.c network.c
result = $(foreach file,$(files),Processing $(file))
all:
@echo "$(result)"

10. 清理与安装
install: program
install -m 755 program /usr/local/bin/
clean:
rm -rf build *.o program
.PHONY: install clean
11. 常见技巧与调试方法
make -n只显示命令不执行。make -B强制重建。make -j4并行构建。make -d调试依赖判断。- 命令前
@隐藏输出,-忽略错误。
12. 常见陷阱
- Tab 与空格混用:命令必须以 Tab 开头。
- 伪目标未声明:
clean必须加.PHONY。 - 依赖不完整:未追踪头文件变化,导致编译不更新。
13. 小结
Makefile 是构建系统的基石,掌握它能让你在 Linux、嵌入式开发中事半功倍。从简单的规则开始,逐步使用模式规则、内置函数、条件判断和多目录管理,就能写出高效、清晰、可维护的 Makefile。
13万+

被折叠的 条评论
为什么被折叠?



