Makefile 实战指南:从零到一掌握自动化构建

🔥个人主页:胡萝卜3.0****

📖个人专栏:************************************************************************************************************************************************************************************************************************************************************《C语言》、《数据结构》 、《C++干货分享》、LeetCode&牛客代码强化刷题****************************************************************************************************************************************************************************************************************************************************************

《Linux系统编程》

⭐️人生格言:不试试怎么知道自己行不行


🎥胡萝卜3.0🌸的简介:


目录

前言

一、初识构建工具:Make与Makefile

[1.1 背景介绍](#1.1 背景介绍)

[1.2 核心作用](#1.2 核心作用)

[1.3 核心概念](#1.3 核心概念)

二、理解make/makefile的编译工作的推导过程,依赖关系和依赖方法

[2.1 定义:什么是依赖关系与依赖方法?在什么地方?](#2.1 定义:什么是依赖关系与依赖方法?在什么地方?)

[2.2 make 的执行引擎:它如何解读并运行你的 Makefile?](#2.2 make 的执行引擎:它如何解读并运行你的 Makefile?)

三、构建工程+清理工程

[3.1 什么是清理工程?](#3.1 什么是清理工程?)

[3.2 什么叫做总是被执行?](#3.2 什么叫做总是被执行?)

[3.3 显式与隐式:为什么make不总是执行命令,而需要.PHONY?](#3.3 显式与隐式:为什么make不总是执行命令,而需要.PHONY?)

四、makefile最佳实践和实用语法

[4.1 最佳实践](#4.1 最佳实践)

[4.2 实用语法](#4.2 实用语法)

语法1:

语法2:

语法3:

语法4:

语法5

结尾


前言

在 Linux C/C++ 开发中,当项目规模增长、源文件数量增多时,手动键入冗长的 gcc 命令会迅速变得难以维护------开发者不仅要时刻厘清头文件与源文件间错综复杂的依赖关系,还需谨慎处理编译顺序,并确保每次编译参数的一致性与正确性。

这正是 Makefilemake 工具大显身手的场景。其核心价值在于 "自动化构建" :通过一份声明式的配置文件(Makefile),将编译、链接、清理等操作规则固化下来。此后,仅需一个简单的 make 命令,系统便能自动完成所有必要的步骤,将开发者从重复劳动中解放出来,从而极大提升开发效率和构建的可重复性。

本文将从Makefile 的基本原理入手,逐步拆解其语法规则、依赖关系、伪目标、变量与函数等核心概念,最终通过一个结构清晰的工程化示例演示其完整用法。我们的目标是帮助你彻底告别"手动编译"的泥潭,系统掌握"自动化构建"的利器,从而游刃有余地应对中小型乃至更复杂的 C/C++ 项目开发。

我们将从以下几个方面进行讲解:

  1. 见一下,如何使用make/makefile
  2. 理解Makefile/make编译工作的推导过程,依赖关系和依赖方法
  3. 学习makefile的最常见的语法和特殊符号

一、初识构建工具:Make与Makefile

1.1 背景介绍

  • 会不会写makefile,从侧面说明了一个人是否具备完成大型工程的能力
  • 一个工程中的源文件不计其数,其按类型,功能,模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于更复杂的操作
  • makefile 带来的好处就是 -- "自动化编译",一旦写好,只需要一个 make命令,整个工程完全自动编译,极大的提高了软件开发的效率。
  • make 是一个命令工工具,是一个解释makefile中指令的命令工具,一般来说,大多数的IDE都有这个命令,比如:Delphi 的 make,Visual C++ 的 namke,Linux下GNU的make。可见,makefile都成为了一种在工厂方面的编译方法。
  • make是一条命令,makefile是一个文件,两个搭配使用,完成项目自动化构建。

1.2 核心作用

  • 定义源文件之间的依赖关系(如可执行文件依赖目标文件,目标文件依赖源文件);
  • 指定编译规则(如如何将.c文件编译为.o文件,如何链接为可执行文件);
  • 支持增量编译(只重新编译修改过的文件,减少重复工作);
  • 提供项目清理功能(一键删除编译产物,方便重新构建)。

1.3 核心概念

  • make:解释 Makefile 规则的命令工具,默认查找当前目录下名为Makefile或makefile的文件;
  • 目标(target):要生成的文件或要执行的命令(如可执行文件、清理操作);
  • 依赖(prerequisites):生成目标所需要的文件或条件(如生成myproc依赖myproc.c);
  • 命令(command):生成目标的具体操作(如gcc -o myproc myproc.c),必须以 Tab 键开头

关键所在------

  • **make:**一个命令
  • makefile/Makefile:一个文件------描述的是如何编译当前工程(这个文件需要我们自己创建)

ok,那我们先来看一下猪跑------

我们先来创建一个code.c源文件,并在里面写一个简单的C语言代码:

bash 复制代码
[carrot@VM-0-16-centos ~]$ touch code.c
[carrot@VM-0-16-centos ~]$ vim code.c
[carrot@VM-0-16-centos ~]$ cat code.c
#include<stdio.h>
int main()
{
  printf("hello world!\n");
  printf("hello world!\n");
  printf("hello world!\n");
  printf("hello world!\n");
  printf("hello world!\n");
  printf("hello world!\n");
  return 0;
}

ok,建完之后,我们创建一个makefile文件,并用vim 编译器打开这个文件(这里先不要管这是怎么写的)------

bash 复制代码
[carrot@VM-0-16-centos ~]$ touch makefile
[carrot@VM-0-16-centos ~]$ vim makefile

然后,执行------

  • 当我们在命令行中输入:make,它会自动的给我链接一个code的可执行程序;
  • 在命令行输入:make clean,自动清理刚刚生成的可执行临时文件

我知道写到这里会有很多uu会感到疑惑?我去,这是怎么做到的。

ok,接下来我们一起来看一下这里面的原理究竟是什么?

二、理解make/makefile的编译工作的推导过程,依赖关系和依赖方法

所谓的make/makefile,其实就是一个依赖关系和依赖方法的集合!!!

2.1 定义:什么是依赖关系与依赖方法?在什么地方?

上图中所展示的就是依赖关系和依赖方法。

ok,接下来我们对上图中所展示的内容进行解释------

😯,原来makefile文件中就是存放着一些依赖文件和依赖方法的文件啊!!!

那makefile文件是怎么执行的呢?为什么是像下面的那种执行方法呢?

bash 复制代码
[carrot@VM-0-16-centos ~]$ make
gcc code.c -o code
[carrot@VM-0-16-centos ~]$ make clean
rm -f code

执行make就生成了code可执行程序,执行make clean 就清理了code可执行临时程序文件呢?

2.2 make 的执行引擎:它如何解读并运行你的 Makefile?

bash 复制代码
[carrot@VM-0-16-centos ~]$ make
gcc code.c -o code
[carrot@VM-0-16-centos ~]$ ll
total 28
drwxrwxr-x 2 carrot carrot 4096 Dec 24 16:44 118
-rwxrwxr-x 1 carrot carrot 8360 Dec 31 09:43 code
-rw-rw-r-- 1 carrot carrot  213 Dec 31 09:39 code.c
-rw-rw-r-- 1 carrot carrot  827 Dec 26 17:29 install.sh
-rw-rw-r-- 1 carrot carrot   67 Dec 31 09:43 makefile
[carrot@VM-0-16-centos ~]$ ./code
hello world!
hello world!
hello world!
hello world!
hello world!
hello world!
[carrot@VM-0-16-centos ~]$ make clean
rm -f code

当命令行执行make命令时,make会从上往下扫描makefile文件

  • 在命令行直接执行 make 命令:默认形成的是make会从上往下扫描makefile文件中第一个遇到的目标文件!!!且仅执行与该目标文件相关的依赖关系和命令
  • 当 Makefile 中有多个目标文件时,我要形成确定的目标文件,使用 make 目标文件 可以构建指定的目标文件

😯,原来是这样啊------

默认执行第一个目标

bash 复制代码
# Makefile 示例
code: code.c          # ← make 默认执行这个(第一个目标)
    gcc code.c -o code

clean:               # ← 这个不会默认执行
    rm -f code

执行逻辑

  • make → 自动找到第一个目标code

  • 检查依赖关系:code 需要 code.c

  • 如果 code.ccode 新(或 code 不存在),执行 gcc code.c -o code

指定执行特定目标

bash 复制代码
# Makefile 示例
code: code.c
    gcc code.c -o code

clean:               # ← 需要明确指定
    rm -f code

执行方式

  • make clean → 明确告诉 make:"我要执行 clean 目标"

  • make 会跳过默认的第一个目标,直接执行 clean 对应的命令

但是一般情况下,我们会将形成可执行程序的目标文件放在最开头!!!以便我们直接执行make生成可执行程序!!!

ok,这个我们理解了之后,我们接着来看------

当我们在makefile文件中写出了gcc编译的详细过程,然后在命令行中直接输入:make

这时会发生什么?

执行make------

bash 复制代码
[carrot@VM-0-16-centos ~]$ make
gcc -E code.c -o code.i
gcc -S code.i -o code.s
gcc -c code.s -o code.o
gcc code.o -o code 

嗯?有没有发现什么?

  • 这是为什么?

还记得我们前面说的:make会从上向下扫描makefile文件

  • 这是什么原理?

这个是不是就有点像栈结结构啊!其实在make命令内部会维护一个栈结构!!!

make会根据依赖关系,若依赖目标文件不存在,就把依赖方法直接入栈,直到依赖目标文件存在,然后将依赖方法出栈,依次执行依赖方法,形成目标文件

  • 核心:

make会利用栈结构来对自己当前对应的依赖关系,尤其是依赖文件列表不存在时,就要将自己当前依赖方法入栈,直到推导出口(也就是依赖文件列表存在),再把所有入栈的依赖方法依次出栈,执行就可以完成语法推导

三、构建工程+清理工程

3.1 什么是清理工程?

执行 rm命令,删除工程的临时文件,就叫做清理工程

也就是这样------

但是,这里好像有那么一点看不懂哎!

这里的依赖方法可以是任何Linux命令!!!

也就是说我们可以这样写------

ok,这上面的内容应该都可以理解,但是这个是什么------

.PHONY 就如同C语言中的int,double......是一个关键词,用来形容后序跟的clean,表明他是一个伪目标。

.PHONY 是一个修饰词,告诉make clean是一个伪目标,伪目标也是目标,也有依赖方法。

伪目标表达的含义是:被 .PHONY 修饰的目标文件,他所对应的依赖方法总是被执行

啥意思?什么叫做总是被执行?

3.2 什么叫做总是被执行?

在看总是被执行之前,我们先来看看不总被执行

  • 不总被执行,那就是目标文件 没有被 .PHONY 修饰
  • 我们再来看总是被执行(也就是目标文件被.PHONY修饰)------

所以,.PHONY关键字一旦修饰目标文件,就代表着将来依赖的某种方法,未来总是被执行,每一次都会被执行

但是,最佳实践:

  • clean 用.PHONY ,形成工程;其他目标可执行文件,不建议用.PHONY修饰

3.3 显式与隐式:为什么make不总是执行命令,而需要.PHONY?

为什么?为什么默认不带.PHONY,对应的gcc和make默认不让我总是被执行?

  • .PHONY修饰目标文件时,代表的是:不要管目标文件是新还是旧,直接调用依赖方法
  • .PHONY不修饰目标文件,gcc就要去关心修饰目标文件是老的还是新的,有没有被修改过

在这里,我们要秉承一个原则------

目标文件可以执行,不建议使用.PHONY修饰,code可执行文件就是要让gcc形成程序时,维持一个不总被执行的特性,只会编译新文件

通过上面的解释,我们只能明白.PHONY的作用是什么,并没有真正理解为什么是这样做的。

ok,那通过下面的一个问题,我们就能理解了------

  • make是如何知道code.c是否需要被重新编译?

那我们就需要来看几个时间了------

bash 复制代码
[carrot@VM-0-16-centos ~]$ stat code.c
  File: 'code.c'
  Size: 213       	Blocks: 8          IO Block: 4096   regular file
Device: fd01h/64769d	Inode: 786967      Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1001/  carrot)   Gid: ( 1001/  carrot)
Access: 2025-12-31 09:39:45.160827997 +0800
Modify: 2025-12-31 09:39:45.159827991 +0800
Change: 2025-12-31 09:39:45.159827991 +0800
 Birth: -

那为什么要说这个时间呢?

我们知道文件=内容+属性

  • 修改属性:单纯的修改文件所对应的属性就会更改Change时间
  • 修改内容:单纯的修改内容,就会更改Modify时间和Change时间(改变内容,文件属性会自动改变)

但是这个Access时间有点不同------

在未来的程序中,我们会经常对文件进行访问的操作,如果每次访问文件,都要修改相应的Access时间,是不是有点浪费,所以:访问文件若干次后,Access时间才会修改,不是时时更新,它有自己的更新策略,和系统有关

补充命令:

bash 复制代码
# 将A C M 时间更新到系统的最新时间
touch 已存在文件

那我怎么知道文件有没有被修改,这个指标是什么?

  • Modfiy时间

make可以通过对比Modfiy时间来看文件是否被修改!!!

  • 总结:

判断目标文件和源文件到底需不需要编译------

取决于Modify时间:比较两个文件的Modify时间谁更新

  • 若可执行程序的Modify时间更新,就不需要重新编译;
  • 若源文件的Modify时间更新,就需要重新编译代码

那我们会过头来看一下:.PHONY修饰目标文件是怎么做到让依赖方法总是被执行呢?

.PHONY忽略源文件和可执行文件的Modify时间对比,就可以让依赖方法总是被执行

四、makefile最佳实践和实用语法

4.1 最佳实践

**最佳实践:**先将源文件编成 .o 文件,然后再把 .o 文件链接成 可执行文件

  • 还有一种简洁的写法:

4.2 实用语法

语法1:
bash 复制代码
1: makefile ? ?                                                                                                     ?? buffers 
  1 code.exe:code.o
  2   gcc -o $@ $^
  3 code.o:code.c
  4   gcc -c code.c

语法解释

  • code.exe:code.o:目标 code.exe 依赖于 code.o

  • gcc -o $@ $^:编译命令

    • $@:代表目标文件名(code.exe

    • $^:代表所有依赖文件(code.o

相当于

bash 复制代码
code.exe: code.o
    gcc -o code.exe code.o

解释:

语法2:

makefile文件中可以定义变量

在定义了变量之后,我们就可以对上面的依赖方法进行改写------

语法3:

此时,博主有一个想法,想在每个执行的步骤前面,给个提醒,让我们知道此时进行的是哪一步,我们可以这样做:

在命令行执行一下make------

bash 复制代码
[carrot@VM-0-16-centos ~]$ make
echo "我要开始编译了......"
我要开始编译了......
gcc -c code.c
echo "我要开始链接了......"
我要开始链接了......
gcc -o code.exe code.o

😯,在这里确实知道我要进行哪一步,但是echo命令会显了,makefile执行任何命令都会把命令给回显到显示器上。

但是,此时我不想让命令回显出来,直接回显字符串就可以了,那我们可以这样做------

  • 命令前+@禁止makefile对命令进行回显
bash 复制代码
[carrot@VM-0-16-centos ~]$ make
我要开始编译了......
gcc -c code.c
我要开始链接了......
gcc -o code.exe code.o

我们在执行命令前面带@,不要让他把命令的执行过程回显出来,方便后续对makefile的输出进行定制化输出

注意:

除此之外,我们还可以在echo的双引号里面添加变量,echo的双引号里面可以直接识别变量的内容------

bash 复制代码
[carrot@VM-0-16-centos ~]$ make debug
Bin: code.exe 
Obi: code.o
Src: code.c

既然我们可以对文件名取变量,那我们就可以对命令取个变量------

语法4:

接下来,我们对这块进行符号化------

这里可以这样替换------

这里的意思就是------

语法5

在makefile中,一般把源文件**.c文件** 编译成**.o文件,** 并不需要把 .o文件 和.c文件的名字明确写出来

我们可以这样写------

%.o 有点像***.c**,都是通配符!!!

那这里的这个是什么意思呢?

ok,假设我们这里有多个源文件:code.c code1.c code2.c,这里的意思就是------

或者这样理解------

假设现在我们创建了10个.c源文件,并且要对这些源文件都要编译成可执行文件,该怎么做呢?

  • 首先,我们必须获取所有源文件:

那我们这样获取吗?

一个一个列举吗?是不是有点太挫了

动态获取当前目录下的所有源文件:

bash 复制代码
Src=$(shell ls *.c) #做法1
Src=$(wildcard *.c) #做法2 

做法1的意思是:执行shell命令ls *.c,然后把ls *.c的结果作为变量的内容赋给Src

  • 然后,我们要将所有的源文件编译成.o文件

动态地将所有源文件编译成.o文件

bash 复制代码
Obj=$(Src:.c=.o)  

将Src中的每一个以 .c 结尾的文件统一改成同名的 .o文件,makefile自动识别它!!!

  • 完整代码------

命令行输入:make,运行结果------

makfile可以直接帮助我们把当前目录下的所有.c源文件依次展开成同名的 .o文件,然后.o文件依次被链接,有多少个源文件就被展开成多少份!!!

结尾

希望对学习Linux相关内容的uu有所帮助,不要忘记给博主"一键三连"哦!

相关推荐
有毒的教程3 小时前
SaltStack 开源自动化运维工具详细介绍
运维·开源·saltstack
Irene.ll3 小时前
DAY31 文件的拆分方法和规范
人工智能·机器学习
大房身镇、王师傅3 小时前
【VirtualBox】VirtualBox 7.1.6 RockyLinux10 配置增强功能 设置共享目录
运维·服务器·virtualbox·rockylinux10
betazhou3 小时前
rsync使用案例分析
linux·运维·服务器·rsync·同步数据
安特尼4 小时前
推荐算法手撕集合(持续更新)
人工智能·算法·机器学习·推荐算法
Dyanic4 小时前
DSFuse:一种用于特征保真度的红外与可见光图像融合的双扩散结构
人工智能·机器学习·计算机视觉
无风听海5 小时前
CBOW 模型中输入矩阵、输出矩阵与词表向量矩阵深入解析
人工智能·机器学习·矩阵
薛定e的猫咪5 小时前
【ICRA 2025】面向杂技机器人的分阶段奖励塑形:一种约束多目标强化学习方法
人工智能·深度学习·机器学习·机器人
晚风予卿云月5 小时前
【Linux】自动化构建—make/Makefile
linux·自动化·make/makefile
minglie15 小时前
谷歌浏览器搜索技巧
运维