第一个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工具会做以下事情:

  1. 读取Makefile,找到第一个目标(默认目标)——这里是app
  2. 检查app的依赖main.o和utils.o是否存在
  3. 如果依赖不存在,递归地去执行生成这些依赖的规则
  4. 如果依赖存在,比较依赖和目标的时间戳
  5. 如果依赖比目标新,重新执行命令

如果我们再次执行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的核心逻辑:

Makefile核心逻辑流程图 main.c utils.c utils.h main.o utils.o app 编译阶段:.c + .h → .o 链接阶段:.o → 可执行文件 虚线表示头文件依赖

3.6 避坑指南

我刚开始用Makefile时,踩过不少坑。这里分享几个常见的:

  • Tab vs 空格:命令前面必须是Tab,不能用空格。如果报错「missing separator」,99%是这个原因。
  • 文件名不要有空格:Makefile对文件名中的空格处理很麻烦,尽量用下划线代替。
  • 默认目标:make默认执行第一个目标。如果你想让clean作为默认目标,记得把它放在最前面。
  • 注释用#:Makefile的注释以#开头,不支持//或/* */。

核心总结:Makefile的本质就是「目标-依赖-命令」的三元组。你只需要告诉make三件事——要生成什么、依赖什么、怎么生成。剩下的时间戳比较、增量编译,make都帮你搞定。

好了,今天的课程就到这里。这个简单的Makefile虽然只有几行,但它包含了Makefile的全部核心思想。下一章我们会在这个基础上,加入变量、自动变量、模式规则等进阶内容,让Makefile变得更强大、更优雅。

可可精品资料库