Makefile入门指南

        Makefile 是一种用于自动化构建程序的工具脚本,核心作用是定义项目的编译规则,通过分析文件依赖关系,仅重新编译修改过的文件,从而大幅提高项目构建效率。尤其在多文件项目中,手动输入编译命令(如 gcc a.c b.c -o app)会非常繁琐,而 Makefile 可以通过 make 命令一键完成构建。

        Makefile 本身是一个文本文件,不需要下载 —— 它是你根据项目编译规则自己编写的脚本文件。真正需要的是执行 Makefile 的工具 make(通常称为 “make 工具”),这个工具可能需要安装,但多数系统会预装。

先搞清楚:Makefile vs make 工具

  • Makefile:是你手动编写的文本文件(类似 .txt),里面定义了编译规则(比如如何从 .c 文件生成可执行文件)。
  • make 工具:是一个程序,用来读取并执行 Makefile 中的规则,自动完成编译、链接等操作。

Linux会预装make,Windows中使用mingw32-make。

一、Makefile 基本结构

Makefile 的核心是规则(Rule),一个完整的规则格式如下:

目标(target): 依赖项(prerequisites)
    命令(command)  # 命令前必须是 Tab 键(不能用空格)
  • 目标(target):通常是要生成的文件(如 .o 目标文件、可执行文件),也可以是 “伪目标”(如 clean,用于执行清理操作)。
  • 依赖项(prerequisites):生成目标所需要的文件或其他目标(如编译 .o 文件需要对应的 .c 文件)。
  • 命令(command):从依赖项生成目标的具体操作(如 gcc -c a.c -o a.o)。

二、核心概念与基础用法

1. 最简单的 Makefile

假设项目有 main.c 和 tool.c 两个源文件,要编译为可执行文件 app,Makefile 可写为:

# 目标:app;依赖:main.o 和 tool.o
app: main.o tool.o
    gcc main.o tool.o -o app  # 链接生成可执行文件

# 目标:main.o;依赖:main.c
main.o: main.c
    gcc -c main.c -o main.o  # 编译 main.c 为目标文件

# 目标:tool.o;依赖:tool.c
tool.o: tool.c
    gcc -c tool.c -o tool.o  # 编译 tool.c 为目标文件

执行 make 命令时,会自动按规则从依赖项生成目标:先编译 .c 为 .o,再链接 .o 为 app

2. 伪目标(.PHONY)

如果目标不是实际文件(如 clean 用于删除编译产物),需要声明为伪目标避免当前目录存在同名文件时 make 误判。示例:

# 声明 clean 为伪目标
.PHONY: clean

# 清理编译生成的文件
clean:
    rm -f app *.o  # 删除可执行文件和所有 .o 文件

执行 make clean 即可触发清理操作。

3. 变量(Variables)

变量用于简化 Makefile 编写(避免重复书写编译器、编译选项等),定义格式:变量名=值,引用格式:$(变量名)

常见预定义变量:

  • CC:默认编译器(通常是 cc,可手动指定为 gcc)。
  • CFLAGS:编译选项(如 -Wall 开启警告,-I 指定头文件路径)。
CC = gcc          # 指定编译器为 gcc
CFLAGS = -Wall -g # 编译选项:开启警告 + 调试信息

app: main.o tool.o
    $(CC) $^ -o $@               # $^ 表示所有依赖,$@ 表示目标

main.o: main.c
    $(CC) $(CFLAGS) -c $< -o $@  # $< 表示第一个依赖

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

.PHONY: clean
clean:
    rm -f app *.o
  • 及时变量(简单赋值,:=:变量在定义时立即展开并确定值,后续对被引用变量的修改不会影响当前变量。

  • 延时变量(递归赋值,=:变量在被使用时才展开并确定值,而非定义时。每次使用变量时,都会重新计算其值,因此后续对被引用变量的修改会影响当前变量。

  • 条件赋值(?=,仅当变量未被定义过时才赋值,若已定义则不生效(无论之前是:=还是=)。
A := 已存在的值
A ?= 新值  # A 已定义,此赋值无效
B ?= 我是新的  # B 未定义,赋值生效

all:
    @echo A=$(A)  # 输出 A=已存在的值
    @echo B=$(B)  # 输出 B=我是新的
2. 追加赋值(+=

用于给变量追加内容,保持原变量的展开特性(及时变量仍及时,延时变量仍延时)。示例:

# 及时变量追加
A := 123
A += 456  # 等价于 A := $(A) 456(立即展开,A 变为“123 456”)

# 延时变量追加
B = 123
B += 456  # 等价于 B = $(B) 456(保持延时,使用时展开)
C := 789
B += $(C)  # B 最终逻辑:$(B) 456 $(C)

all:
    @echo A=$(A)  # 输出 A=123 456
    @echo B=$(B)  # 输出 B=123 456 789(使用时展开所有引用)
4. 自动变量(Automatic Variables)

自动变量用于简化命令中的依赖 / 目标引用,避免重复书写,常用如下:

  • $@:当前规则的目标(如 appmain.o)。
  • $<:当前规则的第一个依赖项(如 main.c)。
  • $^:当前规则的所有依赖项(如 main.o tool.o)。
  • $?:所有比目标新的依赖项(用于增量编译)。
5. 模式规则(Pattern Rules)

当多个目标的编译规则相似时(如所有 .c 生成 .o),可使用模式规则统一处理,格式:%.o: %.c% 为通配符)。

示例(用模式规则替代单个 .o 规则):

CC = gcc
CFLAGS = -Wall -g

# 所有 .o 文件的通用编译规则
%.o: %.c
    $(CC) $(CFLAGS) -c $< -o $@

# 链接规则
app: main.o tool.o
    $(CC) $^ -o $@ 

.PHONY: clean
clean:
    rm -f app *.o

此时,任何 .c 文件都能通过该规则生成对应的 .o,无需单独定义。

函数

在 Makefile 中,函数用于处理字符串、文件路径、循环逻辑等,扩展了 Makefile 的灵活性。函数调用格式为 

$(函数名 参数) 或 ${函数名 参数}

参数间用逗号分隔(注意参数内的空格可能被保留)。以下是常用函数的分类说明:

一、字符串处理函数
1. subst:字符串替换
  • 格式:$(subst from,to,text)
  • 功能:将 text 中所有 from 字符串替换为 to
  • 示例:
    $(subst abc,123,abcxyzabc)  # 结果:123xyz123
    
2. patsubst:模式替换(支持通配符)
  • 格式:$(patsubst pattern,replacement,text)
  • 功能:将 text 中符合 pattern(可含 % 通配符)的字符串替换为 replacement% 对应原字符串的匹配部分)。
  • 示例:
    $(patsubst %.c,%.o,a.c b.c)  # 结果:a.o b.o(将所有.c文件替换为.o)
    $(patsubst x%,y%,x1 x2)      # 结果:y1 y2(替换前缀x为y)
    
二、文件路径处理函数
1. dir:提取目录部分
  • 格式:$(dir names)
  • 功能:返回文件名列表 names 中每个文件的目录部分(以 / 结尾,无目录则返回 ./)。
  • 示例:
    $(dir src/a.c b.c)  # 结果:src/ ./(src/a.c的目录是src/,b.c的目录是当前目录./)
    
2. notdir:提取文件名部分
  • 格式:$(notdir names)
  • 功能:返回文件名列表 names 中每个文件的文件名(去除目录部分)。
  • 示例:
    $(notdir src/a.c b.c)  # 结果:a.c b.c
    
3. basename:去除后缀
  • 格式:$(basename names)
  • 功能:去除文件名列表 names 中每个文件的后缀(最后一个 . 后的部分)。
  • 示例:
    $(basename a.c b.txt)  # 结果:a b
    
4. addsuffix / addprefix:添加后缀 / 前缀
  • 格式:$(addsuffix suffix,names) / $(addprefix prefix,names)
  • 功能:给 names 中的每个文件名添加 suffix 后缀或 prefix 前缀。
  • 示例:
    $(addsuffix .c,a b)    # 结果:a.c b.c(添加后缀)
    $(addprefix src/,a.c)  # 结果:src/a.c(添加前缀)
    
三、文件匹配与过滤函数
1. wildcard:匹配实际文件
  • 格式:$(wildcard pattern)
  • 功能:返回当前目录中符合 pattern(如 *.c)的所有实际存在的文件列表。
  • 示例:
    SRCS = $(wildcard *.c)  # 若当前目录有a.c、b.c,则SRCS=a.c b.c
    
2. filter:保留符合模式的文件
  • 格式:$(filter pattern...,text)
  • 功能:从 text 中保留所有符合 pattern(可多个模式)的字符串。
  • 示例:
    $(filter %.c %.h,a.c b.o c.h)  # 结果:a.c c.h(保留.c和.h文件)
    
3. filter-out:排除符合模式的文件
  • 格式:$(filter-out pattern...,text)
  • 功能:从 text 中排除所有符合 pattern 的字符串(与 filter 相反)。
  • 示例:
    $(filter-out %.o,a.c b.o c.h)  # 结果:a.c c.h(排除.o文件)
    
四、循环与逻辑函数
1. foreach:循环处理列表
  • 格式:$(foreach var,list,text)
  • 功能:遍历 list 中的每个元素,将其赋值给变量 var,然后执行 text(可引用 var),最终拼接所有 text 的结果。
  • 示例:
    $(foreach f,a b,c$(f))  # 遍历a、b,分别执行c$f,结果:ca cb
    
2. if:条件判断
  • 格式:$(if condition,then-part[,else-part])
  • 功能:若 condition 非空,则返回 then-part;否则返回 else-part(可选)。
  • 示例:
    $(if a,true,false)  # 结果:true(condition为a,非空)
    $(if ,true,false)   # 结果:false(condition为空)
    
五、执行与调用函数

用于执行 Shell 命令、调用自定义逻辑等。

1. shell:执行 Shell 命令
  • 格式:$(shell command)
  • 功能:执行 Shell 命令 command,返回命令输出(换行符会被转为空格)。
  • 示例:
    FILES = $(shell ls *.txt)  # 等价于wildcard,但依赖Shell环境
    DATE = $(shell date +%Y%m%d)  # 获取当前日期,如20251109
    
2. call:调用自定义变量(模拟函数)
  • 格式:$(call var,param1,param2...)
  • 功能:将变量 var 作为 “函数模板”,用 param1, param2... 替换模板中的 $(1), $(2)...(参数占位符)。
  • 示例:
    # 定义模板变量(参数$(1)、$(2)为占位符)
    template = $(1)_$(2).txt
    
    # 调用模板,传递参数a和b
    result = $(call template,a,b)  # result的值为a_b.txt
    
3. eval:动态执行 Makefile 语法
  • 格式:$(eval text)
  • 功能:将 text 作为 Makefile 语法解析并执行(可动态生成规则或变量)。
  • 示例:
    # 动态生成规则:a.o: a.c
    rule = a.o: a.c
    $(eval $(rule))  # 执行后,Makefile中会添加规则a.o: a.c
    
六、变量相关函数

用于查询变量的来源或状态。

origin:查询变量来源
  • 格式:$(origin var)
  • 功能:返回变量 var 的定义来源,常见结果:
    • undefined:未定义;
    • file:在当前 Makefile 中定义;
    • environment:来自环境变量;
    • default:Make 的默认变量(如 CC)。
  • 示例:
    $(origin CC)  # 若未自定义CC,返回default(默认值为cc)
    
常用组合示例

实际使用中,函数常组合使用,例如自动生成目标文件列表:

# 1. 获取所有.c源文件
SRCS = $(wildcard src/*.c)

# 2. 将.c文件路径转换为.o文件(如src/a.c → obj/a.o)
OBJS = $(patsubst src/%.c,obj/%.o,$(SRCS))

# 3. 编译所有.o文件
all: $(OBJS)
obj/%.o: src/%.c
	gcc -c $< -o $@

通过这些函数,可大幅简化 Makefile 的编写,尤其在处理大量文件或动态逻辑时非常高效。

三、进阶用法

1. 函数(Functions)

Makefile 提供内置函数用于文件查找、字符串处理等,常用函数:

  • wildcard:查找匹配的文件,如 $(wildcard *.c) 会返回当前目录所有 .c 文件。
  • patsubst:字符串替换,如 $(patsubst %.c,%.o,$(SRC)) 会把 .c 替换为 .o

示例(自动获取源文件和目标文件列表):

CC = gcc
CFLAGS = -Wall -g #开启警告 生成调试信息

# 自动获取所有 .c 源文件
SRC = $(wildcard *.c)  # 假设返回 main.c tool.c

# 自动生成对应的 .o 目标文件(main.o tool.o)
OBJS = $(patsubst %.c,%.o,$(SRC))

app: $(OBJS)
    $(CC) $^ -o $@ 

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

.PHONY: clean
clean:
    rm -f app $(OBJS)

此时新增 .c 文件,无需修改 Makefile,make 会自动识别并编译。

2. 嵌套 Makefile(子目录编译)

大型项目常按模块拆分到子目录(如 src/lib/),主 Makefile 可通过 make -C 子目录 调用子目录的 Makefile。

示例(主 Makefile 调用 src/ 子目录):

# 主 Makefile
SUBDIRS = src  # 子目录列表

# 遍历子目录执行 make
all:
    for dir in $(SUBDIRS); do \
        make -C $$dir; \  # -C 进入子目录执行 make
    done

# 遍历子目录执行 make clean
clean:
    for dir in $(SUBDIRS); do \
        make -C $$dir clean; \
    done

.PHONY: all clean

子目录 src/Makefile 可按常规写法编写(如编译 src/ 下的文件)。

3. 条件判断(Conditionals)

根据不同条件(如操作系统、编译模式)执行不同规则,常用语法:

  • ifeq (值1, 值2):判断值 1 和值 2 是否相等。
  • ifneq (值1, 值2):判断值 1 和值 2 是否不等。

示例(区分 debug/release 模式):

CC = gcc
OBJS = main.o tool.o

# 若指定 make debug,则开启调试模式
ifeq ($(mode), debug)
CFLAGS = -Wall -g  # 调试模式:保留调试信息
else
CFLAGS = -Wall -O2  # 默认 release 模式:优化编译
endif

app: $(OBJS)
    $(CC) $^ -o $@

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

.PHONY: clean
clean:
    rm -f app $(OBJS)

执行 make mode=debug 会按调试模式编译,make 则按 release 模式编译。

4. 包含其他文件(include)

通过 include 指令可引入其他 Makefile(如公共配置、子模块规则),实现模块化管理。

示例:

# 引入公共编译选项
include common.mk

# 引入子模块规则
include src/sub.mk

app: $(OBJS)
    $(CC) $^ -o $@

总结

Makefile 通过规则定义、变量、函数等机制,实现了项目的自动化、增量构建,是 C/C++ 项目中最常用的构建工具。掌握其基础规则和进阶用法,可大幅提升大型项目的开发效率。

posted @ 2025-11-16 16:56  mc12356  阅读(46)  评论(0)    收藏  举报  来源