Makefile教程 C语言编译 多个C文件编译 C文件 编译链接 自动依赖 make工具使用 makefile make 基础语法
一、Makefile三要素
makefile最基本是由三个要素组成,分别为:目标文件,依赖文件,规则(make默认只执行第一条规则,并不是传统语言的按序执行每一条命令,make执行的时候会自动判断目标文件的依赖,若不存在依赖或者依赖更新了,才会去执行对应的依赖文件的规则,所有一般将最终文件所需的生成文件作为第一条规则)。
若不存在依赖或者依赖更新了,才会去执行对应的依赖文件的规则。这一特性确保了make在执行时候跳过对未更新依赖的执行,在编译大型项目的时候这一点尤为重要。
1.1 规则
Makefile由若干条规则(Rule)构成,每一条规则指出一个目标文件(Target),若干依赖文件(prerequisites),以及生成目标文件的命令。
例如,要生成m.txt,依赖a.txt与b.txt,规则如下:
# 目标文件: 依赖文件1 依赖文件2
m.txt: a.txt b.txt
cat a.txt b.txt > m.txt
一条规则的格式为目标文件: 依赖文件1 依赖文件2 ...,紧接着,以Tab开头的是命令,用来生成目标文件。上述规则使用cat命令合并了a.txt与b.txt,并写入到m.txt。用什么方式生成目标文件make并不关心,因为命令完全是我们自己写的,可以是编译命令,也可以是cp、mv等任何shell中的命令。
1.2 伪目标
1.2.1 伪目标的基本概念
在 Makefile 中,伪目标(Phony Targets)是指那些不是实际文件的目标,它们只是用来执行一些特定的命令,而不是生成实际文件。伪目标通常用于执行某些操作,如清理、安装、测试等,而不关心这些操作是否生成了文件。
- 默认情况下,
make会认为目标是一个文件,并会检查该文件是否存在。如果目标文件已经存在且没有被修改,make就不会执行对应的规则。 - 伪目标通过
PHONY变量来声明,告诉make这个目标不需要与实际文件关联。
1.2.2 使用伪目标的原因
-
避免与实际文件同名冲突: 如果你有一个目标和文件同名(例如目标
clean和文件clean),make会将clean当作文件来看待,检查文件的更新时间是否需要重新生成。这时,声明clean为伪目标可以避免make去寻找或更新名为clean的文件。 -
执行非文件操作: 伪目标用于执行诸如清理、构建、安装、测试等操作,这些操作并不生成实际的文件,而是执行一组命令。
1.2.3 伪目标的例子
1.clean 伪目标: 这是一个常见的伪目标,通常用于删除中间文件、临时文件或构建产物(例如 .o 文件、可执行文件等)。
.PHONY: clean
clean:
rm -f *.o my_program
clean目标删除所有.o文件和名为my_program的可执行文件。
2.install 伪目标: 用于安装程序或库文件。
.PHONY: install
install:
cp my_program /usr/local/bin/
install目标将my_program复制到/usr/local/bin/目录。
3. test 伪目标: 用于执行测试。
.PHONY: test
test:
./run_tests.sh
test目标执行run_tests.sh脚本来运行测试。
1.3 隐式规则
隐式规则是 make 内置的一些规则,用于简化常见的构建过程,无需手动指定每个目标的构建命令。当 make 遇到一个目标时,它会根据目标的文件名和文件类型(例如扩展名)自动查找是否存在适用于该目标的显式规则,如果没有显式规则,make 会尝试使用隐式规则来推断如何构建该目标。
2.3.1 默认规则1:从 .c/.cpp 到 .o
一个常见的隐式规则是将 .c 文件编译为 .o 文件,形如:
.o:
$(CC) -c $(CFLAGS) $<
- 当
make需要构建一个.o文件时,它会使用默认的隐式规则。 $<表示第一个依赖文件,即.c文件。$(CC)是编译器,$(CFLAGS)是编译选项。
2.3.1 默认规则2:从 .c/.cpp 到 .exe
假设你有一个源文件 main.c,目标是生成一个可执行文件 main.exe,那么 make 会使用隐式规则来自动推断如何链接 .o 文件,形如:
.exe:
$(CC) $(LDFLAGS) -o $@ $^
$(CC)是编译器,$(LDFLAGS)是链接器选项。$@代表目标文件,$^代表所有的依赖文件(通常是.o文件)。
二、Makefile语法
2.1 执行多条命令
2.1.1 以换行符分隔
cd:
cmd1
cmd2
cmd3
执行每次命令的时候都会创建一个独立的Shell环境,并不会影响当前目录。
2.1.2 以' ; '符号分隔
cd:
cmd1;cmd2;cmd3;
在同一个shell环境内执行所有命令
2.1.3 以' && '符号分隔
cd:
cmd1&&cmd2&&cmd3
当某条命令失败时,后续命令不会继续执行
2.2 命令控制
2.2.1 控制打印
默认情况下,make会打印出它执行的每一条命令。如果我们不想打印某一条命令,可以在命令前加上@,表示不打印命令(但是仍然会执行):
no_output:
@echo 'not display'
echo 'will display'
2.2.2 控制错误
make在执行命令时,会检查每一条命令的返回值,如果返回错误(非0值),就会中断执行。有些时候,我们想忽略错误,继续执行后续命令,可以在需要忽略错误的命令前加上-:
ignore_error:
-rm zzz.txt
echo 'ok'
2.3 变量
2.3.1 自定义变量
可以自定义变量$(CC)替换命令cc,$(CFLAGS)命令用来指定编译器的标志或选项:
$(TARGET): $(OBJS)
$(CC) -o $(TARGET) $(OBJS)
例如使用交叉编译时,指定编译器:
CC = gcc
CFLAGS = -Wall -g
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
2.3.2 自动变量
自动变量是 make 在规则执行时自动设置的变量。它们根据规则的上下文变化,提供了构建目标和依赖文件时的信息。自动变量主要是用于在规则中灵活地引用目标和依赖文件。
-
$@:目标文件的名称。- 例如,在规则
foo.o: foo.c中,$@被替换为foo.o。
- 例如,在规则
-
$<:规则中第一个依赖文件的名称。- 例如,在规则
foo.o: foo.c bar.c中,$<会被替换为foo.c。
- 例如,在规则
-
$^:所有依赖文件的名称(不重复)。- 例如,在规则
foo.o: foo.c bar.c中,$^被替换为foo.c bar.c。
- 例如,在规则
-
$?:所有比目标文件更新的依赖文件。- 例如,在规则
foo.o: foo.c bar.c中,如果foo.c更新了,$?将会是foo.c。
- 例如,在规则
-
$*:不带扩展名的目标文件名。- 例如,在规则
foo.o: foo.c中,$*被替换为foo。
- 例如,在规则
2.3.3 变量赋值
Makefile 中有几种不同的变量赋值方式,它们之间的区别在于变量的展开时机:
-
=(递归展开/延迟展开): 变量的值在使用时才会被展开。这意味着变量的值会受到之后对同一个变量的重新赋值的影响。这种展开方式称为递归展开或延迟展开。x = $(y) y = hello all: @echo $(x) # 输出 hello在这个例子中,
x的值在all规则执行时才会被展开,此时y的值是hello,所以$(x)的值也是hello。使用
=进行递归定义时需要小心,避免出现循环依赖,例如x = $(y)和y = $(x)会导致无限循环。 -
Makefile:=(直接展开/立即展开): 变量的值在定义时就会被立即展开。这意味着变量的值在定义后不会再改变,即使之后对同一个变量进行了重新赋值。这种展开方式称为直接展开或立即展开。x := $(y) y := hello all: @echo $(x) # 输出空字符串在这个例子中,
x的值在定义时就被立即展开,此时y还没有定义,所以$(x)的值为空字符串。即使之后y被定义为hello,x的值也不会改变。 -
Makefile+=(追加赋值): 用于向变量追加值。objects = main.o objects += foo.o objects += bar.o all: @echo $(objects) # 输出 main.o foo.o bar.o -
Makefile?=(条件赋值): 只有在变量未定义时才进行赋值。foo ?= hello foo = world all: @echo $(foo) # 输出 world bar ?= hello all2: @echo $(bar) # 输出 hello在这个例子中,
foo已经被赋值为world,所以foo ?= hello不会生效。而bar最初没有定义,所以bar ?= hello会将bar的值设置为hello。
2.4 内建函数
1.patsubst - 模式替换
patsubst (pattern, replacement, text)
功能:用于将 text 中符合 pattern 的部分替换为 replacement。
示例:
SRCS = foo.c bar.c baz.c
OBJS = $(patsubst %.c, %.o, $(SRCS))
# 输出:OBJS = foo.o bar.o baz.o
2.wildcard - 通配符匹配
wildcard (pattern)
功能:返回当前文件夹下匹配指定模式的所有文件。
示例:
SRCS = $(wildcard *.c)
# 如果当前目录有 foo.c 和 bar.c,SRCS 会是 "foo.c bar.c"
3.foreach - 遍历列表
foreach (var, list, code)
功能:对列表 list 中的每个元素执行 code,每个元素通过 var 访问。
示例:
$(foreach file, $(SRCS), echo $(file);)
# 这个会依次输出 SRCS 中每个文件的名字
4.addsuffix - 在每个元素后添加后缀
addsuffix (suffix, names)
功能:对列表 list 中的每个元素执行 code,每个元素通过 var 访问。
示例:
OBJS = $(addsuffix .o, $(SRCS))
# 如果 SRCS = foo bar,那么 OBJS 会是 foo.o bar.o
addprefix添加前缀。
5.dir-返回文件路径
$(dir <path>)
功能:$(dir <path>) 会从 <path> 中提取出文件的路径部分,去除文件名部分,返回路径的目录部分,如果给定的是一个目录路径,会返回这个路径本身。如果给定的是一个文件路径,会返回文件所在的目录路径。
示例:
$(dir /home/user/project/main.c)
# 结果为:/home/user/project/
6. abspath-用于取绝对路径
BUILD_DIR := $(abspath ./build)
这里的 $(abspath ./build) 表示取 ./build 的绝对路径。 这个函数的主要用途是避免在不同位置使用同一个 Makefile 时,找不到目录或文件的问题。
7. bashname-去掉文件名中的后缀名
file = example.txt
output = $(basename $(file))
这里的$(basename $(file)) 表示去除file中每个字符串的后缀部分,即 .txt。
8. shell-执行 Shell 命令
CWD = $(shell pwd)
这里的 $(shell pwd) 表示执行 pwd 命令,其结果为 pwd 命令输出的当前工作目录。
9. notdir-获取文件名中的非目录部分
OBJS = ./build/main.o ./build/func.o
NOTDIR_OBJS = $(notdir $(OBJS))
这里的 $(notdir \$(OBJS)) 表示获取 OBJS 中每个字符串的非目录部分,即 main.o func.o。
三、头文件引用依赖
在 C/C++ 项目的编译过程中,头文件(.h 文件)的引用依赖问题是一个常见的挑战。具体来说,头文件的修改可能会影响到多个源文件(.c 或 .cpp 文件),因此当头文件发生变化时,相关的源文件需要重新编译。
3.1 手动管理依赖关系
一种常见的方法是显式地为每个源文件添加依赖关系。在这种方法中,你会在 Makefile 中指定哪些源文件依赖于哪些头文件,这样 make 就能知道哪些源文件需要在头文件发生变化时重新编译。例如(一个main.c文件包含了hello.h头文件):
OBJS = $(patsubst %.c,%.o,$(wildcard *.c))
TARGET = world.out
$(TARGET): $(OBJS)
cc -o $(TARGET) $(OBJS)
# 模式匹配规则:当make需要目标 xyz.o 时,自动生成一条 xyz.o: xyz.c 规则:
%.o: %.c
@echo 'compiling $<...'
cc -c -o $@ $<
clean:
rm -f *.o $(TARGET))
必须每次修改HDRS变量,非常麻烦。
3.2 使用 gcc 生成自动依赖
gcc 可以通过 -M 系列选项自动生成依赖文件,这些文件会列出每个源文件所依赖的头文件。通过这种方法,make 可以根据头文件的修改自动触发重新编译。
例如:
$ cc -MM main.c
main.o: main.c hello.h
上述输出告诉我们,编译main.o依赖main.c和hello.h两个文件。
因此,我们可以利用GCC的这个功能,对每个.c文件都生成一个依赖项,通常我们把它保存到.d文件中,再用include引入到Makefile,就相当于自动化完成了每个.c文件的精准依赖。
我们改写上一节的Makefile如下:
# 列出所有 .c 文件:
SRCS = $(wildcard *.c)
# 根据SRCS生成 .o 文件列表:
OBJS = $(SRCS:.c=.o)
# 根据SRCS生成 .d 文件列表:
DEPS = $(SRCS:.c=.d)
TARGET = world.out
# 默认目标:
$(TARGET): $(OBJS)
$(CC) -o $@ $^
# 模式规则,表示对于每个 .c 文件,生成一个对应的 .d 依赖文件。
%.d: %.c
# 删除已有的 .d 文件($@ 是目标文件,即 .d 文件)
rm -f $@; \
# 使用 gcc 的 -MM 选项生成依赖信息,$< 是第一个依赖文件(即 .c 文件)。这会生成包含头文件依赖的 .d 文件,并将结果存储在临时文件 $@.tmp 中
$(CC) -MM $< >$@.tmp; \
# 使用 sed 命令处理 .d 文件内容,将依赖的 .o 文件规则格式化为符合 make 的依赖格式。最终输出到 .d 文件中。
sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.tmp > $@; \
rm -f $@.tmp
# 模式规则:生成 .o 文件
%.o: %.c
$(CC) -c -o $@ $<
clean:
rm -rf *.o *.d $(TARGET)
# 引入所有 .d 文件(相当于在此处复制了.d文件的内容):
include $(DEPS)
这个 Makefile 用于管理项目中的源文件编译、依赖关系和最终目标的生成。首先,它列出了所有的 .c 文件并生成了对应的 .o 和 .d 文件列表。每当 .c 文件有变化时,.d 文件记录的头文件依赖信息会确保只重新编译必要的源文件。Makefile 通过 gcc -MM 选项生成依赖文件,然后,它使用 $(CC) 编译器将 .c 文件编译为 .o 文件,最后通过链接这些 .o 文件生成目标可执行文件 world.out。通过 include $(DEPS) 引入所有 .d 文件内容(引入.c文件的依赖内容),make 能够自动追踪源文件与头文件的依赖,确保文件变动时能正确地触发重新编译,避免不必要的重复编译。clean 目标则用于删除中间生成的文件,保持项目目录整洁。
四、最终Makefile模板
首先要做的事情是创建一个C的目录框架,并且放置一些多续项目都拥有的,基本的文件和目录。一个规范的目录框架可以更好的兼容别人的工作习惯和减少自身对文件执行目录的修改,这也是非常重要的一环,可以极大的便利自身。
以下是一个推荐的C工作目录框架:
project/
│
├── LICENSE # 项目的许可协议
├── README.md # 项目的简要说明
├── Makefile # 项目的构建文件
│
├── bin/ # 存放可执行文件
│
├── build/ # 存放构建过程中生成的文件,如库文件
│
├── src/ # 存放源代码文件,通常包含 .c 和 .h 文件
│ ├── main.c
│ ├── main.h
│ └── hello.c
│
├── tests/ # 存放自动化测试代码
│ ├── test.c
│ └── tess.h
└── minunit.h
- LICENSE:包含了项目的许可协议,说明代码的使用和分发条款。
- README.md:提供项目的简要说明,通常包括项目的功能、如何安装、如何使用等信息。
- Makefile:定义了项目的构建规则,使用
make工具来自动化构建过程。 - bin/:存放生成的可执行文件,通常在构建过程中由
Makefile自动生成。 - build/:存放中间构建文件,如库文件、目标文件等,通常是构建过程中的临时文件夹。
- src/:存放项目的源代码,通常包括
.c文件(源文件)和.h文件(头文件)。 - tests/:存放自动化测试代码,通常用于验证程序功能是否正确。
接下来可以整理出一个通用的编译.c文件的Makefile,对于不同的文件存放情况,只需要简单修改部分路径信息即可。
SRC_DIR = ./src
TEST_DIR = ./tests
BUILD_DIR = ./build
BIN_DIR = ./bin
TARGET = $(BIN_DIR)/main.out
TEST_TARGET = $(BIN_DIR)/test.out
CC = cc
CFLAGS = -Wall -g -std=c99
# ./src/*.c
SRC_SRCS := $(wildcard $(SRC_DIR)/*.c)
# ./tests/*.c
TEST_SRCS := $(wildcard $(TEST_DIR)/*.c)
# ./src/*.c => ./build/*.o
SRC_OBJS := $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.o,$(SRC_SRCS))
# ./tests/*.c => ./build/*.o
TEST_OBJS := $(patsubst $(TEST_DIR)/%.c,$(BUILD_DIR)/%.o,$(TEST_SRCS))
# ./src/*.c => ./build/*.d
SRC_DEPS := $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.d,$(SRC_SRCS))
# ./tests/*.c => ./build/*.d
TEST_DEPS := $(patsubst $(TEST_DIR)/%.c,$(BUILD_DIR)/%.d,$(TEST_SRCS))
# 默认目标:构建src目录下的可执行文件
all: $(TARGET)
# 运行src下编译的可执行程序
run: $(TARGET)
./$(TARGET)
# 构建测试文件目标
test: $(TEST_TARGET)
@echo "building test objects..."
# 运行测试可执行文件
test_run: $(TEST_TARGET)
./$(TEST_TARGET)
# build/*.d 的规则由 src/和tests/*.c文件生成:
$(BUILD_DIR)/%.d: $(SRC_DIR)/%.c $(TEST_DIR)/%.c
@mkdir -p $(dir $@)
$(CC) -MM $< > $@
# build/*.o 的规则由 src/*.c 生成:
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c
@mkdir -p $(dir $@)
$(CC) $(CFLAGS) -c -o $@ $<
# build/*.o 的规则由 test/*.c 生成:
$(BUILD_DIR)/%.o: $(TEST_DIR)/%.c
@mkdir -p $(dir $@)
$(CC) $(CFLAGS) -c -o $@ $<
# 链接src中可执行文件:
$(TARGET): $(SRC_OBJS)
@echo "buiding $@..."
@mkdir -p $(dir $@)
$(CC) -o $(TARGET) $(SRC_OBJS)
# 链接测试可执行文件:
$(TEST_TARGET): $(TEST_OBJS) $(SRC_OBJS)
@echo "buiding $@..."
@mkdir -p $(dir $@)
$(CC) -o $(TEST_TARGET) $(TEST_OBJS) $(SRC_OBJS)
.PHONY: clean build all run test test_run
# 清理 build 目录:
clean:
@echo "Cleaning build directory..."
rm -rf $(BUILD_DIR)/*
# 生成标准的C目录框架
build:
@echo "Creating directory structure..."
mkdir -p $(BUILD_DIR) $(SRC_DIR) $(TEST_DIR) $(BIN_DIR)
touch LICENSE README.md
# 引入所有 .d 文件内的依赖规则:
-include $(SRC_DEPS)
-include $(TEST_DEPS)
该 Makefile 用于编译 C 项目,支持分别编译 src 目录下的源代码生成可执行文件 bin/main.out,以及编译 tests 目录下的测试代码并与 src 代码链接生成测试可执行文件 bin/test.out。使用 make 或 make all 构建主程序,make run 运行主程序,make test 构建测试程序,make test_run 运行测试程序,make clean 清理构建产物,make build 创建项目目录结构。它使用 wildcard 获取源文件列表,patsubst 进行文件名转换,并自动生成依赖关系文件 .d 以实现增量编译,同时通过独立的规则处理 src 和 tests 目录下的 .c 文件,避免命名冲突。
五、常用GCC命令选项
Makefile 中的 gcc 选项用于控制编译过程,设置不同的编译行为。下面是常见的 GCC 编译选项的总结,特别文章的内容中出现过的:
1. 调试相关
-g:启用调试信息。在编译过程中生成调试符号,方便使用调试工具(如gdb)进行调试。这是开发和调试阶段常用的选项。
2. 优化相关
-O2:启用常规优化。在编译时启用常规优化,目的是使程序在运行时更加高效。-O2是一个常用的优化级别,既提供了优化性能,又不会带来过多的编译时间开销。-O3:启用更强的优化。这个优化级别更激进,但可能会增加编译时间,且有时会导致代码行为的微小变化。
3. 警告相关
-Wall:启用所有警告。这是一个常用的选项,它会启用大部分警告信息,有助于开发者及时发现潜在的问题。-Wextra:启用额外的警告。比-Wall更加严格,除了-Wall启用的警告外,还会启用更多的警告信息,帮助发现更多潜在的代码问题。
4. 包含目录
-I<dir>:指定头文件搜索路径。例如,-Isrc表示将src目录添加到头文件的搜索路径中,这样编译器在查找头文件时会首先检查src目录。
5. 动态链接相关
-rdynamic:保留符号信息。这个选项使得在生成的可执行文件中保留符号表,使得动态库能够获取符号信息,通常用于需要动态加载符号的程序。-fPIC:生成位置无关代码(Position Independent Code)。该选项用于编译动态库时,使得生成的目标文件可以在内存中任意位置加载,适用于动态库的构建。
6. 条件编译
-DNDEBUG:定义宏NDEBUG,通常用于禁用调试代码。在发布版本中,NDEBUG宏常常被定义,以关闭调试相关的代码或断言(如assert)。
7. 库链接相关
-ldl:链接动态加载库(libdl)。此库提供了动态加载共享库的功能,适用于需要动态加载的程序。$(OPTLIBS):一般是用户自定义的库链接选项,用于将项目依赖的其他库链接进来。
8. 目标文件相关
-c:仅编译源文件,而不进行链接。通常用于生成目标文件(.o文件)。-MM:生成依赖文件,主要用于自动化构建系统。会输出.d文件,这些文件描述了源文件与头文件之间的依赖关系,通常用于构建新的依赖规则。
9. 生成共享库相关
-shared:生成共享库(动态库),而不是静态库或可执行文件。通常与-fPIC一起使用,用于构建动态链接库(.so文件)。
10. 其他
-o <file>:指定输出文件名。例如,-o $(TARGET)将编译结果输出到目标文件中。-l<library>:链接指定的库。例如,-lm表示链接数学库libm。-rcs(在ar命令中使用):ar是用来创建静态库的工具,-rcs选项分别表示创建库文件、替换已有文件以及生成符号表(加速链接)。
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐


所有评论(0)