Makefile 是 make 工具的配置文件,主要用于定义如何构建和管理项目的编译过程。Makefile 是 C/C++ 编译项目中的重要工具,尤其在多文件、多模块的项目中,为了方便复杂项目的管理,可以通过自动化规则提高编译效率。

这篇文章是 Makefile 的入门教程的介绍,包括其基本语法、常用功能、以及实际使用中的高级技巧。

Makefile 的基本概念

安装 make

sudo apt-get update
sudo apt-get install make

核心功能

  • 自动化编译:根据文件的依赖关系自动决定哪些文件需要重新编译。
  • 提高效率:只编译发生变化的部分。
  • 多任务支持:可以定义清理、打包、测试等任务。

使用方法

运行make命令时,默认会读取当前目录下的Makefile 文件

make            # 执行默认的目标
make <target>   # 执行指定的目标

例如:

make clean      # 执行 "clean" 目标

Makefile 的基本语法

1. 目标语法

Makefile 的基本语法如下:

target: prerequisites
	commands
  • target:目标文件或命令名称。例如可执行文件、目标文件或任务名称。
  • prerequisites:依赖文件或其他目标。只有依赖文件发生变化时,target 才会被重新生成。
  • commands:生成目标的命令(必须以 Tab 开头)。

示例:

hello: hello.o
	gcc -o hello hello.o

hello.o: hello.c
	gcc -c hello.c

在示例代码中:

  1. 如果 hello.c 被修改,则 hello.o 会重新生成。

  2. 如果 hello.o 被修改,则 hello 会重新生成。

2. 变量定义

Makefile 支持变量定义,用于简化配置和重复的命令。

示例1:

CC = gcc
CFLAGS = -Wall -g

# 简单赋值 CC :=$(XX) gcc 当前有效 // 立马获取当前XX变量的值
# 递归赋值 CC =$(XX) gcc			// 去递归寻找XX变量的最后一个值
# 条件赋值 CC ?= gcc				// 如果有CC变量,则该语句无效
# 追加赋值 CC +=$(XX) gcc		// 在原来值的基础上,进行追加

# 使用:$(CC) 获取变量的值

示例2:

x := jake
y := $(x) and rose
x := tonly

all :
		echo "x=$(x),y=$(y)"

使用变量(示例1)

hello: hello.o
	$(CC) $(CFLAGS) -o hello hello.o

输出:(示例2)

x=windx,y=jake and rose

示例

CC = gcc
CFLAGS = -Wall -O2

hello: hello.o
	$(CC) $(CFLAGS) -o hello hello.o

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

3. 内置变量

Makefile 提供了一些常用的内置变量,可以减少重复工作。

变量 | 功能

变量功能
$@当前目标的名称(target)。
$<第一个依赖文件的名称(prerequisite)。
$^所有依赖文件的名称。

代码片:

CC = gcc
CFLAGS = -Wall -g

hello: hello.o
	$(CC) $(CFLAGS) -o $@ $^

在这里:

  • $@ 是 hello(目标)。
  • $^ 是 hello.o(所有依赖文件)。

4. 通配符和自动化规则

Makefile 支持通配符和模式匹配规则,用于简化多文件项目的编写。

通配符

  • *.c:匹配所有 .c 文件。
  • $(wildcard *.c):列出当前目录下所有 .c 文件。
  • $(patsubst %.c, %.o, $(wildcard *.c)):将所有 .c 文件替换为对应的 .o 文件。

自动化规则

使用模式规则可以自动生成目标文件:

%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@
  • 表示所有 .o 文件都可以由对应的 .c 文件生成。

代码:

CC = gcc
CFLAGS = -Wall -O0 -g
SRC = $(wildcard *.c)
OBJ = $(patsubst %.c, %.o, $(SRC))

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

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

5. 伪目标

伪目标(phony targets)是指不生成实际文件的目标,通常用于清理、测试等任务。

伪目标也是一个目标,该目标不需要生成,每一次去执行该目标,都会执行。

定义伪目标:

.PHONY: clean # 将当前目标设定为伪目标,每次执行该目标都会执行命令
clean:
	rm -f *.o hello
  • 使用 .PHONY 声明可以避免冲突(例如当前目录下存在名为 clean 的文件时)。

6. 自动推导规则

在使用make编译.c源文件时,可以省略编译一个.c文件所使用的命令。这是因为make存在一个默认的规则,能够自动完成对.c文件的编译并生成对应的.o文件。它执行命令“cc -c”来编译.c源文件。对于上边的例子,此默认规则就使用命令“cc -c main.c -o main.o”来创建文件“main.o”。因此对一个目标文件是“N.o”,倚赖文件是“N.c”的规则。可以省略其规则的命令行,使用 make 的默认命令。此默认规则称为make的隐含规则(关于隐含规则可查看我上传至资源中的 《GUN make中文手册》第九章 [ 使用隐含规则 ] 一章,有详细说明。)

我们书写 Makefile 时,对于一个.c文件如果使用 make 的隐含规则,那么它会被自动作为对应.o文件的一个依赖文件(对应是指:文件名除后缀外,其余都相同的两个文件)。因此我们也可以在规则中省略目标的倚赖.c文件。

自动推导规则_1

自动推导规则_2

我们可以看见,当app文件被创建之后,再次执行 make 命令时,提示:make: 'app' is up to date.,此处是 make 的时间戳管理方式,如果目标生成的时间 晚于 依赖,该目标是最新的,那么该目标就不需要再编译重新生成。(如果想详细了解makefile的时间戳管理机制,可以点击查看这篇文章,对 Makefile 的时间戳管理机制有更详细的说明。<

笨猫猫的小茶馆:Makefile 时间戳管理2 赞同 · 0 评论文章

项目示例

这是一个多文件 C 项目的示例 Makefile

项目目录

project/
├── main.c
├── utils.c
├── utils.h
├── Makefile
```
##### Makefile
```bash
# 编译器和编译参数
CC = gcc
CFLAGS = -Wall -g

# 源文件和目标文件
SRC = main.c utils.c
OBJ = $(SRC:.c=.o)
TARGET = my_program

# 默认目标
all: $(TARGET)

# 链接目标文件
$(TARGET): $(OBJ)
	$(CC) $(CFLAGS) -o $@ $^

# 编译规则(自动化规则)
%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

# 清理目标
.PHONY: clean
clean:
	rm -f $(OBJ) $(TARGET)

运行说明

  1. 构建项目
make
  1. 清理项目:
make clean

这么写缺失中间的流程,接下来我结合图片将 makefile 管理项目的实际流程走一遍:

要求:(虽然是简易版用于教学的,但其中的过程和项目管理的思路,即使在大型项目中也是一致的,不同的只是划分的层级结构、架构更为复杂而已。)

  1. 创建项目目录

  1. include/project.h 集合的头文件

  1. jack 文件夹下的文件

  1. jack.c 通过编译生成 jack.o 文件 ——> 然后将 jack.o 放入 obj 文件夹下

  1. 此处的Makefile用于管理main.c文件,将main.c编译转化为main.o,将其保存在obj文件夹下统一管理

  1. main.o文件生成成功并放在了统一文件夹obj下,接下来,将main.ojack.o做链接操作生成app文件,因此,我们需要在obj文件夹下创建Makefile文件,用于将所有集合在此处的.0 文件链接生成可执行文件app

  1. 成功生成了可执行文件app

  1. 因为main :会自动寻址至依赖的第一个文件,结果将提示文件已存在。所以这里我们要将需要生成的文件 main 设为伪目标。

至此,前面的步骤我们共计完成了 4 步:

第一步,我们将 jack 文件夹下的 jack.c 生成了 jack.o ,将其放在 obj 文件夹下;

第二步,我们将 main 文件夹下的 main.c 生成了 main.o,将其放在 obj 文件夹下;

第三步,在 obj 文件夹下,将这里所有集合着的 .o 文件 最终生成了 可执行文件 app

最后,我们在整个项目目录下,生成一个 Makefile 文件,用于管理整个项目,这样,最终我们只需要 make 一次,便能够得到整体项目的可执行文件,而不需要经过上述如此繁琐的步骤。

make -C 文件夹/[目标] // 去到该文件夹下 去执行该文件夹下的 Makefile 的 第一个目标[目标]

因为第一个目标已存在,所以此处我们需要提前设置mainclean为伪目标。

  1. 最终生成可执行文件 app

  1. 优化 Makefile 代码。

  1. 添加共享变量,在最终的 Makefile 文件中添加共享变量,这样的话,可供同一级目录 和 该目录下的所有子目录中的 Makefile 文件皆可以共享到最上层的 Makefile文件 中的共享变量。
export 共享变量 // 当前同一级目录下的子目录都可以共享该变量

  1. 这时候进入了一个新人,分配任务给 rose,创建 rose 文件夹并加入这部分模块的代码,

至此,以上为 Makefile 项目管理在开发中的实际应用(简易版)。

高级功能

条件判断

根据条件设置变量或执行不同的规则。

DEBUG = 1

ifeq ($(DEBUG), 1)
CFLAGS += -g
else
CFLAGS += -O2
endif

包含其他 Makefile

使用 include 将多个 Makefile 文件整合在一起。

include common.mk
  • 这种方式适合大型项目,将公共配置提取到单独的文件中。

一些常见问题与技巧

如何调试 Makefile?

运行 make 时加上-n--dry-run参数,可以查看执行的命令而不真正运行:

make -n

如何提高编译效率?

使用 make 的并行编译选项-j,可以同时编译多个目标:

make -j4
  • 其中 4 表示最多允许 4 个任务同时执行。

综上。这篇文章仅仅只介绍了 makefile 的简单用法,更多更详细的内容可以下载资源查看 makefile 手册(中文版),Makefile 是一个强大的工具,适合用于管理从简单到复杂的编译流程。

本章主要介绍了:

  1. 基础语法:理解目标、依赖和命令的结构。
  2. 变量和内置变量:使用变量提高可读性和灵活性。
  3. 自动化规则:使用通配符和模式规则减少重复代码。
  4. 伪目标:定义清理、测试等任务。
  5. 高级功能:条件判断、多文件组织、并行编译。

通过不断优化 Makefile,可以显著提高项目的管理效率和可维护性,适合运用和管理大型项目。

以上。仅供学习与分享交流,请勿用于商业用途!转载需提前说明。