Martian148's blog Martian148's blog
首页
  • ICPC 算法笔记
  • ICPC 算法题解
  • 体系结构
  • 高等数学
  • 线性代数
  • 概率论与数理统计
  • 具体数学
  • Martian148的奇思妙想
  • 游记
  • 通识课笔记
关于
  • useful 网站
  • 友情链接
  • 分类
  • 归档

Martian148

一只热爱文科的理科生
首页
  • ICPC 算法笔记
  • ICPC 算法题解
  • 体系结构
  • 高等数学
  • 线性代数
  • 概率论与数理统计
  • 具体数学
  • Martian148的奇思妙想
  • 游记
  • 通识课笔记
关于
  • useful 网站
  • 友情链接
  • 分类
  • 归档
  • ACM - ICPC

  • 编程语言

  • 体系结构

  • Web

  • 人工智能

  • 计算机网络

  • 数据库

  • 编程工具

    • Git使用指南
    • Vim 使用指南
    • Makefile 使用指南
      • 安装 Make
      • 基础操作
        • 规则
        • 伪目标
      • 使用 makefile 编译 C 程序
      • 使用隐式规则
      • 使用变量
        • 内置变量
        • 自动变量
      • 使用规则模式
      • 自动生成依赖
    • valgrind 使用指南
  • 计算机科学
  • 编程工具
martian148
2025-04-26
目录

Makefile 使用指南

# 安装 Make

在 MacOS 的终端中输入

brew install make gcc
1

会自动安装相关工具

我们可以使用 make -v 来验证安装是否成功

(base) martian148@Mac ~ % make -v
GNU Make 3.81
Copyright (C) 2006  Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.

This program built for i386-apple-darwin11.3.0
1
2
3
4
5
6
7
8

# 基础操作

假设我们现在有 3 个文本文件 a.txt,b.txt,c.txt

现在,我们需要合并 a.txt 和 b.txt 生成中间文件 m.txt,然后用 m.txt 和 c.txt 合并生成 x.txt

image-20250427130943935

现在我们来编写 makefile

# 规则

编写的格式为

目标文件: 依赖文件1 依赖文件2
	命令
1
2

在这里,我们生成 m.txt 的命令就是

m.txt: a.txt b.txt
	cat a.txt b.txt > m.txt
1
2

同理,我们能得出生成 x.txt 的命令

x.txt: m.txt c.txt
	cat m.txt c.txt > x.txt
1
2

根据 make 的默认第一条规则,就是认为 x.txt 为目标文件,所以需要吧 x.txt 放在前面,然后当运行发现缺少 m.txt 时,make 会自动去寻找后面有没有命令生成 m.txt

简单来说就是

你想要生成什么,那个目标就得放在第一条

# 伪目标

因为 m.txt 和 x.txt 都是自动生成的文件,我们可以使用 clean 命令来删除他们

我们需要编写 clean 命令

clean:
	rm -f m.txt
	rm -f x.txt
1
2
3

执行 make clean 后,m.txt 和 x.txt 都被删除了

但是如果我的目录中存在一个名字为 clean 的文件,make 会认为 clean 已经存在就不执行 make clean 的命令

这里就需要一个 .PHONY: clean 命令告诉 make:“clean”是一个伪目标,不是一个实际存在的文件,无论有没有叫 clean 的文件,都需要执行 make clean 命令

.PHONY: clean
clean:
	rm -f m.txt
	rm -f x.txt
1
2
3
4

# 使用 makefile 编译 C 程序

假设有一个 C 项目,包含 hello.c、hello.h 和 main.c

// hello.c
#include <stdio.h>

int hello()
{
    printf("hello, world!\n");
    return 0;
}

1
2
3
4
5
6
7
8
9
// hello.h
int hello();
1
2
// main.c
#include <stdio.h>
#include "hello.h"

int main()
{
    printf("start...\n");
    hello();
    printf("exit.\n");
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11

我们很容易得出逻辑图

image-20250427133052077

我们很容易就能写出 Makefile

# 生成可执行文件:
world.out: hello.o main.o
	cc -o world.out hello.o main.o

# 编译 hello.c:
hello.o: hello.c
	cc -c hello.c

# 编译 main.c:
main.o: main.c hello.h
	cc -c main.c

clean:
	rm -f *.o world.out
1
2
3
4
5
6
7
8
9
10
11
12
13
14

执行 make 可以看到

$ make
cc -c hello.c
cc -c main.c
cc -o world.out hello.o main.o
1
2
3
4

运行 world.out

$ ./world.out 
start...
hello, world!
exit.
1
2
3
4

如果我们修改 hello.c 中的内容为

#include <stdio.h>

int hello()
{
    printf("hello, bob!\n");
    return 0;
}
1
2
3
4
5
6
7

再次 make

$ make
cc -c hello.c
cc -o world.out hello.o main.o
1
2
3

会发现,没有再次编译 main.o 因为 main.o 的没有改变

# 使用隐式规则

其实在前面那个例子里面,我们把生成 .o 的规则都删掉,程序也能正常编译

# 生成可执行文件:
world.out: hello.o main.o
	cc -o world.out hello.o main.o

clean:
	rm -f *.o world.out
1
2
3
4
5
6
$ make
cc    -c -o hello.o hello.c
cc    -c -o main.o main.c
cc -o world.out hello.o main.o
1
2
3
4

其实是 make 内部自定义了一些规则,对于 .o 内置了

xyz.o: xyz.c
	cc -c -o xyz.o xyz.c
1
2

就不用我们手写了

# 使用变量

我们在编写 makefile 的时候会重复写很多文件名,一来容易写错,二来如果要改名就要重新写一次

我们可以使用变量来解决反复引用的问题

看到我们之前的 makefile

world.out: hello.o main.o
	cc -o world.out hello.o main.o

clean:
	rm -f *.o world.out
1
2
3
4
5

其中 world.out 出现了三次,我们可以定义一个变量来替换他

TARGET = world.out

$(TARGET): hello.o main.o
	cc -o $(TARGET) hello.o main.o

clean:
	rm -f *.o $(TARGET)
1
2
3
4
5
6
7

变量定义用变量名 = 值或者变量名 := 值,通常变量名全大写。引用变量用$(变量名)

注意到hello.o main.o这个“列表”也重复了,我们也可以用变量来替换:

OBJS = hello.o main.o
TARGET = world.out

$(TARGET): $(OBJS)
	cc -o $(TARGET) $(OBJS)

clean:
	rm -f *.o $(TARGET)
1
2
3
4
5
6
7
8

如果我们能用一种方法能自动识别当前目录下的所有 .c 文件,然后对应到 .o 最后赋值给 OBJS 就好了,实际上我们可以通过函数来做到这一点

# $(wildcard *.c) 列出当前目录下的所有 .c 文件: hello.c main.c
# 用函数 patsubst 进行模式替换得到: hello.o main.o
OBJS = $(patsubst %.c,%.o,$(wildcard *.c))
TARGET = world.out

$(TARGET): $(OBJS)
	cc -o $(TARGET) $(OBJS)

clean:
	rm -f *.o $(TARGET)
1
2
3
4
5
6
7
8
9
10

# 内置变量

我们可以使用 $(CC) 代替命令 cc

$(TARGET): $(OBJS)
	$(CC) -o $(TARGET) $(OBJS)
1
2

系统内部默认了 $(CC)=cc,但是我们也可以自定义他 $(CC)=gcc

# 自动变量

我们也可以用一些特殊意义的字符来代表一些含义,这些字符叫做自动变量

  • $@ 表示目标文件
  • $^ 表示所有依赖文件
  • $< 表示第一个依赖文件
  • $? 所有比目标新的依赖文件
  • $* 目标文件去掉拓展名,例如 foo.o 的 $* 是 foo

前面的命令可以转化成为

world.out: hello.o main.o
	cc -o $@ $^
1
2

为了更好的调试,我们可以把变量打印出来

world.out: hello.o main.o
	@echo '$$@ = $@' # 变量 $@ 表示target
	@echo '$$< = $<' # 变量 $< 表示第一个依赖项
	@echo '$$^ = $^' # 变量 $^ 表示所有依赖项
	cc -o $@ $^
1
2
3
4
5
gcc    -c -o hello.o hello.c
gcc    -c -o main.o main.c
$@ = world.out
$< = hello.o
$^ = hello.o main.o
cc -o world.out hello.o main.o
1
2
3
4
5
6

# 使用规则模式

当我们没有创建 .o 的生成规则的时候,makefile 会自动生成一条规则,对于 xyz.o xyz.c 他的规则如下

$(CC) $(CFLAGS) -c -o $@ $<
1

这里的变量 $(CC) 和 $(CFLAGS) 可以自己设定

  • $(CFLAGS) 是附加的编译指令

我们如果要自定义这个规则也是可以的

# 模式匹配规则:当make需要目标 xyz.o 时,自动生成一条 xyz.o: xyz.c 规则:
%.o: %.c
	@echo 'compiling $<...'
	cc -c -o $@ $<
1
2
3
4

通过这个指令,我告诉了计算机,如果出现了一个 .o 就需要自动匹配一个 .c

OBJS = $(patsubst %.c,%.o,$(wildcard *.c))
TARGET = world.out

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

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

clean:
	rm -f *.o $(TARGET)
1
2
3
4
5
6
7
8
9
10
11

运行 make

$ make
cc -c -o hello.o hello.c
cc -c -o main.o main.c
cc -o world.out hello.o main.o
1
2
3
4

模式规则的命令完全由我们自己定义,因此,它比隐式规则更灵活。

但是,模式规则仍然没有解决修改hello.h头文件不会触发main.c重新编译的问题

# 自动生成依赖

前面我们讲了隐式规则和模式规则,这两种规则都可以解决自动把.c文件编译成.o文件,但都无法解决.c文件依赖.h文件的问题。

因为一个.c文件依赖哪个.h文件必须要分析文件内容才能确定,没有一个简单的文件名映射规则。

但是,要识别出.c文件的头文件依赖,可以用GCC提供的-MM参数

在终端中运行

$ cc -MM main.c
main.o: main.c hello.h
1
2

一个简单的想法就是把对一个 .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 $@ $^

# xyz.d 的规则由 xyz.c 生成:
%.d: %.c
	rm -f $@; \
	$(CC) -MM $< >$@.tmp; \
	# 这里
	sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.tmp > $@; \ 
	rm -f $@.tmp

# 模式规则:
%.o: %.c
	$(CC) -c -o $@ $<

clean:
	rm -rf *.o *.d $(TARGET)

# 引入所有 .d 文件:
include $(DEPS)

.PHONY: echo
echo:
	@echo "SRCS: $(SRCS)"
	@echo "OBJS: $(OBJS)"
	@echo "DEPS: $(DEPS)"
	@echo "TARGET: $(TARGET)"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
  • 我们先定义了 SRCS OBJS DEPS,通过 echo 命令可以看到这些变量的值
$ make echo
SRCS: hello.c main.c
OBJS: hello.o main.o
DEPS: hello.d main.d
TARGET: world.out
1
2
3
4
5
  • 然后我们定义了 .d 的生成规则

    • 先删除 .d
    • 通过 -MM 命令获得依赖,例如 :main.o: main.c hello.h
    • 然后 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.tmp > 命令通过函数把 main.o : main.c hello.h 转化成 main.o main.d : main.c hello.h 表面了 main.o main.d 都依赖于 main.c hello.h
    • 最后删除 tmp 文件
  • include $(DEPS) 的意思就是

    • 把 $(DEPS) 里面列出的所有文件,当成 Makefile 的一部分,读进来。
    • 就相当于在 makefile 里面写入了 main.o main.d : main.c hello.h 和 hello.o hello.d : hello.c
Vim 使用指南
valgrind 使用指南

← Vim 使用指南 valgrind 使用指南→

最近更新
01
在 ACM 集训队年会上的讲话
07-01
02
计算机网络笔记
06-13
03
LLM101 NLP学习笔记
06-02
更多文章>
Theme by Vdoing | Copyright © 2024-2025 Martian148 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式