Ubuntu中如何使用make与编写Makefile?

来源:网站建设经验作者:BIT程序员头衔:程序员
导读:本期聚焦于BIT程序员创作的《Ubuntu中如何使用make与编写Makefile?》,敬请观看详情。在 Ubuntu 下编译多文件 C 项目时,手动输入 gcc 命令很容易漏掉依赖关系,链接顺序稍有差错就报出一堆未定义引用。make 工具配合 Makefile 就是解决这类构建混乱问题的标准方案。本文从零开始讲解 Makefile 的规则格式、变量用法、自动变量和模式规则,并给出多目录项目的真实示例。还会介绍 make -n、make -B、make -j 等常用参数,帮助你在命令行中快速判断构建行为、强制重编和并行加速。读完这篇文章,你可以自己维护一个结构清晰、增量编译高效的 Makefile,不再被重复编译和链接错误困扰。对于刚接触 Linux 构建工具的同学,这比直接跳进 CMake 更可控,也更容易理解底层编译流程。

Ubuntu 本身预装了 GCC 工具链,但实际项目往往包含十几个源文件和头文件。每次修改代码后手工执行 gcc 命令不仅繁琐,还容易因为链接顺序错误产生 undefined reference。make 通过读取 Makefile 中描述的依赖关系,自动决定哪些文件需要重新编译。它本质上是一个构建自动化工具,核心逻辑是:如果目标文件不存在,或者某个依赖比目标文件更新,就执行对应命令重新生成目标。

Ubuntu中如何使用make与编写Makefile?

一、Makefile 的基本规则

Makefile 的基本组成单位是规则,每条规则由三部分组成:目标、依赖和命令。目标通常是需要生成的文件名,例如可执行文件 app 或目标文件 main.o;依赖是生成目标所需要的源文件或头文件;命令则是实际要执行的编译或链接指令。规则的格式如下:

目标: 依赖1 依赖2
    命令

这里最容易出错的是命令前面的缩进必须使用 Tab 键,不能用空格代替。Ubuntu 下有些编辑器的默认缩进是空格,直接复制网上的 Makefile 时经常出现 Makefile:2: *** missing separator. Stop. 这样的报错。解决方式是用 vim 打开文件后执行 set noexpandtab,或者用 cat -A Makefile 检查行首是否显示为 ^I 而不是空格。

一个最简单的单文件示例:

app: main.c
    gcc -Wall -g -o app main.c

保存后在当前目录执行 make,如果 main.c 没有变化,再次执行 make 会提示 make: 'app' is up to date. 这是因为 make 会比较目标和依赖的修改时间戳。只有当 app 不存在或者 main.c 的修改时间比 app 新时,才会重新运行编译命令。这种增量构建机制大大节省了大型项目的编译时间。

二、变量、自动变量与模式规则

当源文件增多时,重复书写 gcc 命令会让 Makefile 又长又难维护。make 支持变量定义,类似 shell 的变量赋值,但引用时需要用括号包裹:$(CC)、$(CFLAGS)。常见的写法是把编译器和编译选项抽离出来:

CC = gcc
CFLAGS = -Wall -g -O2
LDFLAGS =

app: main.o utils.o
    $(CC) $(CFLAGS) -o app main.o utils.o $(LDFLAGS)

main.o: main.c utils.h
    $(CC) $(CFLAGS) -c main.c

utils.o: utils.c utils.h
    $(CC) $(CFLAGS) -c utils.c

这样修改编译器或者增加编译选项时只需要改一处。make 内置了很多默认变量,比如 CC 默认就是 cc,CFLAGS 默认是空。你可以把变量放在文件顶部,也可以在命令行覆盖它们,像 make CFLAGS=-O0 这样。

除了普通变量,make 还提供自动变量来让规则更通用。自动变量的值由当前规则的目标和依赖决定,常见的有三个:$@ 表示目标文件名,$^ 表示所有依赖列表,$< 表示第一个依赖。借助自动变量,上一个示例可以改写为:

app: main.o utils.o
    $(CC) $(CFLAGS) -o $@ $^

main.o: main.c utils.h
    $(CC) $(CFLAGS) -c $<

utils.o: utils.c utils.h
    $(CC) $(CFLAGS) -c $<

$@ 和 $^ 分别对应目标和所有依赖,而 $< 只取第一个依赖。对于 main.o 规则来说 $< 就是 main.c,对于 utils.o 规则来说就是 utils.c。合理使用这些变量可以让规则更加简洁通用。

如果项目中有大量 .c 文件,逐个写规则仍然很累。make 的模式规则可以解决这个问题。模式规则使用百分号 % 匹配任意字符串,例如 %.o: %.c 表示任何以 .o 结尾的目标都依赖同名的 .c 文件。配合自动变量可以写出通用的编译规则:

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

这样只需要维护一个目标文件和依赖关系的列表,不需要为每个源文件重复写编译命令。不过要注意,模式规则不包含头文件依赖,如果只修改了 utils.h,make 可能不会重新编译包含它的 .c 文件。解决这个问题可以在规则中显式加上头文件,或者使用 gcc -MMD 自动生成依赖文件。

三、多目录项目与伪目标

实际项目通常会把源码放在 src 目录,头文件放在 include 目录,目标文件放在 build 目录,可执行文件放在 bin 目录。此时 Makefile 需要处理路径切换和目录创建。下面是一个典型的多目录结构示例:

CC = gcc
CFLAGS = -Wall -g -Iinclude
SRC_DIR = src
BUILD_DIR = build
BIN_DIR = bin
TARGET = $(BIN_DIR)/app
SRCS = $(wildcard $(SRC_DIR)/*.c)
OBJS = $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.o,$(SRCS))

all: $(TARGET)

$(TARGET): $(OBJS)
    mkdir -p $(BIN_DIR)
    $(CC) $(CFLAGS) -o $@ $^

$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c
    mkdir -p $(BUILD_DIR)
    $(CC) $(CFLAGS) -c $< -o $@

clean:
    rm -rf $(BUILD_DIR) $(BIN_DIR)

.PHONY: all clean

这里用到了两个 make 内置函数:wildcard 用于获取 src 目录下所有 .c 文件列表,patsubst 用于把 .c 文件路径转换成 .o 文件路径。如果 src 目录下有 main.c 和 utils.c,OBJS 就会变成 build/main.o build/utils.o。编译时先执行 mkdir -p 确保目录存在,避免出现没有那个文件或目录的错误。

注意到最后一行 .PHONY: all clean。all 和 clean 并不是真实存在的文件名,如果当前目录恰巧有一个名为 clean 的文件,make clean 会认为目标已经存在而不执行删除命令。加上 .PHONY 之后 make 会无条件执行这些伪目标对应的命令。这是一个很容易忽略但非常重要的细节,建议所有不生成同名文件的规则都声明为 .PHONY。

多目录项目还可以使用 VPATH 变量来指定源文件搜索路径,但它的行为和直接写路径稍有不同。VPATH 适合简单场景,对于复杂项目建议使用上面显式的目录前缀,这样目标与依赖关系更清晰,不容易出现同名文件覆盖问题。

四、make 常用参数与调试技巧

写完 Makefile 后,可以利用 make 自带的参数来检查构建流程。最常用的是 -n 或 --just-print,它只打印要执行的命令而不真正执行,非常适合在运行前确认是否少了某个依赖或者命令写错。比如 make -n 会输出所有需要重新编译的步骤,如果发现某条命令没有出现,说明对应的依赖关系没有触发。

如果怀疑目标文件判断有问题,可以用 -B 或 --always-make 强制所有目标重新生成。这在头文件依赖遗漏导致增量编译结果不对时特别有用,配合 -n 可以先看到完整的重编命令序列。另一个常用参数是 -j,后面接并行任务数,例如 make -j4 可以使用 4 个并行任务同时编译独立的 .o 文件。在多核 Ubuntu 服务器上,make -j$(nproc) 能显著缩短构建时间,但要注意各目标之间的依赖顺序不能因为并行而打乱。

调试 Makefile 时还可以使用 make -d 查看详细的决策过程,输出会说明每个目标为什么需要或不需要重建。不过 -d 的输出量非常大,一般只需要在出错时重定向到文件再搜索关键目标名。另外,make -p 可以打印所有内置变量和规则,方便确认某个变量的实际值。遇到 missing separator 错误时,用 cat -A Makefile 检查 Tab;遇到 No rule to make target 时,通常是指定的源文件路径不存在或者模式规则没有匹配上。

最后提醒一点,Makefile 中的命令默认交给 /bin/sh 执行,而不是 bash。Ubuntu 中 /bin/sh 链接到 dash,语法比 bash 严格,如果命令里使用了 bash 特有的数组或 [[ ]] 条件,需要加上 SHELL := /bin/bash 声明。这个细节在编写复杂构建脚本时经常引发莫名其妙的报错。

UbuntumakeMakefile修改时间:2026-10-05 03:40:01

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/1005/65825.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。