在这里插入图片描述


前言

你是否有过这样的经历?

  • 写了一个多文件的 C 项目,每次编译都要敲一长串 gcc 命令
  • 改了其中一个文件,却要重新编译所有文件,浪费大量时间
  • 听说有个 make 命令,但不知道 makefile 怎么写

别急,这篇文章就是为你准备的。我们把 make/makefile 的来龙去脉、核心概念、实际用法,一次性讲清楚。

🍔 比喻:make/makefile 就像一个智能厨师。你告诉它“我想吃番茄炒蛋”(目标),它知道需要番茄和鸡蛋(依赖),然后按照菜谱(依赖方法)给你做出来。如果你已经做好了,它就不会重复做。


一、make 和 makefile 是什么?

概念 本质 比喻
make 一个命令 厨师本人
makefile 一个文件(存放编译规则) 菜谱

关系make 命令会读取 makefile 文件,按照里面的规则来编译程序。

# 基本用法
make          # 根据 makefile 编译项目
make clean    # 执行清理操作(如果定义了 clean)

二、makefile 的核心概念

2.1 依赖关系和依赖方法

概念 含义 示例
依赖关系 目标文件由哪些源文件生成 code 依赖 code.c
依赖方法 如何从依赖生成目标 gcc -o code code.c

2.2 一个最简单的 makefile 示例

# 依赖关系:code 这个目标依赖于 code.c
code: code.c
# 依赖方法:用 gcc 编译(注意:前面必须是一个 Tab 键,不能是空格!)
	gcc -o code code.c

执行:

make
# 会执行:gcc -o code code.c

⚠️ 巨坑提醒:依赖方法前面的缩进必须是 Tab 键,不能是空格!这是新手 99% 会踩的坑。如果你用的是 VS Code,记得把编辑器设置为「Tab 转空格」关闭。


三、makefile 的工作流程

3.1 默认执行第一个目标

make 命令扫描 makefile 时,从上到下,默认执行第一个目标文件。

# 第一个目标是 code,所以 make 默认执行这个
code: code.c
	gcc -o code code.c

clean:
	rm -f code
make    # 只执行 code 的编译,不会执行 clean

3.2 如何判断是否需要重新编译?

这是 make 最智能的地方:通过比较时间戳。

核心逻辑:

· 如果目标文件不存在 → 执行编译
· 如果目标文件存在 → 比较目标和依赖的修改时间
· 依赖文件(.c)比目标文件(code)更新 → 重新编译
· 依赖文件比目标文件更旧 → 不编译(提示:make: ‘code’ is up to date.)

🕐 比喻:就像你检查牛奶是否过期。生产日期(依赖时间)比当前时间(目标时间)新?说明需要重新买。


四、文件的三个时间(ACM 时间)

4.1 什么是 ACM 时间?

在 Linux 中,每个文件都有三个时间戳:

时间 全称 含义 什么时候改变
Access atime 最后访问时间 catlessvim 打开文件
Modify mtime 最后内容修改时间 echo >vim 修改内容
Change ctime 最后属性修改时间 chmodchown、修改内容也会改

4.2 三者之间的关系

# 查看文件的时间
stat code.c

输出示例:

  File: code.c
  Size: 1024          Blocks: 8          IO Block: 4096   regular file
Access: 2024-05-17 10:00:00.000000000 +0800
Modify: 2024-05-17 09:00:00.000000000 +0800
Change: 2024-05-17 09:00:00.000000000 +0800

重要结论:

· 修改文件内容时,Modify 和 Change 都会变
· 只修改文件权限(chmod)时,只有 Change 变

4.3 make 用哪个时间来判断?

make 通过 Modify 时间来判断文件新旧。

# 手动更新文件的 Modify 时间
touch code.c

# 此时 make 会认为 code.c 比 code 新,触发重新编译
make

💡 touch 命令可以更新文件的三个时间到当前时间。


五、清理操作:clean 和 .PHONY

5.1 为什么要 clean?

清理对象 为什么要删
code(可执行文件) 重新编译时从头生成,避免旧程序残留
*.o(目标文件) 保证完全重新编译,避免链接错误
clean:
	rm -f code *.o

· -f:强制删除,文件不存在也不报错
· *.o:通配符,删除所有以 .o 结尾的文件

5.3 .PHONY:伪目标

.PHONY: clean
clean:
	rm -f code *.o

.PHONY 的作用:告诉 make,clean 不是一个真实的文件,而是一个伪目标,总是执行它的依赖方法。

什么情况下会“不执行”?

没有 .PHONY 声明时,如果当前目录下恰好有一个名为 clean 的文件,make 会比较 clean 文件和它的依赖(如果有),判断是否需要执行。这会导致 clean 命令不执行,非常坑!

# 假设当前目录有一个叫 clean 的文件
touch clean

# 没有 .PHONY 的话,执行 make clean 会提示
make clean
# make: 'clean' is up to date.  (不会执行删除!)

# 加了 .PHONY 后,无论有没有 clean 文件,都会执行

🎯 最佳实践:所有不生成同名文件的目标,都应该用 .PHONY 声明。


六、makefile 中的变量

6.1 为什么要用变量?

· 避免重复书写
· 方便修改(改一处即可)

6.2 变量定义和使用

# 定义变量
BIN   = code
CC    = gcc
SRC   = code.c
OBJ   = code.o
FLAGS = -o

# 使用变量:$(变量名)
$(BIN): $(OBJ)
	$(CC) $(FLAGS) $@ $^

6.3 自动变量(重要!)

自动变量 含义 示例值
$@ 目标文件 code
$^ 所有依赖文件 code.o(如果有多个依赖,就是 a.o b.o c.o
$< 第一个依赖文件 code.o
BIN = code
CC = gcc
SRC = code.c
OBJ = code.o

# 生成 .o 文件
$(OBJ): $(SRC)
	$(CC) -c $< -o $@

# 生成可执行文件
$(BIN): $(OBJ)
	$(CC) $^ -o $@

# 清理
.PHONY: clean
clean:
	rm -f $(OBJ) $(BIN)

七、多文件项目实战

7.1 项目结构

project/
├── main.c
├── add.c
├── add.h
├── sub.c
├── sub.h
└── makefile

7.2 源文件内容

// add.h
#ifndef ADD_H
#define ADD_H
int add(int a, int b);
#endif
// add.c
#include "add.h"
int add(int a, int b) {
    return a + b;
}
// sub.h
#ifndef SUB_H
#define SUB_H
int sub(int a, int b);
#endif
// sub.c
#include "sub.h"
int sub(int a, int b) {
    return a - b;
}
// main.c
#include <stdio.h>
#include "add.h"
#include "sub.h"

int main() {
    printf("10 + 20 = %d\n", add(10, 20));
    printf("10 - 20 = %d\n", sub(10, 20));
    return 0;
}

7.3 makefile 写法

# 变量定义
BIN   = app
CC    = gcc
CFLAGS= -Wall -g
OBJS  = main.o add.o sub.o

# 默认目标
$(BIN): $(OBJS)
	$(CC) $^ -o $@

# 编译每个 .c 为 .o
main.o: main.c add.h sub.h
	$(CC) -c $< -o $@

add.o: add.c add.h
	$(CC) -c $< -o $@

sub.o: sub.c sub.h
	$(CC) -c $< -o $@

# 清理
.PHONY: clean
clean:
	rm -f $(OBJS) $(BIN)

# 重新编译(清理 + 编译)
.PHONY: rebuild
rebuild: clean $(BIN)

7.4 简化版(使用通配符)

BIN   = app
CC    = gcc
CFLAGS= -Wall -g
SRCS  = $(wildcard *.c)
OBJS  = $(SRCS:.c=.o)

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

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

.PHONY: clean
clean:
	rm -f $(OBJS) $(BIN)

解释:

· $(wildcard *.c):获取所有 .c 文件
· $(SRCS:.c=.o):把所有 .c 替换成 .o


八、makefile 常用函数和技巧

8.1 常用函数

函数 语法 作用
wildcard $(wildcard pattern) 获取匹配模式的文件列表
patsubst $(patsubst pattern,replacement,text) 模式替换
shell $(shell command) 执行 shell 命令

8.2 实用技巧

# 打印变量调试
print:
	@echo "BIN = $(BIN)"
	@echo "OBJS = $(OBJS)"

# 使用 shell 命令获取信息
CURRENT_DIR = $(shell pwd)

# 条件判断
ifeq ($(CC), gcc)
	CFLAGS += -D_GNU_SOURCE
endif

九、常见问题速查

问题 原因 解决方法
make: Nothing to be done for 'code' 目标已是最新 修改源文件或用 touch 更新
Makefile:2: *** missing separator. Stop. 缩进用了空格 改成 Tab
make: 'clean' is up to date. 存在 clean 文件且无 .PHONY 添加 .PHONY: clean
undefined reference to 'func' 链接时缺少目标文件 检查 OBJS 是否包含所有 .o

总结速查表

知识点 核心内容
make 命令,读取 makefile 执行编译
makefile 文件,定义编译规则
依赖关系 目标 ← 依赖
依赖方法 如何从依赖生成目标(Tab 缩进
.PHONY 声明伪目标,总是执行
ACM 时间 Access、Modify、Change
make 判断新旧 比较 Modify 时间
自动变量 $@(目标)、$^(所有依赖)、$<(第一个依赖)
变量使用 $(变量名)
通配符 *.o$(wildcard *.c)

最后

make/makefile 是 Linux 开发的必备技能,核心就三点:

  1. 依赖关系:目标由什么生成
  2. 依赖方法:怎么生成(Tab 缩进)
  3. 时间判断:依赖比目标新就重新编译

掌握这些,你就可以轻松管理任何规模的项目了。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐