84、Makefile 基础:如何编写一个简单的 Makefile?

说实话,很多学C语言的朋友,写代码溜得很,一提到Makefile就头大。我当年刚入行时也是这样,觉得反正IDE一键编译,学这玩意儿干嘛?直到有一次在嵌入式项目里,IDE死活配置不对,我硬着头皮手写了一个Makefile,才发现——原来这东西这么香。

今天咱们就来聊聊,一个最简单的Makefile该怎么写。别怕,真的不难。

为什么需要Makefile?

你想想看,一个项目里少说几十个源文件。每次改一行代码,难道要把所有文件重新编译一遍?那太傻了。Makefile的核心作用就是:只重新编译那些被修改过的文件

它通过比较源文件和目标文件的时间戳,来判断哪些需要重新编译。说白了,就是帮你省时间。

核心思想: 如果源文件比目标文件新,就重新编译;否则跳过。

一个最简单的Makefile

咱们先从一个单文件的例子开始。假设你有一个 hello.c

# 最简单的Makefile
hello: hello.c
    gcc -o hello hello.c

嗯,就这么几行。解释一下:

  • hello 是目标(target),也就是最终要生成的可执行文件
  • hello.c 是依赖(prerequisites),编译需要它
  • 第二行是命令(recipe),前面必须有一个Tab缩进

注意: 命令前面必须是Tab键,不能用空格!这是新手最容易踩的坑。我曾经因为这个bug排查了半小时,最后发现是编辑器把Tab自动转成了空格……

多文件项目的Makefile

实际项目里,文件肯定不止一个。比如你有 main.cutils.cutils.h。这时候Makefile可以写成这样:

# 多文件Makefile
myapp: main.o utils.o
    gcc -o myapp main.o utils.o

main.o: main.c utils.h
    gcc -c main.c

utils.o: utils.c utils.h
    gcc -c utils.c

clean:
    rm -f *.o myapp

这里我用了 -c 选项,只编译不链接,生成 .o 目标文件。最后再统一链接成可执行文件。这样做的好处是:如果你只改了 main.c,那只需要重新编译 main.outils.o 直接复用。

用变量让Makefile更灵活

硬编码文件名不是个好习惯。我建议用变量来管理:

# 使用变量的Makefile
CC = gcc
CFLAGS = -Wall -g
TARGET = myapp
OBJS = main.o utils.o

$(TARGET): $(OBJS)
    $(CC) -o $@ $^

%.o: %.c
    $(CC) $(CFLAGS) -c $< -o $@

clean:
    rm -f $(OBJS) $(TARGET)

这里有几个自动变量,我解释一下:

  • $@ 表示目标文件名
  • $^ 表示所有依赖文件
  • $< 表示第一个依赖文件

你看,这样改起来就方便多了。想换编译器?改 CC 就行。想加优化选项?改 CFLAGS 就行。

Makefile的核心逻辑

为了让你更直观地理解Makefile的工作流程,我画了一张图:

Makefile 工作流程 源文件 (.c) 头文件 (.h) 编译 (gcc -c) 目标文件 (.o) 链接 (gcc -o) 可执行文件 Makefile 通过比较 .c/.h 和 .o 的时间戳,决定哪些文件需要重新编译 只编译修改过的文件,未修改的直接复用已有的 .o 文件 源文件时间戳 > 目标文件时间戳 → 重新编译

伪目标:clean和all

你注意到上面例子里的 clean 了吗?它不是一个真正的文件,而是一个伪目标。为了防止目录下恰好有个叫 clean 的文件导致命令不执行,我习惯这样写:

.PHONY: clean all

all: $(TARGET)

clean:
    rm -f $(OBJS) $(TARGET)

.PHONY 告诉Make:这些目标不是真正的文件,不管有没有同名文件,都执行命令。

一个完整的模板

最后,我给你一个我常用的模板,直接拿去用:

# 通用Makefile模板
CC = gcc
CFLAGS = -Wall -O2 -g
LDFLAGS =
TARGET = myapp
SRCS = $(wildcard *.c)
OBJS = $(SRCS:.c=.o)

.PHONY: all clean

all: $(TARGET)

$(TARGET): $(OBJS)
    $(CC) $(LDFLAGS) -o $@ $^

%.o: %.c
    $(CC) $(CFLAGS) -c $< -o $@

clean:
    rm -f $(OBJS) $(TARGET)

这里用了 $(wildcard *.c) 自动获取所有 .c 文件,再用 $(SRCS:.c=.o) 把后缀替换成 .o。这样你新增文件时,Makefile 完全不用改。

小技巧: 如果你在嵌入式开发中,交叉编译器通常叫 arm-none-eabi-gcc 之类的,直接把 CC 变量改成对应的就行。我在STM32项目里就是这么干的,省事。

常见错误与避坑

  • Tab vs 空格: 命令前必须是Tab。我建议你在编辑器里设置「显示空白字符」,一眼就能看出来。
  • 依赖没写全: 如果改了头文件,但Makefile里没写头文件依赖,那Make不会重新编译。我吃过这个亏,排查了半天才发现是头文件改了但没触发重编。
  • 变量引用:$(变量名) 而不是 $变量名。后者只对单字符变量名有效。

好了,这就是Makefile最基础的内容。你把它练熟了,后面再学条件判断、函数、多目录嵌套这些高级特性,就会轻松很多。记住,Makefile的核心就三件事:目标、依赖、命令。把这三点搞明白,你就入门了。


公众号:蓝海资料掘金营,微信deep3321