Linux Makefile

一、Makefile 基础概念

1. 什么是 Makefile?

很多新手刚接触Linux编程时,都会手动输入 gcc main.c -o main 这样的编译命令,可一旦项目文件变多(比如10个、20个.c文件),手动输入命令不仅麻烦,还容易出错。

而Makefile,就是一份「自动化构建脚本」,配合 make 命令使用,能自动识别文件依赖关系、只编译修改过的文件,大幅提升构建效率,避免重复劳动。

简单说:Makefile = 定义依赖 + 指定规则,让编译变得"自动化、高效化"。

2. 核心适用场景

知道了Makefile是什么,再看看它能用到哪些地方,避免学了用不上:

  • 最常用:C/C++ 项目编译(单文件、多文件都适用)
  • 嵌入式开发:内核编译、驱动开发(嵌入式工程师必备)
  • 大型项目:多目录、多文件项目的自动化构建(避免手动管理依赖)

3. 基本格式

Makefile的核心是「规则」,每一条规则都遵循固定格式,这是入门的基础,一定要记牢!

makefile 复制代码
目标: 依赖文件
[TAB] 命令     # 必须以 TAB 开头,不能用空格!(踩坑重点)

拆解说明(通俗好记):

  • 目标:你要生成的文件(比如可执行文件app、中间文件main.o),也可以是"伪目标"(比如clean,不生成文件,只执行命令)。
  • 依赖:生成这个目标,需要哪些文件(比如生成app,需要main.o和func.o)。
  • 命令:生成目标的具体操作(比如gcc编译、rm删除文件),必须以TAB开头,用空格会直接报错(新手最容易踩的坑)。

二、Makefile 核心规则

光记格式没用,结合实际案例,才能快速上手。下面从最简单的单文件,到多文件,一步步演示,新手可以跟着敲一遍,加深印象。

1. 最简单的 Makefile(单文件场景)

假设你有一个单文件main.c,要生成可执行文件main,Makefile可以这么写:

makefile 复制代码
# 注释:生成 main 可执行文件,依赖 main.c(注释用#开头)
main: main.c
	gcc main.c -o main  # 编译命令,前面是TAB(必须保留)

使用方法(终端执行):

  • 编译:直接输入 make,会自动执行第一条规则,生成main文件。
  • 清理:输入 make clean,会执行clean规则,删除main文件。

备注:如果你的文件不是main.c,比如test.c,只需要把"main: main.c"改成"test: test.c",命令改成"gcc test.c -o test"即可。

2. 多文件编译(核心常用场景)

实际开发中,很少有单文件项目,比如我们有main.c(主函数)和func.c(自定义函数),要生成可执行文件app,Makefile如下:

makefile 复制代码
# 最终目标:生成 app 可执行文件,依赖 main.o 和 func.o
app: main.o func.o
	gcc main.o func.o -o app  # 链接两个.o文件,生成app

# 生成 main.o 文件,依赖 main.c
main.o: main.c
	gcc -c main.c  # -c 表示只编译,不链接,生成.o中间文件

# 生成 func.o 文件,依赖 func.c
func.o: func.c
	gcc -c func.c

# 清理所有生成的文件(.o文件和app)
clean:
	rm -f *.o app  # *.o 匹配所有.o文件

核心逻辑:先把每个.c文件编译成.o中间文件,再把所有.o文件链接起来,生成最终的可执行文件。这样做的好处是,当你只修改了某个.c文件(比如func.c),make会只重新编译func.o,而不是所有文件,提升效率。


三、Makefile 变量(简化代码)

看上面的多文件Makefile,你会发现有很多重复的内容(比如gcc、main.o func.o),如果文件多了,修改起来很麻烦。这时候,变量就派上用场了------把重复的内容定义成变量,后续修改只需要改变量的值即可。

1. 变量定义与使用

变量的定义和使用非常简单,格式如下:

  • 定义:变量名 = 值(比如 CC = gcc)
  • 使用:$(变量名)(比如 $(CC) 就等于 gcc)

用变量优化后的多文件Makefile:

makefile 复制代码
# 定义常用变量(一目了然,方便修改)
CC = gcc          # 编译器(如果换clang,只改这里)
CFLAGS = -Wall -g # 编译选项:-Wall显示所有警告,-g生成调试信息
TARGET = app      # 最终生成的程序名
OBJ = main.o func.o # 目标文件列表(所有.o文件)

# 最终目标:用变量替换重复内容
$(TARGET): $(OBJ)
	$(CC) $(OBJ) -o $(TARGET) $(CFLAGS)

# 生成 .o 文件
main.o: main.c
	$(CC) -c main.c $(CFLAGS)

func.o: func.c
	$(CC) -c func.c $(CFLAGS)

# 清理文件
clean:
	rm -f $(OBJ) $(TARGET)

优势:比如你想把编译器换成clang,只需要把 CC = gcc 改成 CC = clang,不需要修改所有命令;想改程序名,只改TARGET的值即可。

2. 变量赋值方式(重点,面试常考)

变量的赋值方式有4种,用法和含义不同,一定要区分清楚,避免踩坑:

赋值符号 用法示例 核心含义 通俗解释
= A = 10<br>B = $(A) 递归赋值,延迟展开 B的值不是当前A的值,而是最后一次给A赋值的值(可能被后续修改)
:= A := 10<br>B := $(A) 直接赋值,立即展开 B的值就是当前A的值,后续A修改,B不会变(最常用)
?= A ?= 10 条件赋值 只有A没被定义过,才会赋值10;如果A已经有值,不做任何操作
+= CFLAGS = -Wall<br>CFLAGS += -g 追加赋值 在变量原有值的基础上,追加新的内容(比如给编译选项加新参数)

备注:日常开发中,:=(直接赋值)和 +=(追加赋值)用得最多,建议优先掌握。


四、自动变量(简化命令,必学重点)

用了变量之后,代码已经简化了很多,但还有可以优化的地方。比如上面的 $(CC) $(OBJ) -o $(TARGET),其实可以用「自动变量」进一步简化------自动变量会自动匹配规则中的目标和依赖,不用手动写变量名。

注意:自动变量只能用在规则的命令中,不能用在目标或依赖里。

常用自动变量(4个就够日常用)

自动变量 核心含义 用法示例
$@ 当前规则的目标文件名 规则 app: main.o func.o 中,$@ 就是 app
$^ 当前规则的所有依赖文件 规则 app: main.o func.o 中,$^ 就是 main.o func.o
$< 当前规则的第一个依赖文件 规则 main.o: main.c 中,$< 就是 main.c
$? 比当前目标更新的所有依赖文件 只编译修改过的依赖文件,提升效率(较少直接用)

用自动变量优化后的极简 Makefile

makefile 复制代码
CC = gcc
CFLAGS = -Wall -g
TARGET = app
OBJ = main.o func.o

# 用 $@ 代替 TARGET,$^ 代替 OBJ
$(TARGET): $(OBJ)
	$(CC) $^ -o $@ $(CFLAGS)

# 通配符规则:所有 .o 文件都依赖对应的 .c 文件(重点优化)
%.o: %.c
	$(CC) -c $< -o $@ $(CFLAGS)  # $< 代替 对应的.c文件

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

对比之前的版本,是不是简洁了很多?尤其是 %.o: %.c 这一行,直接匹配所有.o文件和对应的.c文件,不用再为每个.c文件写一条规则------如果有10个.c文件,这一行就能搞定,极大简化代码。


五、模式规则与通配符(简化多文件编译)

上面用到的 %.o: %.c 就是「模式规则」,配合「通配符」,能实现"万能编译",不用手动管理每个文件的依赖,这也是Makefile的核心技巧之一。

1. 常用通配符(2个就够)

  • *:匹配任意字符(比如 *.c 匹配当前目录下所有.c文件,*.o 匹配所有.o文件)。
  • %:模式匹配(Makefile专用,用于模式规则,比如 %.o: %.c 表示"所有以.o结尾的文件,都依赖同名的.c文件")。

注意:* 是"全局匹配",而 % 是"模式匹配",只能用在模式规则中,两者不能混用。

2. 模式规则(万能编译模板)

模式规则的核心是「批量匹配」,最常用的就是下面这一行,几乎所有单目录C项目都能用:

makefile 复制代码
# 所有 .o 文件 依赖 同名 .c 文件,统一编译(万能模板)
%.o: %.c
	gcc -c $< -o $@ $(CFLAGS)

解释:不管你有多少个.c文件,只要执行make,就会自动为每个.c文件生成对应的.o文件,不用手动写每个.o文件的规则。比如有a.c、b.c、c.c,会自动生成a.o、b.o、c.o。


六、伪目标(.PHONY)------ 避免踩坑的关键

不知道大家有没有遇到过这样的问题:写了clean规则后,执行 make clean 却报错,提示"clean is up to date"。这是因为你的目录下,有一个名叫"clean"的文件------make会把clean当作"目标文件",而不是"命令标签",所以会报错。

解决方案:用 (.PHONY) 声明伪目标,告诉make"这个目标不是文件,只是一个命令标签"。

伪目标的正确用法

makefile 复制代码
# 声明伪目标(推荐把所有命令型目标都放这里)
.PHONY: clean all

# all 是默认构建目标(执行make时,会先执行all)
all: $(TARGET)

# 清理文件
clean:
	rm -f $(OBJ) $(TARGET)

重点说明:

  • (.PHONY) 后面可以跟多个伪目标,用空格分隔(比如上面的clean和all)。
  • all 是最常用的伪目标,作为默认构建目标------执行 make 时,会自动执行all规则,而all依赖$(TARGET),所以会先编译生成目标文件。
  • 推荐所有"不生成文件"的目标(比如clean、all、install),都用 (.PHONY) 声明,避免和目录下的文件重名。

七、Makefile 常用函数(自动化管理文件)

如果项目文件很多,手动写 OBJ = main.o func.o a.o b.o 还是很麻烦。这时候,Makefile的内置函数就能帮我们自动识别文件,进一步简化代码。

函数的通用格式:$(函数名 参数1,参数2,...)(参数之间用逗号分隔,没有空格)。

1. wildcard 函数------自动匹配文件

作用:匹配指定路径下的所有符合条件的文件,返回文件列表。

makefile 复制代码
# 获取当前目录下所有 .c 文件(自动识别,不用手动写)
SRC = $(wildcard *.c)

比如当前目录有main.c、func.c、test.c,那么 $(SRC) 就等于 main.c func.c test.c

2. patsubst 函数------替换文件名

作用:将文件列表中的文件名,按照指定格式替换(最常用:把.c文件替换成.o文件)。

makefile 复制代码
# 把所有 .c 文件替换为 .o 文件(自动生成目标文件列表)
OBJ = $(patsubst %.c, %.o, $(SRC))

解释:%.c 是原格式,%.o 是目标格式,(SRC)是要替换的文件列表。比如(SRC) 是要替换的文件列表。比如(SRC)是要替换的文件列表。比如(SRC)是main.c func.c,替换后就是main.o func.o。

3. 终极通用 Makefile(自动识别所有C文件)

结合上面两个函数,我们可以写出一个"万能模板",不管你有多少个.c文件,都能自动编译、自动清理,直接复制到项目中就能用!

makefile 复制代码
CC = gcc
CFLAGS = -Wall -g
TARGET = app

# 自动获取当前目录所有.c文件
SRC = $(wildcard *.c)
# 自动将所有.c文件替换为.o文件
OBJ = $(patsubst %.c, %.o, $(SRC))

# 构建目标
$(TARGET): $(OBJ)
	$(CC) $^ -o $@ $(CFLAGS)

# 模式规则:自动编译所有.o文件
%.o: %.c
	$(CC) -c $< -o $@ $(CFLAGS)

# 伪目标声明
.PHONY: clean
# 清理文件
clean:
	rm -f $(OBJ) $(TARGET)

✅ 重点备注:这个模板可以直接用于「任意单目录C项目」,不管你有1个还是100个.c文件,执行 make 就能编译,make clean 就能清理,完全不用修改!


八、条件判断(控制编译流程,实战必备)

实际开发中,我们经常需要"调试版"和"发行版"两种编译模式:调试版需要生成调试信息(-g),方便调试;发行版需要优化编译(-O2),提升运行效率。这时候,条件判断就能帮我们实现"一键切换"。

条件判断的常用写法

makefile 复制代码
CC = gcc
CFLAGS = -Wall

# 条件判断:如果DEBUG=1,编译调试版;否则编译发行版
ifeq ($(DEBUG), 1)
CFLAGS += -g  # 调试版:添加调试信息
else
CFLAGS += -O2  # 发行版:优化编译
endif

TARGET = app
SRC = $(wildcard *.c)
OBJ = $(patsubst %.c, %.o, $(SRC))

$(TARGET): $(OBJ)
	$(CC) $^ -o $@ $(CFLAGS)

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

.PHONY: clean
clean:
	rm -f $(OBJ) $(TARGET)

使用方法:

  • 编译调试版:make DEBUG=1(生成的app包含调试信息,可用于gdb调试)。
  • 编译发行版:直接输入 make(生成的app经过优化,运行更快)。

备注:条件判断还有其他写法(比如ifneq、ifdef),但日常开发中,ifeq用得最多,掌握这一种就够了。


九、多目录项目 Makefile(进阶用法)

当项目规模变大时,我们通常会把文件分类放在不同目录(比如src/放源代码、inc/放头文件、obj/放中间.o文件、bin/放可执行文件),这样项目结构更清晰。这时候,就需要写多目录的Makefile。

多目录项目结构(推荐)

bash 复制代码
project/          # 项目根目录
├── src/          # 源代码目录(.c文件)
│   ├── main.c
│   └── func.c
├── inc/          # 头文件目录(.h文件)
│   └── func.h
├── obj/          # 中间文件目录(.o文件)
├── bin/          # 可执行文件目录
└── Makefile      # 核心Makefile

多目录 Makefile 写法

makefile 复制代码
# 目录定义(清晰明了,方便修改)
SRC_DIR = src
INC_DIR = inc
OBJ_DIR = obj
BIN_DIR = bin

# 编译器和编译选项(-I 指定头文件目录,避免头文件找不到)
CC = gcc
CFLAGS = -Wall -g -I$(INC_DIR)

# 自动获取src目录下所有.c文件
SRC = $(wildcard $(SRC_DIR)/*.c)
# 自动将src目录下的.c文件,替换为obj目录下的.o文件
OBJ = $(patsubst $(SRC_DIR)/%.c, $(OBJ_DIR)/%.o, $(SRC))
# 最终可执行文件路径(放在bin目录下)
TARGET = $(BIN_DIR)/app

# 构建目标:生成bin目录下的app
$(TARGET): $(OBJ)
	@mkdir -p $(BIN_DIR)  # 若bin目录不存在,自动创建(@表示不打印命令)
	$(CC) $^ -o $@ $(CFLAGS)

# 模式规则:生成obj目录下的.o文件
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
	@mkdir -p $(OBJ_DIR)  # 若obj目录不存在,自动创建
	$(CC) -c $< -o $@ $(CFLAGS)

# 伪目标声明
.PHONY: clean
# 清理文件:删除obj和bin目录(递归删除)
clean:
	rm -rf $(OBJ_DIR) $(BIN_DIR)

核心要点:

  • -I$(INC_DIR):指定头文件目录,告诉编译器去哪里找.h文件(避免报"头文件找不到"错误)。
  • @mkdir -p $(BIN_DIR)@ 表示不打印命令,-p 表示如果目录不存在,就创建(避免手动创建目录)。
  • 所有中间文件(.o)都放在obj目录,可执行文件放在bin目录,项目根目录更整洁,方便管理。

十、make 命令常用参数(日常必备)

写好Makefile后,配合make命令的参数,可以更灵活地控制编译过程。下面是最常用的6个参数,记下来就能直接用:

命令参数 核心作用 使用场景
make 执行Makefile,构建第一个目标 日常编译,最常用
make clean 执行clean目标,清理生成的文件 编译出错、重新编译时
make -n 只打印命令,不执行(测试) 检查Makefile是否有语法错误
make -f 文件名 指定非默认Makefile(比如Makefile.test) 一个项目有多个Makefile时
make -jN 多线程编译(N是线程数,比如-j4) 大型项目,加速编译(缩短时间)
make V=1 显示完整编译命令 编译出错时,排查错误原因

备注:多线程编译 make -j4 非常实用,比如4核CPU,用-j4可以同时编译4个文件,大幅缩短编译时间。


十一、Makefile 执行流程(理解原理,避免踩坑)

很多新手只知道"make就能编译",但不知道make是怎么工作的,遇到问题就无从下手。其实Makefile的执行流程很简单,记住5步即可:

  1. make 命令执行后,会先在当前目录查找 Makefilemakefile(大小写敏感)。
  2. 找到后,解析文件中的「第一个目标」(通常是all或可执行文件),将其作为「最终目标」。
  3. 递归查找最终目标的依赖文件,判断依赖文件是否比目标文件更新(比如.c文件修改时间比.o文件晚)。
  4. 如果依赖文件更新,就执行对应的命令,生成目标文件;如果没有更新,就跳过(避免重复编译)。
  5. 按顺序执行所有需要的命令,直到生成最终目标;如果执行过程中遇到错误(比如语法错误、编译错误),立即停止执行。

举个例子:执行 make 后,make会先找第一个目标all,all依赖app,app依赖main.o和func.o,然后判断main.o是否比main.c旧,如果旧,就编译main.c生成main.o,同理处理func.o,最后链接所有.o文件生成app。


十二、常见错误总结

整理了新手最容易踩的5个坑,每个坑都标注了原因和解决方案,遇到问题直接对照查找,节省排查时间:

  1. 命令行必须以 TAB 开头,不能用空格
    原因:Makefile的语法要求,命令必须以TAB开头,用空格会被识别为目标或依赖,直接报错。
    解决方案:检查命令行前面的缩进,确保是TAB(很多编辑器默认是空格,需要手动切换)。
  2. **变量引用必须用 (变量名),不能直接写变量名** 原因:直接写变量名,make会当作普通字符串,不会识别为变量。 解决方案:比如要用CC变量,必须写 (CC),而不是CC。
  3. 伪目标未用 .PHONY 声明,导致报错
    原因:目录下有和伪目标同名的文件(比如clean文件),make会把伪目标当作文件处理。
    解决方案:所有伪目标(clean、all等)都用 .PHONY 声明。
  4. 依赖文件路径错误,导致编译失败
    原因:比如多目录项目中,依赖文件在src目录,却没写路径,make找不到文件。
    解决方案:正确定义目录变量(比如SRC_DIR = src),引用时加上路径(比如 $(SRC_DIR)/main.c)。
  5. 头文件未用 -I 指定,报"头文件找不到"
    原因:头文件在inc目录,编译器不知道去哪里找,导致编译报错。
    解决方案:在CFLAGS中添加 -I$(INC_DIR),指定头文件目录。

总结(核心要点,快速记忆)

最后,用5句话总结Makefile的核心,方便大家快速记忆和回顾:

  1. Makefile 核心 = 规则 + 变量 + 自动变量 + 函数,四者结合,实现自动化编译。
  2. 最简万能模板:用 wildcard 自动获取.c文件,用 patsubst 自动生成.o文件,复制即用。
  3. 新手必记2个重点:命令以TAB开头伪目标用.PHONY声明,避免踩坑。
  4. 多目录项目:用目录变量管理文件,-I 指定头文件目录,让项目结构更清晰。
  5. 日常使用:写好一份通用Makefile,所有单目录C项目直接复用;多目录项目稍作修改即可。
相关推荐
A小辣椒12 小时前
TShark:Wireshark CLI 功能
linux
A小辣椒16 小时前
TShark:基础知识
linux
AlfredZhao18 小时前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao1 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334662 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪2 天前
linux 拷贝文件或目录到指定的位置
linux
大树882 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠2 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质2 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush42 天前
嵌入式linux学习记录十四、术语
linux·嵌入式