第一个Makefile:从零开始构建你的编译规则
各位同学,欢迎来到《Makefile内存泄漏检测实战》的第三讲。
前两章我们聊了内存泄漏的本质,也搭建好了开发环境。今天,咱们正式进入Makefile的世界。说实话,我见过太多工程师,代码写得漂亮,但一提到Makefile就头疼。其实Makefile没那么玄乎,它就是个自动化编译的脚本,说白了就是告诉编译器「先编译谁,再链接谁」。
我个人习惯,学任何工具都从最简单的例子入手。今天我们就写一个只有两个源文件的C程序,然后用Makefile把它管起来。
3.1 Makefile是什么?
Makefile是make工具读取的规则文件。make是个自动化构建工具,它根据文件的时间戳来判断哪些文件需要重新编译。
为什么要用Makefile?你想想看,一个项目几十个源文件,每次改一行代码都要全部重新编译?那不得等到天荒地老。Makefile只编译修改过的文件,这就是它的核心价值。
我在项目中遇到过这样的场景:一个同事手动编译,每次改完代码都执行gcc -o app *.c。项目小的时候还行,等文件到了50个,每次编译要3分钟。后来我帮他写了个Makefile,增量编译只需要10秒。他当场就让我教他写Makefile。
3.2 Makefile的基本语法
一个Makefile由若干条规则组成。每条规则的格式如下:
目标: 依赖
命令
注意:命令前面必须是一个Tab键,不能是空格。这是新手最容易踩的坑,我曾经因为这个Tab问题排查了半小时,最后发现是编辑器把Tab自动转成了空格。
我们来拆解一下这三个部分:
- 目标:通常是要生成的文件名,比如可执行文件、目标文件(.o)
- 依赖:生成目标所需要的文件,可以是源文件、头文件或其他目标文件
- 命令:如何从依赖生成目标,通常是gcc、g++等编译命令
核心思想:Makefile就是告诉make工具——「如果依赖比目标新,就重新执行命令生成目标」。
3.3 一个简单的编译示例
假设我们有三个文件:main.c、utils.c、utils.h。
main.c
#include <stdio.h>
#include "utils.h"
int main() {
int result = add(3, 5);
printf("3 + 5 = %d\n", result);
return 0;
}
utils.h
#ifndef UTILS_H
#define UTILS_H
int add(int a, int b);
#endif
utils.c
#include "utils.h"
int add(int a, int b) {
return a + b;
}
现在,我们来写第一个Makefile:
# 第一个Makefile
app: main.o utils.o
gcc -o app main.o utils.o
main.o: main.c utils.h
gcc -c main.c -o main.o
utils.o: utils.c utils.h
gcc -c utils.c -o utils.o
clean:
rm -f app *.o
这个Makefile定义了4条规则:
- app:最终的可执行文件,依赖main.o和utils.o
- main.o:目标文件,依赖main.c和utils.h
- utils.o:目标文件,依赖utils.c和utils.h
- clean:清理规则,没有依赖,直接执行删除命令
小技巧:把头文件也写在依赖里,这样当头文件修改时,对应的.o文件会自动重新编译。我早期写Makefile经常漏掉头文件依赖,导致改了头文件但没重新编译,排查了半天才发现是Makefile的问题。
3.4 执行Makefile
在终端执行:
$ make
gcc -c main.c -o main.o
gcc -c utils.c -o utils.o
gcc -o app main.o utils.o
make工具会做以下事情:
- 读取Makefile,找到第一个目标(默认目标)——这里是app
- 检查app的依赖main.o和utils.o是否存在
- 如果依赖不存在,递归地去执行生成这些依赖的规则
- 如果依赖存在,比较依赖和目标的时间戳
- 如果依赖比目标新,重新执行命令
如果我们再次执行make:
$ make
make: 'app' is up to date.
因为所有文件都没有修改,所以make告诉你「已经是最新的了」。
如果我们修改了main.c,再执行make:
$ make
gcc -c main.c -o main.o
gcc -o app main.o utils.o
只重新编译了main.o,然后重新链接。utils.o没有变化,所以跳过了。
注意:如果只修改了utils.h,那么main.o和utils.o都会重新编译,因为它们都依赖utils.h。这就是为什么要把头文件写在依赖里的原因。
3.5 知识体系图
下面这张图展示了Makefile的核心逻辑:
3.6 避坑指南
我刚开始用Makefile时,踩过不少坑。这里分享几个常见的:
- Tab vs 空格:命令前面必须是Tab,不能用空格。如果报错「missing separator」,99%是这个原因。
- 文件名不要有空格:Makefile对文件名中的空格处理很麻烦,尽量用下划线代替。
- 默认目标:make默认执行第一个目标。如果你想让clean作为默认目标,记得把它放在最前面。
- 注释用#:Makefile的注释以#开头,不支持//或/* */。
核心总结:Makefile的本质就是「目标-依赖-命令」的三元组。你只需要告诉make三件事——要生成什么、依赖什么、怎么生成。剩下的时间戳比较、增量编译,make都帮你搞定。
好了,今天的课程就到这里。这个简单的Makefile虽然只有几行,但它包含了Makefile的全部核心思想。下一章我们会在这个基础上,加入变量、自动变量、模式规则等进阶内容,让Makefile变得更强大、更优雅。
可可精品资料库