Makefile快速上手笔记

一、为什么需要 Makefile?

在linux开发过程中,当项目只有一两个.c文件时,手动执行gcc命令并不麻烦:

gcc main.c hello.c-oapp

但随着项目规模增长,源文件越来越多,手动编译变得难以维护,特别是对于有复杂依赖关系的项目:

  • 效率低:每次改动一个文件就要重新输入冗长的编译命令
  • 容易出错:漏编译某个文件、忘记清理中间产物
  • 浪费资源:未修改的文件被重复编译

Makefile的出现解决了这些问题——它定义了一套编译规则,make命令自动按规则执行,并智能地只重新编译被修改过的文件(增量编译)。


二、快速上手:第一个 Makefile

2.1 项目结构

假设有一个最简单的 C 项目:

project/ ├── code.c # 源文件--->假设我们写了hello world └── Makefile # 编译规则文件

2.2 编写 Makefile

code:code.c gcc code.c -o code

这是最简单的Makefile格式
之后在终端输入make即可编译文件

2.3 使用

make#终端会输出 gcc code.c -o code

2.4 语法要点

一条 Makefile 规则由两部分组成:

  • 编译目标:编译依赖文件
  • 所需要执行的命令(必须以tab键开头)

比如上述的code就是编译的目标文件,而code.c就是编译依赖
所执行的命令就是gcc code.c -o code

2.5 链式依赖

我们以C代码的编译四个过程为例

编译四个过程为
预处理---->.c文件变为.i
编译------->.i文件变为.s
汇编 ------>.s文件变为.o文件
链接------->.o文件变为可执行文件

Makefile示例

# 链接阶段:.o 生成可执行文件,依赖为main.o,而main.o在下一行为也有依赖,以此类推 main: main.o gcc main.o -o main # 汇编阶段:.s 汇编成目标文件 .o main.o: main.s gcc -c main.s -o main.o # 编译阶段:预处理文件.i 编译成汇编.s main.s: main.i gcc -S main.i -o main.s main.i: main.c gcc -E main.c -o main.i

编译完成后即可生成main.i,main.s,main.o以及可执行文件main

2.6 清理编译产物

由于make会寻找Makefile的第一个目标,第一个目标就是我们要编译的目标,如果要增加一个清除所有编译产物的命令,比如clear,clean,就需要生成一个伪目标,当我们指定这个命令时,就不会去找第一个要执行的目标,而是我们指定的命令目标

在Makefile文件后添加

.PHONY:clean #表示声明一个伪目标clean clean: #目标clean rm -f code code.o code.s code.i

在终端执行make clean即可删除编译产物

三、标准化Makefile

可以看出其实Makefile只是将一些要编译的命令放在一个文件当中,当要执行的时候只是把命令缩短了。如何把一个Makefile能够更加拓展性的用在各个项目框架中呢

  1. 变量
    使用变量来代替一些文件名,即可做到拓展性
BIN=code #生成的可执行文件名 SRC=$(wildcard *.c) #编译源文件,表示该文件下所有.c文件 COMPILE=gcc #你要使用的编译器 CFLAGS=-c #编译选项,生成目标文件 #一般情况下会保留编译的目标二进制文件即.o文件 LFLAGS=-o #链接形成可执行文件选项 OBJ=$(SRC:.c=.o) #表示该文件夹下所有.c的同名.o文件 $(BIN):$(OBJ) @$(COMPILE) $^ $(LFLAGS) $@ #^是将所有的OBJ都进行使用 @echo "将$^链接为$@" %.o:%.c @$(COMPILE) $(CFLAGS) $< #<是一一对应,也就是将对应的.c文件意义对应为.o文件 @echo "将$<编译为$@" .PHONY:clean clean: $(RM) $(BIN) $(OBJ)

上述代码中,BIN,SRC等等是我们手动设置的变量,需要我们自己定义,而^<@等变量为自动变量,有其独特的含义
^:表示在目标与依赖关系中冒号右边的所有文件
<:表示冒号右边的文件与冒号左边的文件一一对应的命令关系
@如果在所有命令之前,表示关闭回显(也就是执行了make之后不用显示具体执行了哪个命令),如果用作变量,则表示冒号左边的所有文件

^<的区别在于
前者是指所有文件,比如gcc code1.o code2.o code3.o -o code其中的code1~3.o就可以用^表示
后者在gcc -c code1.c -o code1.ogcc -c code2.c -o code2.ogcc -c code3.c -o code3.o这一组命令中code1~3.c可以用<来代替

注意,在使用变量时,需要在变量前添加$符号标识这是一个变量

我们还可以添加一些提示信息、比如将xxx.c编译为了xxx.o,将xxx1.o,xxx2.o,xxx3.o链接为了xxx.exe
如下

BIN=code.exe #编译执行的可执行文件 CC=gcc #编译器 SRC=$(wildcard *.c) #源文件 CFLAGS=-c #编译选项 RM=rm -f LFLAGS=-o OBJ=$(SRC:.c=.o) $(BIN):$(OBJ) @$(CC) $^ $(LFLAGS) $@ @echo "将$^链接为$@" %.o:%.c @$(CC) $(CFLAGS) $< @echo "将$<编译为$@" .PHONY:clean clean: $(RM) $(BIN) $(OBJ)

四、增量式编译

这部分是关于Makefile的特点。

make 只会重新编译比目标文件更新的依赖文件,没改动的文件跳过不编译,节省时间,这就是增量编译。底层原理是每个文件都有修改时间戳,make 执行时自动对比:
规则格式:目标: 依赖

  1. 如果依赖文件时间晚于目标文件 → 依赖被修改,执行命令重新生成目标
  2. 如果依赖比目标旧 → 文件无改动,直接跳过这条规则,不执行 gcc

这一点其实在目前很多IDE上都有体现。