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.c、utils.c、utils.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.o,utils.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的工作流程,我画了一张图:
伪目标: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