章节一:Make简介与Makefile编写
在上一章,我们还在手动输入 gcc main.c -o main。如果项目有 100 个源文件,手动编译不仅是“体力活”,更是“精细活”——改动一个文件,该重编哪些?漏掉一个怎么办?make 工具因此诞生。它不仅能一键完成所有工作,还能聪明地只处理“变动过”的部分。
1.0 安装 Make/CMake
在 Linux 上,可以使用包管理器安装:
sudo apt update
sudo apt install build-essential cmakeWindows / MacOS 端的安装稍复杂,可自行搜索相关教程。在我们的实验中,只需要 Linux 端的 Make/CMake
安装完成后,你可以在命令行输入 make --version 和 cmake --version 来验证安装是否成功。
1.1 为什么需要 Make?
在大型 C/C++ 项目中,通常会有许多源文件。
- 自动化:一键完成编译、链接甚至打包。
- 增量编译:
make会检查文件的修改时间。如果func.c没变,它就不会重新编译func.o,大大节省了大型项目的构建时间。
1.2 Makefile 核心语法
make 本身并不知道如何编译 C 语言。你需要通过一个名为 Makefile 的文本文件(无拓展性)告诉它规则。
Makefile 的核心是规则,其格式如下:
target: dependencies
command
- Target (目标):你想生成的东西(如
main程序、tool.o文件)。也可以是一个动作名(如clean)。 - Dependencies (依赖):生成目标需要哪些“原材料”(源文件或其他目标)。
- Command (命令):具体执行的命令(通常是
gcc命令)。
Warning
Makefile 诞生于 1976 年,当时的作者为了方便,规定命令前必须是 \t (Tab)。这个“设计缺陷”保留了 50 年。 所以:命令前必须是一个 Tab 字符,不能是空格!
在同一个 Makefile 中可以定义多个目标,你可以通过运行 make <targetname> 来生成名称为 <targetname> 的目标,或者运行 make,它会运行文件中第一个 Target 为文件而非动作的目标。
Makefile 通过执行你在目标下面定义的 command 来生成目标,当它发现 dependencies 缺失,它会首先寻找生成 dependencies 的目标。
示例:
假设项目结构如下:
main.c(调用了tool.h中的函数)tool.c(函数的实现)tool.h(函数声明)
如下是一个 Makefile 文件:
# 1. 最终目标(规则 A):将两个 .o 文件链接成可执行文件
main: main.o tool.o
gcc -o main main.o tool.o
# 2. 中间目标(规则 B):将 main.c 编译成 main.o
# 注意:如果 tool.h 变了,main.o 也必须重编,因为它 #include 了 tool.h
main.o: main.c tool.h
gcc -c main.c
# 3. 中间目标(规则 C):将 tool.c 编译成 tool.o
tool.o: tool.c tool.h
gcc -c tool.c
# 4. 伪目标:清理现场
# 执行 'make clean' 即可删除所有中间产物,还你一个干净的目录
.PHONY: clean
clean:
rm -f main *.o
编译过程
当你输入 make 时,幕后发生了什么?
make看到第一个目标是main,它发现main依赖main.o和tool.o。- 它去找
main.o,发现有规则 B。它检查main.c和tool.h是否比main.o新。如果是,执行gcc -c main.c。 - 它再去处理
tool.o,发现有规则 C。同样检查时间戳,必要时执行gcc -c tool.c。 - 当“原材料”
main.o和tool.o都准备好(或更新完)后,它最后执行规则 A 里的链接命令。
递归检查
这里的核心逻辑是:递归检查
当你告诉 make 去生成 target 时,它会启动一个递归过程:
- 检查依赖是否存在? 如果
dependencies里的某个文件不存在,make会在 Makefile 中寻找是否有其他规则可以生成这个“原材料”。 - 检查是否需要更新? 如果
dependencies中任何一个文件的“最后修改时间”比target更新,make就会重新执行command。
目标名为 target 代表生成该目标会得到名为 target 的文件,但有时我们希望目标不生成文件,而是执行例如清理文件夹的工作。这是通过 .PHONY: Target 实现的,例如:
.PHONY: clean
clean:
rm -f main *.o
这里的 clean 就是动作名而非文件名,make clean 不会生成 clean 文件而是清理工作区。
如果你不小心定义了
.PHONY: main
main: main.o tool.o
gcc -o main main.o tool.o
那么 make main 就作为动作执行,由于 make 不知道 main 是一个文件名,
所以即使 main.o, tool.o 在上次执行后没有任何变化,
make 也还是会重新执行 gcc -o main main.o tool.o
1.3 变量与自动变量
在上面的例子中,如果我们想把编译器从 gcc 换成 clang,或者增加编译选项 -O2(优化级别),我们需要修改每一行命令。这显然不符合程序员“偷懒”的美德。我们可以使用变量和模式匹配来简化。
1.3.1 使用变量 (Variables)
变量就像 C 语言里的宏,方便统一修改。
CC = gcc # 指定编译器
CFLAGS = -Wall -g # 编译选项:显示所有警告,保留调试信息
TARGET = main # 最终产物名
OBJS = main.o tool.o # 对象文件列表
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) -o $(TARGET) $(OBJS)
Tip
变量名通常约定俗成用大写。CC 代表 C Compiler,CFLAGS 代表 C Flags(编译参数)。
1.3.2 模式规则与自动变量 (Pattern Rules)
对于几十个 .c 文件,我们不可能每个都写一遍规则。Makefile 提供了一种通配符 %(类似于正则表达式中的 *)。
在模式规则中,由于文件名是动态确定的,我们无法写死文件名。因此,Makefile 提供了三个自动变量:
$@:代表当前的目标(Target)。比如上面的main.o。$<:代表第一个依赖文件。比如上面的main.c。$^:代表所有的依赖文件(以空格分隔)。
# 这里的 % 相当于通配符。这条规则的意思是:
# 任何一个 .o 文件都依赖于对应的 .c 文件
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
发生了什么
假设 make 现在正在处理 main.o 这个目标:
- 模式匹配:
%.o匹配到了main.o,于是%就是main。 - 依赖推导:
%.c自动变成了main.c。 - 变量代换:
$@(目标) ->main.o$<(第一个依赖) ->main.c
- 最终生成的命令:
gcc -Wall -g -c main.c -o main.o
1.3.3 演示
我们将 1.2.2 的冗长代码简化为“工业级”写法:
CC = gcc
CFLAGS = -Wall -g
TARGET = main
OBJS = main.o tool.o
# 链接阶段:使用 $^ 包含所有 .o 文件
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) -o $@ $^
# 编译阶段:使用模式规则
# 对于每一个 .o 文件,都去找对应的 .c 文件
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
1.4 Q&A
Q: 我输入
make提示make: 'main' is up to date.是什么意思?- A: 这说明你自上次编译以来没改过代码。如果你非要重编,可以先
make clean再make。
- A: 这说明你自上次编译以来没改过代码。如果你非要重编,可以先
Q: 为什么提示
missing separator. Stop.?- A: 检查一下!你的命令前面是不是用了空格而不是 Tab?
Q: 为什么头文件
.h不需要写在gcc -c命令里,却要写在依赖列表里?- A: 编译器会自动去找
#include的头文件,但make需要你显式告诉它:如果.h变了,对应的.o也得重编。
- A: 编译器会自动去找