「Linux 系统编程」Makefile 基本使用方法

1 Makefile 简介

1.1 什么是 Makefile

Makefile 是 Linux/Unix 下的自动化构建工具,用于管理代码编译、安装、清理等流程。常用于编译 C/C++ 项目、自动化测试、软件打包等。

通过定义 目标(target)、依赖(dependencies)执行命令(commands),自动检测文件改动,仅重新构建受影响的部分。

Makefile 包含以下特点:

  • 增量编译:只编译修改过的文件
  • 依赖管理:自动处理文件间的依赖关系
  • 跨平台支持:通过规则抽象编译细节

基本用法:执行 make 自动编译最新改动,make clean 清理生成文件。


1.2 Makefile 文件的命名规则

在项目目录下执行 make 命令,默认会按照 GNUmakefilemakefileMakefile 的顺序查找 Makefile 编译文件。

通常推荐以 Makefile 作为项目 Makefile 编译文件的名称。此时,只需要执行如下命令即可对项目进行自动编译:

1
make

如果要自定义编译文件的名称,例如自定义名称为 myMakefile,需要通过 -f--file 参数显式指定:

1
make -f myMakefile


2 Makefile 基础语法和依赖规则

2.1 基础语法结构

1
2
target: dependencies
command
  • target:生成的 目标文件(如 app)或操作名(如 clean)
  • dependencies:生成 target 所需的文件或目标(空格分隔)
  • command生成目标的命令(必须以 Tab 开头,不能用空格)

示例:

1
2
app: main.cpp utils.cpp
g++ main.cpp utils.cpp -o app

假设有 utils.cpputils.hmain.cpp 文件内容分别如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// utils.cpp
#include <iostream>
#include <string>
#include "utils.h"

void hello(const std::string& s){
std::cout << "Hello, " << s << std::endl;
}

int add(int a, int b){
return a + b;
}

long long factor(int n){
long long res = 1;
for(int i = 2; i <= n; i ++ ){
res *= i;
}
return res;
}
1
2
3
4
5
6
7
8
9
10
11
// utils.h
#ifndef _UTILS_H_
#define _UTILS_H_

#include <string>

void hello(const std::string&);
int add(int, int);
long long factor(int);

#endif
1
2
3
4
5
6
7
8
9
10
11
12
// main.cpp
#include <iostream>
#include "utils.h"
using namespace std;

int main(){
hello("Kirisame Marisa");
cout << "3 + 4 = " << add(3, 4) << endl;
cout << "5! = " << factor(5) << endl;

return 0;
}

Makefile 文件内容如下:

1
2
app: main.cpp utils.cpp
g++ main.cpp utils.cpp -o app

执行 make 命令进行编译得到可执行程序:

上述过程实现了一个简单的 使用 Makefile 编译项目 的示例。


2.2 依赖规则

在之前 2.1 基础语法结构 的示例中,假如有很多个 .cpp 文件,后续有一个被修改而其它并未被修改,此时再执行 make 对项目进行重新编译,会发现所有的 .cpp 也都会被重新编译。

编译往往需要很多的时间,显然之前 2.1 基础语法结构 中示例的做法是比较耗时的。

我们可以借助 Makefile 中的 依赖规则,使得只有 被修改的文件部分进行目标重建

  1. 目标重建条件

    • 依赖文件dependencies)的时间晚于 目标文件target)的时间时。
    • 目标文件不存在 时。
  2. 依赖传递

    根据目标重建条件,可以将 2.1 基础语法结构 的示例 修改如下:

    1
    2
    3
    4
    5
    6
    7
    8
    app: main.o utils.o
    g++ main.o utils.o -o app

    main.o: main.cpp
    g++ -c main.cpp -o main.o

    utils.o: utils.cpp
    g++ -c utils.cpp -o utils.o

    在上面修改过后的 Makefile 中,app 依赖的是 main.outil.o 文件;main.o 依赖的是 main.cpp 文件;utils.o 依赖的是 utils.cpp 文件。

    执行上述 Makefile 文件编译项目。在项目目录中也生成了每个 .cpp 对应的 .o 目标文件:

    假设 utils.cpp 被修改了,其它文件均没有修改。此时执行 make 命令重新编译文件,发现只执行了 g++ -c utils.cpp -o utils.og++ main.o utils.o -o app 命令。根据依赖规则,只有 utils.cpp 为起点的一条依赖链上的目标文件重新进行了构建


2.3 最终目标指定

在 Makefile 中,最终目标(即默认目标)通过以下规则确定:

  1. 位置优先原则

    文件中的 第一个有效目标 会被视为 默认目标。当用户仅输入 make 命令时,将自动构建此目标。

    1
    2
    3
    4
    5
    6
    7
    8
    app: main.o utils.o   # 默认第一个有效目标就是最终目标
    g++ main.o utils.o -o app

    main.o: main.cpp
    g++ -c main.cpp -o main.o

    utils.o: utils.cpp
    g++ -c utils.cpp -o utils.o
  2. 显式声明最终目标

    如果 Makefile 文件中 app 不在第一个有效目标的位置,此时可以通过 显式声明 .DEFAULT_GOAL 来指定最终目标

    基本语法如下:

    1
    .DEFAULT_GOAL := 目标名  # 注意变量赋值用 :=,且目标名不需要依赖和命令

    示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    .DEFAULT_GOAL := app  # 显式声明最终目标

    main.o: main.cpp
    g++ -c main.cpp -o main.o

    app: main.o utils.o
    g++ main.o utils.o -o app

    utils.o: utils.cpp
    g++ -c utils.cpp -o utils.o
  3. 使用 all 指定最终目标

    基本格式如下:

    1
    2
    all: 目标1 目标2 ...
    [命令]

    all 后面跟随的是它所依赖的其他目标,当执行 make all 时,会先执行这些依赖目标,然后再执行 all 自身的命令(如果有的话)。

    特别的,当 all 放在 Makefile 文件的第一行,执行 make 命令进行编译和 make all 的效果就一样了。

    示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    all: app

    main.o: main.cpp
    g++ -c main.cpp -o main.o

    app: main.o utils.o
    g++ main.o utils.o -o app

    utils.o: utils.cpp
    g++ -c utils.cpp -o utils.o
  4. all 结合 .DEFAULT_GOAL 指定最终目标

    可以将 all 结合 .DEFAULT_GOAL 来指定最终目标。这样即便 all 不放在第一个目标位置,make 命令也会执行 all 构建程序。

    示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    .DEFAULT_GOAL := all

    main.o: main.cpp
    g++ -c main.cpp -o main.o

    all: app

    app: main.o utils.o
    g++ main.o utils.o -o app

    utils.o: utils.cpp
    g++ -c utils.cpp -o utils.o

    在上面的示例中,即便第一个目标为 main.o,结合 all.DEFAULT_GOAL 使得执行 make 命令构建的最终目标为程序 app



3 Makefile 两个常用函数和 clean

3.1 两个常用函数 wildcard 和 patsubst

两个常用函数 wildcardpatsubst 主要是为了提升 Makefile 文件编译的可扩展性。

  1. $(wildcard pattern)

    获取当前工作目录下 匹配模式文件列表

    示例:

    1
    SRC = $(wildcard *.cpp)  # 获取所有 .cpp 文件

    在上面的示例中,获取所有当前目录下的 .cpp 文件,并将文件名组成列表,赋值给变量 SRC

  2. $(patsubst pattern, replacement, text)

    模式字符串替换。具体为将参数 3 text 中包含参数 1 pattern 中的部分,全部替换为参数 2 replacement

    示例:

    1
    OBJ = $(patsubst %.cpp, %.o, $(SRC))

    在上面的示例中,将 SRC 文件列表中所有后缀为 .cpp 文件的后缀替换为 .o


3.2 clean 清理项目生成中间件

在 Makefile 中,clean 是一个常用的自定义目标,主要用于 清理项目构建过程中生成的中间文件(如 .o 目标文件)、最终可执行文件日志 等临时文件,以便重新构建项目或清理工作目录。

基本用法如下:

1
2
clean:
rm -f *.o app

在清理编译生成的文件时,通常使用 .PHONYclean 声明为伪目标(避免与同名文件冲突):

1
2
3
.PHONY: clean  # 声明clean为伪目标,确保即使存在clean文件也能执行
clean:
rm -f *.o app

在工作目录下执行 make clean 即可清理文件。

在清理文件时,通常会先加上 -n 参数选项(模拟执行),即执行 make clean -n。这样执行之后会显示 clean 目标将要执行的所有命令,但不会实际执行这些命令。这相当于做一次 “预演”,确认清理操作会删除哪些文件,避免误操作。


3.3 常用函数及 clean 使用

由上面的两个常用函数 wildcardpatsubst 以及 clean 清理文件,可以将先前示例的 Makefile 文件修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.DEFAULT_GOAL := all  # 指定默认目标为 all
.PHONY: all clean # all 和 clean 声明为伪目标

SRC = $(wildcard *.cpp) # get .cpp list
OBJ = $(patsubst %.cpp, %.o, $(SRC)) # replace all .cpp to .o

all: app

app: $(OBJ)
g++ $(OBJ) -o app

main.o: main.cpp
g++ -c main.cpp -o main.o

utils.o: utils.cpp
g++ -c utils.cpp -o utils.o

clean:
rm -f $(OBJ) app

在上面的 Makefile 文件中,将 allclean 都声明为伪目标,避免目录下有同名文件使得编译崩溃;将 main.o utils.o 都替换为 $(OBJ)。执行 make clean 以及 make 命令效果如下:


在 Makefile 的 clean 等目标中,常在 rm 命令前加 -,即 -rm -f $(OBJ) app

这样可以 忽略命令执行的错误,如果 rm 命令执行失败(例如要删除的文件不存在),make 不会终止后续操作,而是继续执行其他命令。



4 Makefile 自动变量和模式规则

4.1 三个核心自动变量

  • $@:在当前规则命令中的 目标文件名称

    1
    2
    app: $(OBJ)
    g++ $^ -o $@ # $@ = "app"
  • $<:在当前规则命令中的 第一个依赖条件

    • 在普通规则中,始终是依赖文件列表的第一个文件。
    1
    2
    main.o: main.cpp
    g++ -c $< -o $@
    • 模式规则 中,自动匹配当前目标的对应文件。
    1
    2
    3
    4
    %.o: %.cpp
    g++ -c $< -o $@ # $< 自动匹配当前 .o 对应的 .c 文件
    # 当构建 main.o 时:$< = "main.c"
    # 当构建 utils.o 时:$< = "utils.c"
  • $^:在当前规则命令中的 所有依赖文件列表

    如果这个列表中有重复项,则去重。文件名以空格分隔。

    1
    2
    app: $(OBJ)
    g++ $^ -o $@ # $^ = "main.o utils.o"

4.2 模式规则

使用 % 通配符定义通用规则:

1
2
3
# 通用规则:将任意 .c 文件编译为 .o 文件
%.o: %.cpp
g++ -c $< -o $@

4.3 静态模式规则

静态模式规则(Static Pattern Rules)是 Makefile 中一种精确控制目标匹配范围的模式规则变体。与普通模式规则不同,它 显式指定应用规则的目标列表,提供更精细的控制。

1
2
targets...: target-pattern: prereq-patterns...
recipe

基本语法示例:

1
2
$(OBJ): %.o: %.cpp
g++ -c $< -o $@

在上面的示例中,指定模式规则给 OBJ 用。当一个项目比较庞大以后,同一种模式规则会有不同生成目标的方式,同一类目标文件也会有不同的依赖(普通模式规则会进行全局匹配),此时就需要指定哪个文件集合用什么模式规则。

示例:

1
2
3
4
5
6
7
8
9
10
SERVER_OBJS = server_main.o server_utils.o
CLIENT_OBJS = client_main.o client_net.o

# 服务器对象特殊编译选项
$(SERVER_OBJS): %.o: %.c
gcc -DSERVER_MODE -c $< -o $@

# 客户端对象不同优化级别
$(CLIENT_OBJS): %.o: %.c
gcc -O3 -c $< -o $@
1
2
3
4
5
6
7
8
9
10
C_SOURCES = main.c utils.c
ASM_SOURCES = startup.asm

# 编译C文件
$(C_SOURCES:.c=.o): %.o: %.c
gcc -c $< -o $@

# 汇编ASM文件
$(ASM_SOURCES:.asm=.o): %.o: %.asm
nasm -f elf64 $< -o $@

4.3 自动变量和模式规则使用

由上面的三个核心自动变量 $@$<$^ 以及模式规则,可以进一步优化之前 3.3 常用函数及 clean 使用 中 的示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.DEFAULT_GOAL := all
.PHONY: all clean

SRC = $(wildcard *.cpp) # get .cpp list
OBJ = $(patsubst %.cpp, %.o, $(SRC)) # replace all .cpp to .o

all: app

app: $(OBJ) # $^ = "main.o utils.o" $@ = "app"
g++ $^ -o $@

$(OBJ): %.o: %.cpp
g++ -c $< -o $@

clean:
rm -f $(OBJ) app

执行 make cleanmake 命令效果如下:



5 Makefile 扩展

可以添加一些自定义变量,例如 ARGS 作为编译过程中添加的参数选项、TARGET 作为最终生成的程序名称等等。

如果 .cpp 文件、.h 文件放置于当前目录的子目录 ./src./include 下(或者其它目录),并且生成的 .o 文件需要放置在 ./obj 目录下,可以修改最终的 Makefile 文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.DEFAULT_GOAL := all  # 指定 all 为默认目标
.PHONY: all clean # 指定 all 和 clean 为伪目标

INCLUDE_PATH = ./include/ # include 路径,存放 .h
SOURCE_PATH = ./src/ # src 路径,存放 .cpp
OBJECT_PATH = ./obj/ # obj 路径,存放生成的 .o
ARGS = -Wall
TARGET = app

SRC = $(wildcard $(SOURCE_PATH)*.cpp) # get .cpp list
OBJ = $(patsubst $(SOURCE_PATH)%.cpp, $(OBJECT_PATH)%.o, $(SRC)) # replace all .cpp to .o

all: $(TARGET)

# $^ = "main.o utils.o" $@ = "app"
$(TARGET): $(OBJ)
g++ $(ARGS) $^ -o $@

$(OBJ): $(OBJECT_PATH)%.o: $(SOURCE_PATH)%.cpp
g++ -c $(ARGS) -I $(INCLUDE_PATH) $< -o $@

clean:
-rm -rf $(OBJ) $(TARGET)

执行程序构建结果如下:


「Linux 系统编程」Makefile 基本使用方法
https://marisamagic.github.io/2025/08/18/20250818/
作者
MarisaMagic
发布于
2025年8月18日
许可协议