欢迎光临
我们一直在努力

Makefile 手把手教你如何让其写的“看不懂”(新手入门笔记)(从简单到精通)

1. 什么是Makefile?

Makefile 是一个存放编译规则的文件,它的核心作用是根据文件的时间戳,判断当前文件是否需要参与本次编译,从而实现高效编译。

Makefile 主要解决三个核心问题:

  • 只需编写一次编译规则,后续无需重复输入复杂的编译命令;

  • 实现增量编译:改了哪个文件,就只重新编译哪个文件,避免重复编译所有文件,节省时间;

  • 一条 make 命令即可完成整个工程的编译,简化操作。

2. 什么是make?

make 是一个可执行程序,通常存放在 /usr/bin/ 路径下。当在终端执行 make 命令时,它会自动解析当前目录下的 Makefile 文件,并按照文件中定义的规则,编译当前目录下的工程。

3. 直接上手写一个简单的Makefile

先准备一个简单的C文件 test.c

#include <stdio.h>

int main(int argc, const char * argv[])
{
printf("hello world\\n");
return 0;
}

在同一目录下新建 Makefile 文件,内容如下:

test:test.c
gcc test.c -o test
clean: # clean是伪目标(不会生成名为clean的文件),执行 make clean 即可触发
rm test

看不懂没关系,下面我们逐步拆解基础知识,再一步步优化这个 Makefile。

4. 基础知识讲解

Makefile 的核心是「规则(rule)」,每个规则的基本格式如下:

target: dependencies
command

  • target(目标):可以是要生成的目标文件(如 test、main.o),也可以是一个动作名称(如 all、clean);

  • dependencies(依赖):生成当前目标所需要的文件或其他目标(如生成 test 依赖test.c);

  • command(命令):构建当前目标的具体命令(如 gcc test.c -o test),必须以 Tab 键开头(绝对不能用4个或8个空格代替,否则会报错)。

5. 逐步优化Makefile(从“看得懂”到“看不懂”)

先准备3个文件:add.c、add.h、main.c(模拟多文件工程),先写一个简单的 Makefile:

main:main.c add.c add.h
gcc main.c add.c add.h -o main
clean:
rm main

这个写法很简单,但存在一个问题:无法实现增量编译——只要修改了其中任意一个文件,所有文件都会被重新编译,效率很低。

因此我们优化一下,将每个 .c 文件单独编译成 .o 目标文件,再链接成可执行文件:

main:main.o add.o
c main.o add.o -o main # 注意:原代码多写了sub.o,此处修正(无sub相关文件)
main.o:main.c
main.c -o main.o # -c 表示仅编译,不链接,生成.o文件
add.o:add.c
-c add.c -o add.o
clean:
main rm *.o gcc gcc -c gc

注意:无需在依赖中写.h 文件,编译器会自动查找并包含头文件。

这样修改后,就能实现增量编译了:改了哪个 .c 文件,就只重新编译对应的 .o 文件,再链接生成可执行文件。但这样写还是太直观,我们继续优化,让它更“专业”。

5.1 引入Makefile变量

Makefile 中的变量无需定义类型,随用随写即可。

  • 变量赋值:var=456;

  • 变量使用:通过$(变量名) 或 ${变量名} 获取变量值(前面必须加 $);

  • 命令回显:命令前加 @,可以取消命令执行时的回显(避免终端显示冗余的命令本身);

  • 注释:用 # 开头,后面的内容为注释。

示例:

var1=123
all:
echo $(var1) # 终端只显示123,不显示echo $(var1)
# 这是一条注释,不会被执行 @ @

5.2 变量的4种赋值方式

(1)延迟赋值 =

赋值时不立即确定变量值,而是在 Makefile 解析到最后一次赋值时,才确定变量的最终值。

var1=123
var2=$(var1) # 此时不确定var2的值,等待var1的最后一次赋值
var1=456

all:
@echo $(var2) # 输出456(取var1最后一次赋值的结果)
@echo $(var1) # 输出456

(2)立即赋值 :=

赋值时立即确定变量值,后续修改原变量,不会影响已赋值的变量。

var1=123
var2:=$(var1) # 立即赋值,var2的值固定为当前var1的值(123)
var1=456

all:
echo $(var2) # 输出123(不受var1后续修改影响)
cho $(var1) # 输出456 @e @

(3)附加赋值 +=

在原有变量值的末尾追加新内容,类似 C 语言中的 strcat 函数。

var1=123
var2=hello
var2 += $(var1) # 在var2末尾追加var1的值

all:
ho $(var2) # 输出hello 123 @ec

(4)询问赋值 ?=

询问变量是否已经赋值:如果之前已经赋值,本次赋值无效;如果之前未赋值,本次赋值生效。

var1=123
# var2=hello # 注释掉,模拟var2未赋值
var2 ?= $(var1) # var2未赋值,本次赋值生效

all:
cho $(var2) # 输出123 @e

(5)命令行赋值

除了在 Makefile 中赋值,还可以在执行 make 命令时,通过命令行直接给变量赋值,优先级高于 Makefile 中的赋值。

var2 ?= 123
all:
echo $(var2) @

执行命令及结果:

  • make → 输出 123(Makefile 中赋值生效);

  • make var2=345 → 输出 345(命令行赋值覆盖 Makefile 中的赋值)。

5.3 用变量优化Makefile

结合变量赋值,我们优化之前的 Makefile,让它更简洁、更灵活:

TARGET?=main # 可执行文件名,支持命令行覆盖
OBJs:= main.o add.o # 目标文件列表,立即赋值
CC?=gcc # 编译器,支持命令行覆盖(如 make CC=clang)
Cflags:= -c -o # 编译选项(-c:仅编译,-o:指定输出文件)

$(TARGET):$(OBJs)
) $(OBJs) -o $(TARGET) # 链接所有.o文件,生成可执行文件
main.o:main.c
(CC) main.c $(Cflags) main.o # 编译main.c生成main.o
add.o:add.c
CC) add.c $(Cflags) add.o # 编译add.c生成add.o
clean:
BJs) $(TARGET) # 清理生成的.o文件和可执行文件 rm $(O $( $ $(CC

这样修改后,Makefile 已经开始“看不懂”了,但我们还能继续优化!

5.4 引入自动变量

Makefile 提供了几个常用的自动变量,可替代重复的目标名、依赖名,让代码更简洁(仅在规则中生效):

  • $@:当前规则的目标名;

  • $<:当前规则的第一个依赖名;

  • $^:当前规则的所有依赖名(去重);

  • $*:仅在模式规则中生效,匹配% 对应的文件名主干(等价于“去掉扩展名的目标”,但普通规则中使用为空)。

示例(直观理解自动变量):

main.o:main.c add.c
cho $@ # 输出 main.o(目标名)
o $* # 输出 main(模式规则中,去掉扩展名的目标主干)
$< # 输出 main.c(第一个依赖)
@echo $^ # 输出 main.c add.c(所有依赖) @echo @ech @e

用自动变量优化 Makefile,去掉重复的文件名:

TARGET?=main
OBJs:= main.o add.o sub.o
CC?=gcc
Cflags:= -c -o

$(TARGET):$(OBJs)
$(CC) $^ -o $@
main.o:main.c
$(CC) $< $(Cflags) $@
add.o:add.c
$(CC) $< $(Cflags) $@
clean:
rm $(OBJs) $(TARGET)

现在的 Makefile 已经和最开始的版本完全不一样了,但我们还能继续优化!

5.5 引入模式通配符 %

% 是 Makefile 特有的模式通配符,核心作用是:替代重复的单个规则,批量匹配“相同主干、不同后缀”的文件,实现批量编译。

比如,所有 .c 文件编译成对应的 .o 文件,无需为每个 .c 文件单独写规则,用 %.o:%.c 即可批量匹配。

用 % 继续优化 Makefile,去掉重复的 .o 编译规则:

TARGET?=main
OBJs:= main.o add.o sub.o
CC?=gcc
Cflags:= -c -o

$(TARGET):$(OBJs)
$(CC) $^ -o $@

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

clean:
rm $(OBJs) $(TARGET)

到这里,Makefile 已经足够“专业”(看不懂)了,但我们还要加一个知识点,让它更健壮!

5.6 伪目标声明 .PHONY

.PHONY 是 Makefile 的伪目标声明指令,用于标记“不是实际文件,仅用于执行命令”的目标(如 clean、all)。

Makefile 的核心设计是「构建文件」,默认认为所有目标都是“要生成的文件”,执行 make 目标 时,会先检查:

  • 该目标文件是否存在;

  • 目标文件的修改时间是否比依赖文件新。

  • 如果满足“文件存在且最新”,make 会直接提示 目标 is up to date,跳过目标对应的命令——这会导致 clean 这类“执行命令”的目标失效。

    比如:如果当前目录下误创建了一个名为 clean 的文件,执行 make clean 时,make 会把 clean 当成“要检查的文件”,发现文件存在且无依赖,直接跳过 rm 命令,导致清理失败。

    此时,加上 .PHONY 声明,就能避免这种问题——它会告诉 make:“这个目标不是文件,忽略文件检查,直接执行命令”。

    最终优化版(最健壮、最专业):

    TARGET?=main
    OBJs:= main.o add.o sub.o
    CC?=gcc
    Cflags:= -c -o

    $(TARGET):$(OBJs)
    $(CC) $^ -o $@

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

    .PHONY: clean
    clean:
    rm $(OBJs) $(TARGET)

    6. 新手必记注意事项

    • 命令必须以 Tab 键 开头,不能用空格代替,否则会报错;

    • 变量赋值的4种方式:=(延迟)、:=(立即)、+=(附加)、?=(询问),根据需求选择;

    • 自动变量仅在规则中生效,$* 仅在模式规则中有效;

    • % 是 Makefile 特有通配符,用于批量匹配,需在目标和依赖中配对使用;

    • clean、all 等“命令型目标”,必须加 .PHONY 声明;

    • 清理命令 rm 建议加 -f,避免文件不存在时报错,更健壮。

    赞(0)
    未经允许不得转载:171主机测评 » Makefile 手把手教你如何让其写的“看不懂”(新手入门笔记)(从简单到精通)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址