环境说明:本系列使用 Xshell 7 连接 腾讯云服务器 Ubuntu 22.04 进行操作演示。如果你还没有搭建好环境,请先阅读本系列前面的文章。
前言
在上一篇文章中,我们学会了使用 GCC 编译器编译代码。但那个例子只用了一个源文件,几行命令就能搞定。可是真实的项目是什么样的呢?
一个真实的 C/C++ 项目,源文件可能有几百甚至上千个,它们分散在不同的目录里,相互之间还存在依赖关系。如果每次都手动敲 gcc xxx.c yyy.c zzz.c -o app,不仅麻烦,而且——
- 改一个文件要重新编译全部文件:浪费时间
- 文件多了根本记不住依赖关系:容易出错
- 每次编译都要手敲一长串命令:效率极低
幸好,Linux 世界早就有了解决方案——make 和 Makefile。它们能让整个项目"一键编译",是每个 Linux 程序员必须掌握的技能。
一句话总结:会不会写 Makefile,从侧面反映了一个人是否具备完成大型工程的能力。
思维导图
下面是本文内容的完整思维导图,建议先整体浏览一遍,建立全局认知后再逐节学习。

一、make 与 Makefile 简介
1.1 什么是 make 和 Makefile?
语法形式
make # 执行 Makefile 中的第一个目标
make 目标名 # 执行指定的目标
功能
这里要先区分两个东西:
| 名称 | 是什么 | 作用 |
| make | 一个命令工具 | 解释并执行 Makefile 中的指令 |
| Makefile | 一个文件 | 定义项目的编译规则 |
它们是搭配使用的:make 是命令,Makefile 是规则文件。你在终端敲 make,make 工具就会去读当前目录下的 Makefile 文件,然后按照里面定义的规则自动编译项目。
类比理解:把 Makefile 想象成一份"菜谱",make 就是"厨师"。菜谱(Makefile)写清楚每道菜需要什么食材、怎么做;厨师(make)照着菜谱一步步操作,最后端出成品(可执行程序)。
Makefile 带来的好处:
- 自动化编译:一旦写好,只需一个
make命令,整个工程完全自动编译2. - 增量编译:只重新编译修改过的文件,未修改的文件不重复编译,大大节省时间
- 依赖管理:自动维护文件之间的依赖关系,指定谁先编译、谁后编译
- 跨平台:几乎所有 IDE 都支持 make,Linux 下的 GNU make 更是标配
示例
——演示 make 的基本使用流程
# 1. 先创建一个简单的 C 程序
vim myproc.c
// myproc.c
#include <stdio.h>
int main()
{
printf("hello Makefile!\n");
return 0;
}
# 2. 创建 Makefile 文件(注意:文件名必须是 Makefile 或 makefile)
vim Makefile
myproc:myproc.c
gcc -o myproc myproc.c
# 3. 直接执行 make 命令
make
# 输出:
# gcc -o myproc myproc.c
# 4. 运行生成的可执行文件
./myproc
# 输出:hello Makefile!
新手注意:
make会自动在当前目录找名为Makefile(首字母大写)或makefile(全小写)的文件。文件名千万别写错,否则 make 会报错make: *** No targets specified and no makefile found. Stop.
二、Makefile 的基本语法规则
Makefile 的核心就是一条条规则(Rule)。每条规则告诉 make:"要生成什么,需要什么,怎么生成"。
2.1 规则的三要素
语法形式
目标(target): 依赖(dependency)
命令(command)
注意:命令前面必须是 Tab 键缩进(不能用空格),这是新手最容易踩的坑!
功能
一条 Makefile 规则由三部分组成:
| 组成部分 | 英文 | 含义 |
| 目标 | target | 要生成的文件(冒号左边) |
| 依赖 | dependency | 生成目标所需要的文件(冒号右边) |
| 命令 | command | 具体的生成动作(下一行 Tab 缩进) |
规则的含义:目标 依赖于 依赖,如果 依赖 比 目标 新,就执行 命令 来生成 目标。
一句话理解:规则格式可以读作——"目标 由 依赖 生成,生成方法是执行 命令"。
示例
——演示最基本的 Makefile 规则:依赖关系与依赖方法
# Makefile 内容
myproc:myproc.c
gcc -o myproc myproc.c
# 逐行理解这条规则:
# 【目标】myproc —— 要生成的可执行文件
# 【依赖】myproc.c —— 生成 myproc 需要 myproc.c 这个源文件
# 【命令】gcc -o myproc myproc.c —— 用这条命令从 myproc.c 生成 myproc
# 执行 make
make
# make 发现 myproc 不存在(或 myproc.c 比 myproc 新),于是执行命令
# 输出:gcc -o myproc myproc.c
# 再次执行 make(此时 myproc 已经是最新的)
make
# 输出:make: 'myproc' is up to date.
# make 发现 myproc 比 myproc.c 新,不需要重新编译!
关键概念:
- 依赖关系:
myproc依赖myproc.c
- 依赖方法:
gcc -o myproc myproc.c一进一出的对应关系,就是 Makefile 的最小单元。
——演示 Tab 缩进的坑(新手必看)
# 错误示范:命令前面用的是空格而不是 Tab
vim Makefile
myproc:myproc.c
gcc -o myproc myproc.c # 这行用的是 4 个空格,不是 Tab!
make
# 报错:
# Makefile:2: *** missing separator. Stop.
# ↑ 缺少分隔符,就是因为你用了空格而不是 Tab
# 正确做法:命令前面必须是 Tab 键
# 在 Vim 中,先按 Tab 键,再输入命令
# 可以用 cat -A Makefile 查看缩进(Tab 会显示为 ^I)
cat -A Makefile
# myproc:myproc.c$
# ^Igcc -o myproc myproc.c$ ← ^I 就是 Tab
新手避坑:"missing separator" 是新手最常遇到的 Makefile 报错,99% 的原因是命令前用了空格而不是 Tab。记住:Makefile 中命令必须用 Tab 缩进!在 Vim 里如果 Tab 键被设置成了空格,可以先去设置里确认,或者用
cat -A检查一下。
2.2 多规则与依赖链
语法形式
目标1: 依赖1
命令1
目标2: 依赖2
命令2
功能
一个 Makefile 可以包含多条规则。当目标的依赖本身又是另一条规则的目标时,就形成了依赖链,make 会一层层递归地处理。
示例
——演示完整编译流程的 Makefile(从 .c 到可执行文件)
还记得上一篇文章讲的 GCC 编译四大步骤吗?我们用 Makefile 把每一步都写进规则里:
# 完整版 Makefile
myproc: myproc.o
gcc myproc.o -o myproc
myproc.o: myproc.s
gcc -c myproc.s -o myproc.o
myproc.s: myproc.i
gcc -S myproc.i -o myproc.s
myproc.i: myproc.c
gcc -E myproc.c -o myproc.i
# 执行 make
make
# 输出:
# gcc -E myproc.c -o myproc.i ← 先生成 .i
# gcc -S myproc.i -o myproc.s ← 再生成 .s
# gcc -c myproc.s -o myproc.o ← 再生成 .o
# gcc myproc.o -o myproc ← 最后链接成可执行文件
理解依赖链:make 从第一个目标
myproc开始,发现它依赖myproc.o;而myproc.o又依赖myproc.s……于是一层层往下找,直到找到最底层的myproc.c,然后反过来一层层生成。这就像一个"堆栈"过程:先压栈找依赖,再出栈执行命令。
三、make 的工作原理
理解 make 的工作机制,是写好 Makefile 的关键。我们分点来讲。
3.1 make 的执行流程
功能
当你直接输入 make(不带参数)时,make 会按照以下步骤工作:
- 找文件:在当前目录下找名为
Makefile或makefile的文件 - 找目标:找到文件后,把第一个目标作为最终目标(默认目标)
- 查依赖:检查这个目标的依赖文件是否存在、是否比目标更新
- 递归生成:如果依赖文件不存在,就在 Makefile 中寻找生成该依赖的规则,递归处理
- 执行命令:所有依赖都就绪后,执行命令生成目标
- 出错退出:如果被依赖的文件找不到,make 会直接退出并报错
示例
——演示 make 的依赖查找与增量编译
# 假设 Makefile 内容是规则链版本(上面 2.2 的完整版)
# 第一次 make:所有文件都不存在,从头生成
make
# 输出:执行了 4 条 gcc 命令
# 文件都在了,再次 make
make
# 输出:make: 'myproc' is up to date.
# 什么都不做,因为都是最新的
# 只修改 myproc.c 文件
touch myproc.c # 用 touch 更新 myproc.c 的时间戳
# 再次 make
make
# 输出:
# gcc -E myproc.c -o myproc.i
# gcc -S myproc.i -o myproc.s
# gcc -c myproc.s -o myproc.o
# gcc myproc.o -o myproc
# 因为 myproc.c 更新了,所有依赖它的文件都要重新生成
# 只修改中间文件 myproc.s
touch myproc.s
make
# 输出:
# gcc -c myproc.s -o myproc.o ← 只重新生成 myproc.o 及之后
# gcc myproc.o -o myproc
# myproc.i 不用重新生成,因为 myproc.c 没变!
这就是增量编译的威力:make 会智能判断哪些文件需要重新编译,只做必要的操作,绝不做无用功。这就是它比手敲命令高效的核心原因。
3.2 文件时间戳:理解"是否需要更新"
功能
make 判断"是否需要重新编译"的依据是文件的时间戳(时间戳)。这里的核心是一个知识点:文件的三个时间。
| 时间 | 全称 | 含义 | 何时更新 |
| Access | 访问时间 | 文件最近一次被读取的时间 | 文件被访问时 |
| Modify | 修改时间 | 文件内容被修改的时间 | 内容变更时 |
| Change | 状态时间 | 文件属性(如权限)变更的时间 | 属性变更时 |
公式:文件 = 内容 + 属性。Modify 管内容变更,Change 管属性变更。
示例
——演示 stat 命令查看文件的时间戳
# 用 stat 命令查看文件的详细时间信息
stat myproc.c
# 输出示例:
# File: myproc.c
# Size: 89 Blocks: 8 IO Block: 4096 regular file
# Device: fd01h/64769d Inode: 1321125 Links: 1
# Access: (0664/-rw-rw-r--) Uid: ( 1000/ user) Gid: ( 1000/ user)
# Access: 2026-10-07 17:05:30.430619002 +0800 ← 访问时间
# Modify: 2026-10-07 17:05:25.940595116 +0800 ← 修改时间
# Change: 2026-10-07 17:05:25.940595116 +0800 ← 状态时间
# make 判断需要更新的核心:比较依赖文件和目标文件的 Modify 时间
# 如果 依赖的Modify时间 > 目标的Modify时间 → 需要重新生成
——演示用 touch 修改时间戳来触发重新编译
# 假设 myproc 已是最新,make 不会做任何事
make
# make: 'myproc' is up to date.
# touch 命令可以"更新文件时间"(不改内容,只改时间戳)
touch myproc.c
# 现在 myproc.c 的 Modify 时间比 myproc 新了
make
# 会重新编译!因为 make 发现依赖比目标新
实用技巧:
touch filename可以不改动文件内容,只更新时间戳,常用于"强制触发重新编译"。
3.3 .PHONY 伪目标
语法形式
.PHONY: 目标名
目标名:
命令
功能
clean 这类目标很特殊——它不生成任何文件,只是执行一些清理命令。这类目标叫伪目标(Phony Target)。
为什么需要 .PHONY?
如果目录里恰好有个叫 clean 的文件,make 会认为 clean 目标"已经存在且是最新的",于是不执行清理命令!用 .PHONY:clean 声明后,make 就会忽略文件比对,每次都执行这个目标。
.PHONY的作用:让 make 忽略"源文件和目标文件的 Modify 时间对比",让该目标总是被执行。
示例
——演示 clean 目标:项目清理
# Makefile
myproc:myproc.c
gcc -o myproc myproc.c
.PHONY:clean
clean:
rm -f myproc
# 编译
make
# gcc -o myproc myproc.c
# 查看目录
ls
# Makefile myproc myproc.c
# 清理项目(删除可执行文件)
make clean
# 输出:rm -f myproc
# 再次查看
ls
# Makefile myproc.c
# myproc 被删除了
——演示 .PHONY 的必要性(有同名文件时)
# 假设当前目录下有个叫 clean 的文件
touch clean
# 如果 Makefile 中没有 .PHONY:clean
ls
# Makefile clean myproc.c
make clean
# 输出:make: 'clean' is up to date.
# !清理命令根本没执行,因为 make 看到有个叫 clean 的文件,以为它是最新的
# 加上 .PHONY:clean 之后
# Makefile 内容:.PHONY:clean
make clean
# 输出:rm -f myproc
# 正常执行清理,因为 .PHONY 让 make 忽略文件比对
结论:像
clean这种不生成文件的目标,一定要用.PHONY声明,否则一旦目录中存在同名文件就会失效。
——演示 .PHONY 让目标总是被执行
# 把普通目标也声明成伪目标测试效果
.PHONY:myproc
myproc:myproc.c
gcc -o myproc myproc.c
make
# gcc -o myproc myproc.c
make
# gcc -o myproc myproc.c ← 注意:即使已是最新,也会重新执行!
# 因为 .PHONY:myproc 让它忽略时间比对,总是执行
# 这验证了"伪目标的特性是总是被执行"
四、Makefile 变量与自动化
当项目变大时,重复写 gcc、myproc 这些字符串会很啰嗦。Makefile 提供了变量和自动化变量来简化写法,让 Makefile 更简洁、更好维护。
4.1 自定义变量
语法形式
变量名 = 值 # 定义变量
$(变量名) # 使用变量
功能
Makefile 中的变量和 Shell 变量类似,用于存储字符串。用 $(变量名) 的方式引用。使用变量能让 Makefile 更容易修改(改一处即可全局生效)。
示例
——演示变量的定义与使用
# 定义变量
CC = gcc
BIN = myproc
SRC = myproc.c
# 使用变量
$(BIN):$(SRC)
$(CC) -o $(BIN) $(SRC)
.PHONY:clean
clean:
rm -f $(BIN)
好处:使用变量后,要修改编译器、目标名等信息,只需改定义那一行,不用在每条命令里查找替换。这在大型项目中非常重要。
4.2 wildcard 函数与替换
语法形式
$(wildcard 模式) # 获取匹配模式的所有文件名
$(变量:.旧后缀=.新后缀) # 将变量中所有旧后缀替换为新后缀
功能
- wildcard:自动获取当前目录下符合模式的所有文件(比如所有
.c文件),不用手动一个个列。
- 后缀替换:把文件列表中的后缀统一替换(比如把
.c列表替换成.o列表)。
示例
——演示 wildcard 获取所有 .c 文件,并替换为 .o 文件
# Makefile
CC = gcc
BIN = myproc
SRC = $(wildcard *.c) # 获取当前目录所有 .c 文件
OBJ = $(SRC:.c=.o) # 把所有 .c 替换成 .o
$(BIN):$(OBJ)
$(CC) -o $@ $^
%.o:%.c
$(CC) -c $<
.PHONY:clean
clean:
rm -f $(OBJ) $(BIN)
.PHONY:test
test:
@echo $(SRC)
@echo $(OBJ)
# 用 test 目标查看变量展开后的内容
make test
# 输出:
# myproc.c ← $(SRC) 自动获取了所有 .c 文件
# myproc.o ← $(OBJ) 自动把 .c 换成了 .o
# 如果目录里有多个 .c 文件
# SRC 会自动变成:a.c b.c c.c
# OBJ 会自动变成:a.o b.o c.o
# 完全不用手动修改 Makefile!
$(wildcard *.c)的妙用:项目里新增一个.c文件,Makefile 不用改,重新make就会自动把它纳入编译。这是大型项目 Makefile 的常见写法。
4.3 自动化变量 $@ $^ $<
语法形式
| 变量 | 含义 |
|
| 目标文件名(target) |
|
| 所有依赖文件列表(dependencies) |
|
| 第一个依赖文件(first dependency) |
功能
自动化变量是 Makefile 的"语法糖",能自动替换成当前规则中的目标名、依赖名,避免重复书写。
示例
——演示 $@ / $^ / $< 的含义与效果
# 不使用自动化变量(啰嗦写法)
myproc:myproc.o
gcc myproc.o -o myproc
# 这里的 myproc 写了两遍,myproc.o 也写了两遍
# 使用自动化变量(简洁写法)
myproc:myproc.o
gcc $^ -o $@
# $^ 自动替换为 myproc.o(所有依赖)
# $@ 自动替换为 myproc(目标)
# 展开后等价于:gcc myproc.o -o myproc
# $< 表示第一个依赖
myproc.o:myproc.c myproc.h
gcc -c $< -o $@
# $< 自动替换为 myproc.c(第一个依赖)
# $@ 自动替换为 myproc.o(目标)
# 展开后:gcc -c myproc.c -o myproc.o
make
# 输出:gcc -c myproc.c -o myproc.o
# gcc myproc.o -o myproc
——演示 @ 符号:不回显命令
# 命令前加 @ 可以"静默执行",不显示命令本身
test:
@echo "这条命令不会回显"
echo "这条命令会回显"
# 对比:
.PHONY:test
test:
@echo "只显示这行输出"
echo "命令和执行结果都显示"
make test
# 输出:
# 只显示这行输出
# echo "命令和执行结果都显示"
# 命令和执行结果都显示
# ↑ 加了 @ 的那行,命令本身不显示;没加的会显示命令
自动化变量记忆法:
$@→ @ 像"目标靶心",代表目标
$^→ ^ 像"一堆东西堆起来",代表所有依赖
$<→ < 像"箭头指向第一个",代表第一个依赖
4.4 模式规则 %.o:%.c
语法形式
%.o:%.c
命令
功能
% 是通配符(相当于"任意名字")。%.o:%.c 表示:"任意一个 .o 文件,都可以由同名的 .c 文件生成"。这样一条规则就能自动处理所有 .c → .o 的转换,不用为每个文件单独写规则。
示例
——演示模式规则自动处理多个文件
# Makefile
CC = gcc
BIN = myproc
SRC = $(wildcard *.c)
OBJ = $(SRC:.c=.o)
# 链接规则
$(BIN):$(OBJ)
$(CC) -o $@ $^
@echo "linking ... $^ to $@"
# 模式规则:任意 .o 由同名 .c 生成
%.o:%.c
$(CC) -c $<
@echo "compiling ... $< to $@"
.PHONY:clean
clean:
rm -f $(OBJ) $(BIN)
make
# 输出:
# compiling ... a.c to a.o
# compiling ... b.c to b.o
# linking ... a.o b.o to myproc
# 一条 %.o:%.c 规则,自动处理了所有 .c 文件的编译!
总结:
%.o:%.c+$(wildcard *.c)+ 自动化变量,是 Makefile 的"黄金三件套"。掌握它们,你的 Makefile 就能自动适应项目文件的增减,无需手动维护。
五、完整实战:一个通用的 Makefile
把前面学的知识串起来,这里给出一份经典的"通用 Makefile",可以直接用于大多数小型 C 项目:
# ===== 通用 Makefile 模板 =====
# 1. 定义变量
BIN = proc.exe # 目标可执行文件名
CC = gcc # 编译器
SRC = $(wildcard *.c) # 自动获取所有 .c 文件
OBJ = $(SRC:.c=.o) # 把 .c 列表替换成 .o 列表
LFLAGS = -o # 链接选项
FLAGS = -c # 编译选项
RM = rm -f # 删除命令
# 2. 链接规则(最终目标)
$(BIN):$(OBJ)
@$(CC) $(LFLAGS) $@ $^
@echo "linking ... $^ to $@"
# 3. 模式规则(编译 .c 到 .o)
%.o:%.c
@$(CC) $(FLAGS) $<
@echo "compiling ... $< to $@"
# 4. 清理
.PHONY:clean
clean:
$(RM) $(OBJ) $(BIN)
# 5. 测试(查看变量)
.PHONY:test
test:
@echo $(SRC)
@echo $(OBJ)
模板说明:
@让命令静默执行,只显示自定制的 echo 信息,界面更清爽
$(wildcard *.c)自动获取源文件,新增文件无需改 Makefile
$@、$^、$<自动化变量减少重复输入
六、常用命令与语法速查表
make 命令速查
| 命令 | 功能 |
|
| 执行第一个目标(默认目标) |
|
| 执行指定的目标 |
|
| 清理生成的文件 |
|
| 指定 Makefile 文件名(默认是 Makefile) |
|
| 只显示要执行的命令,不实际执行(预演) |
Makefile 语法速查
| 语法 | 说明 |
|
| 定义一条规则 |
|
| 命令必须用 Tab 缩进 |
|
| 声明伪目标,总是被执行 |
|
| 定义变量 |
|
| 引用变量 |
|
| 获取所有 .c 文件 |
|
| 后缀替换 |
|
| 模式规则 |
|
| 目标文件名 |
|
| 所有依赖文件 |
|
| 第一个依赖文件 |
|
| 不回显该命令 |
文件时间戳速查
| 时间 | 含义 |
| Access | 访问时间 |
| Modify | 内容修改时间(make 判断依据) |
| Change | 属性变更时间 |
|
| 查看文件详细时间信息 |
|
| 更新时间戳(不改内容) |
七、总结
本篇我们学习了 make 和 Makefile 的自动化构建,核心内容回顾:
- make 与 Makefile 简介:make 是命令工具,Makefile 是规则文件,二者搭配实现自动化编译
- 基本语法规则:
目标:依赖+ Tab 缩进的命令,构成一条规则
- 工作原理:找第一个目标 → 比较时间戳 → 递归查找依赖 → 增量编译
- 伪目标 .PHONY:让 clean 等目标总是被执行,避免同名文件干扰
- 变量与自动化:自定义变量(
CC、BIN)、wildcard函数、自动化变量($@、$^、$<)、模式规则(%.o:%.c)
学习建议:
- 理解"依赖关系 + 依赖方法"是 Makefile 的灵魂,多动手写几遍
- Tab 缩进是第一大坑,遇到 "missing separator" 先检查缩进
make clean和.PHONY是项目管理的标配,务必掌握
- 熟练后试试
$(wildcard *.c)+ 模式规则 + 自动化变量 这套组合,你的 Makefile 就能"以不变应万变"
下一篇我们将开始动手写 Linux 的第一个真正意义上的程序——进度条,把 GCC 和 Makefile 的知识真正用起来,敬请期待!
专栏目录:
- 第六篇(本篇):make 与 Makefile 自动化构建
- 第七篇:Linux 第一个系统程序——进度条(敬请期待)
- ...
如果本文对你有帮助,欢迎点赞收藏关注,你的支持是我持续创作的动力!
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/Xiao_running/article/details/167283104





