Makefile 是一个用于自动化编译和链接过程的脚本文件,它定义了一系列的规则,告诉 make 工具如何根据源文件生成最终的可执行文件或库,使用 Makefile 可以极大地简化项目管理,尤其是在项目包含多个源文件时。

(图片来源网络,侵删)
为什么需要 Makefile?
假设你有一个简单的 C 项目,包含三个文件:main.c, utils.c, utils.h。
main.c调用了utils.c中的函数。utils.h声明了utils.c中的函数。
没有 Makefile 时,你需要手动编译:
-
首先编译
utils.c生成目标文件:gcc -c utils.c -o utils.o
-c: 只编译,不链接,生成.o(object) 文件。
-
然后编译
main.c生成目标文件:
(图片来源网络,侵删)gcc -c main.c -o main.o
-
将所有目标文件链接成可执行文件:
gcc main.o utils.o -o my_program
如果项目有几十个甚至上百个源文件,这个过程会变得非常繁琐且容易出错。
有了 Makefile,你只需要在项目根目录下执行:
make
make 工具会自动读取 Makefile 文件,并根据你定义的规则完成所有编译和链接工作。

(图片来源网络,侵删)
Makefile 的核心概念
一个 Makefile 主要由 规则、变量 和 函数 构成。
a. 规则
规则是 Makefile 的基本单元,它定义了“目标”及其“依赖项”,以及如何从依赖项生成目标的“命令”。
target: dependencies
command
command
...
target(目标): 你想要生成的文件,通常是一个可执行文件、一个库文件或一个中间的.o文件,它也可以是一个“伪目标”,clean,用于执行清理操作。dependencies(依赖项): 生成target所需要的文件,如果依赖项比目标文件新,或者目标文件不存在,make就会执行下面的命令。command(命令): 用于生成目标的 shell 命令。注意:每个命令行前必须按Tab键,而不是空格!这是最常见的初学者错误。
b. 变量
变量用于存储文件名、编译器选项等,使 Makefile 更易于维护和修改。
CC = gcc # 定义一个变量 CC,值为 gcc
CFLAGS = -Wall # 定义一个变量 CFLAGS,值为 -Wall (显示所有警告)
# 在规则中使用变量
my_program: main.o utils.o
$(CC) $(CFLAGS) main.o utils.o -o my_program
$(CC)和$(CFLAGS)是变量的引用方式。
c. 自动变量
make 提供了一些有用的自动变量,在规则的命令中非常方便:
- 代表规则中的“目标”。
$<: 代表规则中的“第一个依赖项”。$^: 代表规则中的“所有依赖项”。
示例:从简单到复杂
示例 1:最简单的 Makefile
项目结构:
.
├── main.c
├── utils.c
└── utils.h
Makefile
# 定义变量
CC = gcc
CFLAGS = -Wall -g
TARGET = my_program
SRCS = main.c utils.c
OBJS = $(SRCS:.c=.o) # 这是一个函数,将 .c 替换为 .o
# 默认目标,当只输入 make 时执行
all: $(TARGET)
# 规则:如何生成最终的可执行文件
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) $^ -o $@
# 隐式规则:告诉 make 如何从 .c 文件生成 .o 文件
# .o 文件依赖于同名的 .c 文件
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# 清理规则:删除所有编译生成的文件
clean:
rm -f $(TARGET) $(OBJS)
# .PHONY 声明:告诉 make "clean" 是一个伪目标
# 即使目录下存在名为 "clean" 的文件,make 也会将其视为规则
.PHONY: all clean
执行步骤:
-
首次编译:
make
make看到all是默认目标。all依赖于my_program。my_program不存在,所以需要生成它,它依赖于main.o和utils.o。main.o不存在,make查找规则,发现%.o: %.c这个隐式规则,于是执行gcc -Wall -g -c main.c -o main.o。- 同理,执行
gcc -Wall -g -c utils.c -o utils.o。 main.o和utils.o都存在了,make执行链接命令:gcc -Wall -g main.o utils.o -o my_program。
-
再次编译:
make
make检查所有依赖项的时间戳。- 发现
my_program的时间戳比main.o和utils.o都新,所以什么也不做,输出make: 'my_program' is up to date.。
-
修改源文件后编译:
# 修改 utils.c make
make检查到utils.c的时间戳比utils.o新。- 它会重新编译
utils.c生成新的utils.o。 - 然后发现
my_program的时间戳比新的utils.o旧,所以会重新链接my_program。
-
清理项目:
make clean
make执行clean规则下的命令rm -f my_program main.o utils.o,删除所有生成的文件。
进阶:多级目录和库项目
示例 2:多级目录项目
项目结构:
project/
├── Makefile
├── src/
│ ├── main.c
│ └── utils.c
├── include/
│ └── utils.h
└── build/ # 存放编译中间文件和最终可执行文件
Makefile 内容 (位于 project/ 目录):
# --- 变量定义 ---
CC = gcc
CFLAGS = -Wall -g -I./include # -I 指定头文件搜索路径
SRCDIR = src
BUILDDIR = build
TARGET = $(BUILDDIR)/my_program
# 获取 src 目录下所有 .c 文件,并生成对应的 .o 文件路径
SRCS = $(wildcard $(SRCDIR)/*.c)
OBJS = $(SRCS:$(SRCDIR)/%.c=$(BUILDDIR)/%.o)
# --- 规则定义 ---
.PHONY: all clean
all: $(TARGET)
# 最终可执行文件的生成规则
$(TARGET): $(OBJS)
@echo "Linking..."
$(CC) $(CFLAGS) $^ -o $@
# 从 .c 文件生成 .o 文件的规则
# 使用 vpath 告诉 make 在哪里找 .c 文件
vpath %.c $(SRCDIR)
$(BUILDDIR)/%.o: %.c
@echo "Compiling $<..."
@mkdir -p $(dir $@) # 确保目标目录存在
$(CC) $(CFLAGS) -c $< -o $@
clean:
@echo "Cleaning..."
rm -rf $(BUILDDIR)
关键点说明:
wildcard $(SRCDIR)/*.c:make的函数,用于匹配src目录下所有.c文件,得到类似src/main.c src/utils.c的列表。SRCS:$(SRCDIR)/%.c=$(BUILDDIR)/%.o: 字符串替换函数,将src/main.c替换为build/main.o,src/utils.c替换为build/utils.o。vpath %.c $(SRCDIR): 声明所有.c文件都在src目录下查找,这样在$(BUILDDIR)/%.o: %.c规则中,make就能找到src/main.c。@mkdir -p $(dir $@):dir函数提取文件所在的目录(如build/),mkdir -p确保目录存在,避免首次编译时出错。@echo ...: 符号表示不显示命令本身,只显示其输出,让编译过程更清晰。
现代 C/C++ 项目:使用 CMake
对于非常复杂的项目,直接手写 Makefile 会变得很困难,现代 C/C++ 项目更推荐使用 CMake。
CMake 的工作流程:
- 你编写一个
CMakeLists.txt文件,用高级的、与平台无关的语法描述你的项目。 - CMake 会根据这个
CMakeLists.txt文件,为你生成适合当前平台和编译器的 Makefile(或者其他构建系统文件,如 Visual Studio 的.sln)。 - 你使用
make命令来执行生成的 Makefile。
示例 CMakeLists.txt (对应上面的多级目录项目):
cmake_minimum_required(VERSION 3.10)
project(MyProject C)
# 设置 C++ 标准
set(CMAKE_C_STANDARD 11)
# 添加头文件搜索路径
include_directories(include)
# 收集所有源文件
aux_source_directory(src SOURCES)
# 添加可执行文件
add_executable(my_program ${SOURCES})
# 指定输出目录
set_target_properties(my_program PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
)
使用 CMake 的步骤:
# 1. 创建一个构建目录 mkdir build cd build # 2. 运行 cmake,生成 Makefile cmake .. # 3. 使用 make 进行编译 make
CMake 自动处理了所有复杂的 Makefile 细节,是工业界和大型开源项目的标准做法。
| 特性 | 手写 Makefile | CMake |
|---|---|---|
| 复杂度 | 简单项目直接,复杂项目繁琐 | 简单描述,自动生成复杂 Makefile |
| 平台兼容性 | 差,不同平台/编译器需修改 | 极好,自动适配不同平台和编译器 |
| 可维护性 | 差,规则多时难以维护 | 好,逻辑清晰,易于扩展 |
| 适用场景 | 小型项目、学习、嵌入式环境 | 几乎所有 C/C++ 项目,特别是中大型项目 |
建议:
- 初学者/小型项目: 学习并掌握手写 Makefile,这能帮助你深刻理解编译和链接过程。
- 实际项目/大型项目: 直接使用 CMake,它能让你更专注于代码逻辑而不是构建脚本。
