【编译器 + 自动化构建器】目录
- 前言:
- ---------------编译器---------------
-
- [1. 程序的编译流程是什么?](#1. 程序的编译流程是什么?)
- [2. 什么是静态连接和动态连接?](#2. 什么是静态连接和动态连接?)
- [3. 什么是静态库和动态库?](#3. 什么是静态库和动态库?)
-
- [一、静态库:编译时 "全量打包"](#一、静态库:编译时 “全量打包”)
- [二、动态库:运行时 "共享加载"](#二、动态库:运行时 “共享加载”)
- [5. 怎么查看并自定义静态连接?](#5. 怎么查看并自定义静态连接?)
- [6. 什么是条件编译?](#6. 什么是条件编译?)
- [7. 如何控制条件编译?](#7. 如何控制条件编译?)
- [8. 条件编译的核心价值怎么应用?](#8. 条件编译的核心价值怎么应用?)
- [9. 为什么编译C/C++代码要先变成汇编?](#9. 为什么编译C/C++代码要先变成汇编?)
- ---------------自动化构建器---------------
-
- [1. 什么是make?什么是Makefile?](#1. 什么是make?什么是Makefile?)
- [2. 如何编写Makefile文件?](#2. 如何编写Makefile文件?)
-
- -------第一版-------
- [(1)为什么 make 提示 app is up to date?](#(1)为什么 make 提示 app is up to date?)
- -------第二版-------
- [(1)为什么直接输入 clean 会报错?](#(1)为什么直接输入 clean 会报错?)
- [(2)伪目标 clean 为什么 "总是被执行"?](#(2)伪目标 clean 为什么 “总是被执行”?)
- -------第三版-------
- (1)变量名一定要大写吗?
- [(2)符号是干什么用的?](#(2)符号是干什么用的?)
- [(3)为什么要用 @ 和 ^ 这样的自动化变量?](#(3)为什么要用 @ 和 ^ 这样的自动化变量?)
- -------第四版-------
- (1)@符号是干什么用的?
- -------第五版-------
- [(1)(wildcard \*.c) 是什么意思?](#(1)(wildcard *.c) 是什么意思?)
- [(2)(SRC:.c=.o) 是什么意思?](#(2)(SRC:.c=.o) 是什么意思?)
- (3)%符号是干什么用的?
- [(4)@(CC) (FLAGS) \< -o @ 怎么理解?](#(4)@(CC) (FLAGS) < -o @ 怎么理解?)
- [3. make是如何工作的?](#3. make是如何工作的?)

往期《Linux系统编程》回顾:/------------ 入门基础 ------------/
【Linux的前世今生】
【Linux的环境搭建】
【Linux基础 理论+命令】(上)
【Linux基础 理论+命令】(下)
【权限管理】/------------ 开发工具 ------------/
【软件包管理器 + 代码编辑器】
前言:
hi~ ,小伙伴们大家好啊!(ノ≧∀≦)ノ
纪念一下昨天万圣节,今天是11月的第一天,碰 ~,你你......竟然埋伏鼠鼠(╬•᷅д•᷄╬), 你......,原来是鼠鼠的忠实粉丝啊,那没事了 ( ̄▽ ̄*)ゞ,哈哈( ˘▽˘)っ🍿
有小伙伴的给鼠鼠说:之前教了怎么使用的vim编写C/C++的代码,嗯~ o( ̄▽ ̄ )o,鼠鼠知道了,这代码四天了来没有运行是吧(ಥ﹏ಥ)!真的不是鼠鼠的错,溜~ ╰(°▽°)╯那么今天我们要学习的内容就是:【编译器 + 自动化构建器】(~o ̄3 ̄)~
简单说下重点:(敲黑板)(╯°□°)╯︵ ┻━┻
编译器:是帮咱们把写好的源代码,转换成电脑能看懂、能执行的程序的 (。・ω・。)ノ♡自动化构建器:则能帮咱们批量处理编译流程,不用反复手动输入复杂命令,尤其适合多文件项目 (ノ>ω<)ノ学会这俩工具,你用 Vim 写的代码就能成功 "落地运行",再也不用让它 "躺平" 啦~ 赶紧一起学起来!ヾ(◍°∇°◍)ノ゙
---------------编译器---------------
1. 程序的编译流程是什么?

程序编译流程主要包含 预处理、编译、汇编、链接 四个核心阶段,每个阶段逐步将高级语言代码转换为可执行的机器指令,以下结合 C 语言(用
gcc编译器举例)详细拆解:
1. 预处理(Preprocessing)
核心作用:处理代码中的
宏定义、头文件引入、条件编译等纯文本替换逻辑,生成 "干净" 的中间代码。关键操作:
- 宏替换:把
#define定义的宏(如:#define PI 3.14)直接替换成对应内容- 头文件展开:将
#include <stdio.h>这类头文件的全部内容,插入到当前源文件中- 条件编译处理:根据
#if/#ifdef决定哪些代码保留(如:调试代码#if DEBUG ... #endif)命令示例:
bashgcc -E code.c -o code.i
-E:仅执行预处理 ,输出文件code.i是纯文本,可直接查看替换后的代码
2. 编译(Compilation)
核心作用:将预处理后的纯文本代码(
code.i) 转换为汇编语言代码(code.s) ,完成 "高级语言 → 低级语言" 的关键转换。关键操作:
- 词法分析:把代码拆成一个个 "单词"(如:关键字
int、变量名num)- 语法分析:验证代码语法是否正确,构建语法树(如:
if-else结构是否匹配)- 语义分析:检查逻辑合理性(如:变量未定义就使用),最终生成对应 CPU 架构的汇编指令)
命令示例:
bashgcc -S code.i -o code.s
-S:仅执行编译 ,输出文件code.s包含汇编代码(如:x86 或 ARM 指令)
3. 汇编(Assembly)
核心作用:将汇编代码(
code.s) 转换为二进制机器码(code.o) ,生成 "可重定位目标文件"。关键概念:
- 可重定位目标文件:包含二进制机器码,但函数、变量的最终内存地址未确定(需链接阶段处理)
- 跨平台差异:Windows 下后缀是
.obj,Linux 下是.o,本质功能一致命令示例:
bashgcc -c code.s -o code.o
-c:仅执行汇编 ,输出文件code.o是二进制格式(无法直接文本查看,需用objdump分析)
4. 链接(Linking)
核心作用:将多个目标文件(如:
code.o、util.o) 和系统库(如:libc.so) 整合,生成可执行文件。关键操作:
- 符号解析:找到函数、全局变量的实际地址(解决 "未定义符号" 报错)
- 库整合:自动链接标准库(如:
printf实际来自libc.so)- 地址重定位:为代码、数据分配最终虚拟内存地址,确保程序能正确运行
命令示例:
bashgcc code.o math.o -o app
- 输入多个
.o文件,输出app是可直接运行的程序(Windows 下是.exe)

2. 什么是静态连接和动态连接?
在实际开发中,程序很少仅靠单个源文件完成功能,往往需要拆分多个源文件协同工作。这些源文件并非独立,会存在复杂依赖(如:A 文件调用 B 文件的函数 )
由于每个
.c源文件需单独编译生成.o目标文件,为让分散的目标文件协作运行,链接过程成为关键,由此衍生出静态链接 与动态链接两种核心方案
静态连接和动态连接是处理 程序与** 库文件** 、目标文件 之间依赖关系的两种不同方式
一、静态连接
静态连接:是在程序的编译链接阶段,将程序所依赖的所有目标文件(.o文件 )和库文件(如:静态库.a文件 )中的代码,全部复制并整合到可执行文件中。
- 这样一来,最终生成的可执行文件包含了运行时所需的所有代码,不再依赖外部的库文件
原理:
- 编译器首先将每个源文件 (如:.c 文件 )独立编译成目标文件(.o文件 )
- 之后链接器会扫描程序中引用的函数 和变量,从对应的静态库中找到这些符号的定义,并把相关代码段复制到可执行文件中
- 例如 :在 C 语言中,若程序调用了标准输入输出函数
printf,链接器会从标准 C 静态库中找到printf函数的实现代码,将其复制到可执行文件中
优点:
- 运行效率高:由于可执行文件包含了所有运行所需的代码,运行时不需要再去查找和加载外部库,减少了运行时的开销,因此程序的启动速度和执行效率相对较高
- 独立性强:可执行文件不依赖外部的库文件,在没有安装相关库的环境中也能正常运行,便于程序的移植和分发
缺点
- 文件体积大:每个可执行文件都包含了所依赖库的完整代码,如果多个程序都依赖同一个库,会导致库代码在磁盘和内存中大量重复,造成存储空间的浪费
- 更新维护困难:当库文件有更新(如:修复了一个漏洞或增加了新功能 )时,所有依赖该库的程序都需要重新进行编译和链接,否则无法享受到库更新带来的好处
场景:
- 在嵌入式开发等对运行环境要求苛刻、对可执行文件的独立性要求较高,且对文件体积不是特别敏感的场景中,静态连接比较常用
- 此外,在开发一些对安全性要求极高,不希望依赖外部不可控库文件的程序时,也会采用静态连接
二、动态连接
动态连接:在程序运行时,由操作系统的动态链接器 (如:Linux 中的ld-linux-x86-64.so.2,Windows 中的ntdll.dll)将程序所依赖的库文件加载到内存,并将程序中对库函数的调用与实际的库函数地址进行绑定,从而使程序能够正确执行库函数。
- 在程序编译链接阶段,并不把库文件的代码直接复制到可执行文件中
原理:
- 在编译链接阶段,链接器仅在可执行文件中记录程序所依赖的库文件名称 和函数符号等信息
- 当程序运行时,动态链接器首先会查找 并加载 程序所依赖的动态库文件(如:Linux 中的
.so文件,Windows 中的.dll文件 )到内存中- 然后解析库文件中的符号表,将程序中对库函数的调用指令与库函数在内存中的实际地址进行关联
- 例如 :程序运行时需要调用
printf函数,动态链接器会找到libc.so.6(C 标准动态库 )中printf函数的实际内存地址,并将程序中调用printf的指令与该地址进行绑定
优点:
- 节省空间:多个程序可以共享使用同一个动态库,库代码只需要在内存中加载一份,大大减少了内存和磁盘空间的占用,提高了资源的利用率
- 更新方便:当动态库有更新时,只要库文件的接口保持不变,依赖该库的程序无需重新编译,只需要更新动态库文件,程序在下次运行时就会自动使用更新后的库,降低了软件维护的成本
缺点:
- 运行时依赖:程序的运行依赖于系统中安装的特定版本的动态库,如果目标运行环境中没有安装所需的动态库,或者动态库的版本不兼容,程序就无法正常运行
- 性能开销 :程序运行时需要动态链接器进行
库的加载和符号绑定等操作,会带来一定的运行时性能开销,尤其是在程序启动阶段
场景:
- 在大多数通用的软件开发中,动态连接是首选的方式
- 比如:日常使用的各种操作系统下的应用程序、数据库管理系统等。它能够有效节省系统资源,并且方便软件的更新和维护
3. 什么是静态库和动态库?
静态库 和动态库是程序开发中用于复用代码的两种库文件形式,它们在存储形式、链接方式、使用场景等方面存在差异。
一、静态库:编译时 "全量打包"
静态库:是一种将多个目标文件(.o)打包在一起形成的库文件。
- 原理 :编译链接阶段, 链接器会把静态库的全部代码直接 "复制粘贴" 到可执行文件里
- 表现 :
- 优点 :运行时无需依赖外部库文件(所有逻辑已打包进程序)
- 缺点:生成的可执行文件体积大(因为包含库的完整代码)
- 标识 :
- Linux 后缀 :
.a(如:libmath.a)- Windows 后缀 :
.lib(但 Windows 下.lib也可能是 "导入库",需注意区分)
二、动态库:运行时 "共享加载"
动态库:也叫共享库,它同样包含了编译好的二进制代码,但在程序运行时才会被加载到内存,并且可以被多个程序同时共享使用。
- 原理 :编译链接阶段只记录 "库的位置和接口",运行时才真正加载库代码。多个程序可共享同一份动态库,避免重复存储。
- 表现 :
- 优点:可执行文件体积小(仅包含 "调用库的逻辑",不包含库代码)
- 缺点 :运行时依赖外部库文件 (如:Linux 的
.so、Windows 的.dll)- 标识 :
- Linux 后缀 :
.so(如:libc.so.6,系统标准 C 库)- Windows 后缀 :
.dll
直观对比:静态库 vs 动态库
| 维度 | 静态库(.a/.lib) | 动态库(.so/.dll) |
|---|---|---|
| 链接时机 | 编译时 "全量嵌入" | 运行时 "动态加载" |
| 文件体积 | 大(包含库完整代码) | 小(仅存调用逻辑) |
| 运行依赖 | 无需外部库文件 | 依赖系统中的动态库 |
| 更新成本 | 库更新后,程序需重新编译 | 库更新后,程序无需重新编译 (直接替换库文件即可) |
5. 怎么查看并自定义静态连接?
用
gcc编译程序时,默认生成动态链接的可执行文件(依赖动态库)可通过 file 命令查看链接类型验证:
bash# 编译生成可执行文件 gcc code.c -o app # 查看链接类型 file app # 输出示例(Linux): # app: ELF 64-bit LSB shared object, x86-64, ... dynamically linked ...
动态链接在实际开发中应用更广泛,我们可通过 ldd 命令直观查看程序的动态库依赖:
bashldd app linux-vdso.so.1 (0x00007ffc4bbd5000) # 内核提供的虚拟动态库,优化系统调用 libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f2f34f6b000) # C 标准动态库,提供 printf 等函数 /lib64/ld-linux-x86-64.so.2 (0x00007f2f3516e000) # 动态链接器,负责加载依赖库
linux-vdso.so.1:内核为进程优化系统调用的# 内核提供的虚拟动态库,优化系统调用虚拟库,无实际磁盘文件libc.so.6:Linux 下的 C 标准动态库(Glibc ),是大多数 C 程序的基础依赖ld-linux-x86-64.so.2:动态链接器,程序启动时由它加载并绑定所有动态库
如果需要强制静态链接 ,需添加 -static 参数:
bashgcc -static code.c -o app_static file app_static # 输出示例: # app_static: ELF 64-bit LSB executable, ... statically linked ...

总结:
- 静态库 像
"自给自足的胖子"(体积大但独立)- 动态库 像
"共享经济的瘦子"(体积小但依赖环境)理解两者差异,能帮你解决 "程序在开发环境能跑,生产环境报错" 的经典问题,也能合理控制可执行文件体积和更新成本~
6. 什么是条件编译?
条件编译:是一种在程序预处理阶段,根据特定条件(通常是宏定义)决定代码片段是否参与后续编译的技术。
- 它能让同一套源代码根据不同场景(如:开发环境、硬件平台、功能需求)"动态裁剪",最终编译出不同的可执行程序,而无需维护多份相似代码
关键价值:一套代码适配多场景
条件编译的核心作用是用同一套代码满足不同需求,避免代码冗余和维护成本上升,典型应用场景包括:
功能分级:
商业软件中,通过条件编译区分 "免费版" 和 "专业版"(如:控制是否包含高级功能代码)
跨平台适配:
同一套代码需在 Linux、Windows 等不同系统运行时,可通过
#ifdef __linux__或#ifdef _WIN32等系统宏,编译对应平台的适配代码调试与发布切换:
开发阶段通过
DEBUG宏保留调试日志、断言检查等代码;发布时剔除这些逻辑,减少程序体积并提升性能内核与嵌入式开发:
如 Linux 内核通过大量条件编译(如:
CONFIG_NET控制网络功能),根据硬件配置动态裁剪代码,适配从服务器到嵌入式设备的不同场景
一、预处理的代码筛选
条件编译的逻辑在预处理阶段 完成(对应
gcc -E命令),本质是对源代码进行 "文本级别的筛选":
- 预处理程序会扫描代码中的条件编译指令(如:
#if、#ifdef等),结合宏定义判断哪些代码段需要保留,哪些需要剔除- 最终生成的预处理文件(
.i)只包含 "符合条件" 的代码,后续的编译、汇编、链接阶段仅处理这些内容
二、常用的指令与用法
条件编译主要通过一组以
#开头的预处理指令实现,核心指令如下:1. #ifdef / #ifndef / #endif:判断宏是否定义
#ifdef MACRO:如果MACRO宏已定义,则保留后续代码,直到#endif#ifndef MACRO:如果MACRO宏未定义,则保留后续代码(与#ifdef相反)#endif:结束条件编译块,必须与#ifdef/#ifndef配对示例 :根据是否定义
DEBUG宏,决定是否编译调试日志代码
c#include <stdio.h> // 可通过 gcc -DDEBUG 编译时定义该宏 #ifdef DEBUG #define LOG(msg) printf("Debug: %s\n", msg) // 调试模式:打印日志 #else #define LOG(msg) // 发布模式:剔除日志代码 #endif int main() { LOG("程序启动"); // 调试模式下执行,发布模式下不执行 printf("主逻辑执行中...\n"); return 0; }
2. #if / #elif / #else / #endif:更灵活的条件判断
#if 表达式:如果表达式为真(非 0),则保留后续代码#elif 表达式:当前面的#if条件不满足时,判断新的表达式(类似else if)#else:当前面所有条件都不满足时,保留后续代码示例 :根据
VERSION宏的值,编译不同版本的功能
c#define VERSION 2 // 版本号:1=基础版,2=高级版 #if VERSION == 1 void feature() { printf("基础版功能\n"); // 版本 1 编译这段 } #elif VERSION == 2 void feature() { printf("高级版功能(含扩展接口)\n"); // 版本 2 编译这段 } #else #error "不支持的版本号" // 版本不符时直接报错 #endif
7. 如何控制条件编译?
条件编译的 "开关"(宏定义)可通过两种方式控制:
- 代码中定义 :直接在源代码中用
#define MACRO定义宏(如:#define DEBUG)- 编译时传递 :通过编译器参数动态定义(如:
gcc -DDEBUG或gcc -DVERSION=2),无需修改代码即可切换条件简单说:条件编译就像给代码加了 "智能开关",让程序能根据不同场景 "按需编译",是提高代码复用性和灵活性的重要技术。
gcc 支持通过命令行参数定义宏,让我们无需修改代码,就能改变编译逻辑。
1. 基础用法:定义空宏 --->
gcc code.c -o code -DM
-D:是gcc定义宏的参数(D代表 Define )M:是宏的名称(这里定义了一个空宏M,没有值 )在代码中,可通过
#ifdef M判断是否编译某段代码:
c#ifdef M printf("M 宏已定义,这段代码会被编译!\n"); #else printf("M 宏未定义,这段代码被剔除!\n"); #endif
2. 带值宏定义:传递动态参数 --->
gcc code.c -o code -DM=100
M=100:定义宏M的值为100,等价于在代码最顶部插入:#define M 100在代码中,可通过
#if M == 100做更灵活的条件判断:
c#if M == 100 printf("M 的值是 100,执行专属逻辑!\n"); #else printf("M 的值不是 100,执行其他逻辑!\n"); #endif
8. 条件编译的核心价值怎么应用?
条件编译的核心价值:代码动态裁剪
- 条件编译的应用场景非常广泛,本质是让同一套代码适配不同需求,避免维护多份冗余代码
1. 软件功能分级(业务场景)
假设开发一款工具,分 "免费版" 和 "专业版":
c#define PRO_VERSION // 开发专业版时取消注释,或通过 gcc -DPRO_VERSION 启用 #ifdef PRO_VERSION // 专业版专属功能:如高级算法、更多接口 void advanced_feature() { ... } #else // 免费版功能:基础逻辑 void basic_feature() { ... } #endif
- 编译免费版:
gcc code.c -o free_app(不定义PRO_VERSION)- 编译专业版:
gcc code.c -o pro_app -DPRO_VERSION(通过-D启用专业版宏 )
2. 内核与系统开发(深度优化)
Linux 内核源码中,条件编译无处不在。例如,根据硬件平台(
ARM/x86)、功能开关(CONFIG_NET开启网络功能 )动态裁剪代码:
c#ifdef CONFIG_NET // 编译网络相关代码:协议栈、驱动 #include "net/network.c" #else // 剔除网络代码,减小内核体积 #define NET_DISABLED #endif通过这种方式,内核可适配不同设备(如:嵌入式设备可关闭不必要功能,减小体积 )
3. 开发工具与调试(效率提升)
开发阶段,可通过条件编译快速切换 "调试模式" 和 "发布模式":
c#define DEBUG // 开发时启用,发布时注释或通过 gcc -DDEBUG 控制 #ifdef DEBUG // 调试代码:打印详细日志、断言检查 #define LOG(...) printf(__VA_ARGS__) #else // 发布代码:剔除调试逻辑,提升性能 #define LOG(...) #endif
- 开发时:
gcc code.c -o app -DDEBUG→ 编译调试代码,方便排查问题- 发布时:
gcc code.c -o app→ 剔除调试代码,程序更简洁高效
总结:
- 条件编译的核心是 "让代码根据宏定义动态裁剪" ,而
gcc的-D参数让我们无需修改代码,就能通过命令行控制编译逻辑- 无论是
商业软件分级、内核裁剪,还是开发调试,条件编译都能帮我们用 同一套代码适配多场景,既减少冗余,又提升开发效率
9. 为什么编译C/C++代码要先变成汇编?
在 C/C++ 编译流程中,"先转汇编" 是连接高级语言和硬件的核心桥梁
一、从 "打孔编程" 到汇编:语言分层的必然
早期计算机没有 "高级语言",程序员直接用二进制打孔纸带写程序(如:上图的打孔编程)
但二进制难记、易错,于是出现了汇编语言 ------ 用
push、mov等 "助记符" 代替二进制指令,本质是 "机器码的人类友好版"随着开发需求变复杂,C/C++、Java、Python 等高级语言诞生(更贴近人类思维)。但硬件只能执行二进制机器码,因此需要一套 "翻译流程"
"先转汇编" 的核心意义是:在 "人类易写的代码" 和 "硬件能跑的机器码" 之间,插入一层 "可理解、可调试" 的中间层。
二、汇编的价值:让编译更可控、更灵活
(一)编译器的 "翻译逻辑" 需要中间层
编译器(如:
gcc)把 C/C++ 转成机器码时,并非 "直接翻译",而是拆成两步:
- 前端 :把 C/C++ 语法转成汇编代码(处理词法、语法分析,生成与架构无关的中间表示)
- 后端 :把汇编代码转成特定 CPU 架构的机器码(如:x86、ARM 指令集)
这种分层设计让编译器更灵活:
- 前端只需处理 "高级语言 → 汇编",适配不同语言(如:C++、Go 可共用同一套汇编后端)
- 后端只需处理 "汇编 → 机器码",适配不同 CPU 架构 (如:
gcc可同时支持 x86、ARM )(二)汇编是 "可调试的机器码"
汇编代码直接对应机器指令(如:
push %rbp就是一条 x86 指令的助记符),但比二进制更易读。如果编译流程跳过汇编:
- 开发者无法直接看到 "高级语言对应哪些机器指令",调试时会陷入二进制的 "黑盒"
- 编译器优化、硬件适配的逻辑会变得难以维护(比如不同 CPU 架构的指令差异,全靠二进制硬编码处理)
三、从汇编到机器码:编译器的 "自举" 之路
现代编译器(如:gcc)本身也是 "用高级语言写的程序",但最初的编译器必须用汇编甚至二进制编写(否则无法启动)
这涉及 "编译器自举" 的经典问题:
- 第一步:用汇编语言写一个 "能编译汇编代码的编译器"(即汇编器)
- 第二步:用汇编器编译更复杂的编译器(如用汇编写一个简单 C 编译器前端)
- 第三步:用 "简单 C 编译器" 编译更完整的 C 编译器(实现自举)
这个过程中,汇编是连接 "原始二进制" 和 "高级语言编译器" 的唯一桥梁 ------ 没有汇编,就无法从 "打孔纸带" 时代跨越到现代编程语言。

---------------自动化构建器---------------
1. 什么是make?什么是Makefile?
在软件开发,尤其是 C、C++ 等编译型语言的项目中,
make和makefile是极为重要的工具和文件。
- 它们能高效管理项目的编译流程,提升开发效率,是实现高效自动化构建的核心工具
make
make:是一个命令行工具,用于自动化构建和维护软件项目
- 它依据
Makefile文件中定义的规则,自动判断哪些文件需要重新编译,并执行相应的编译命令
原理:
make基于文件的时间戳来判断文件是否发生改变。
在项目编译过程中,源文件经过编译生成目标文件,目标文件再经过链接生成可执行文件
当再次执行
make时,它会对比源文件和目标文件的时间戳如果源文件的时间戳比目标文件新,就说明源文件发生了修改,
make会自动执行对应的编译命令,重新生成目标文件和可执行文件
例如在一个包含多个
.c文件的 C 项目中,当其中一个.c文件被修改后,make能识别到这一变化只重新编译这个修改过的
.c文件及其相关依赖,而不是重新编译整个项目的所有文件,从而大大节省了编译时间
优点:
- 极大地提高了大型项目的编译效率,减少了手动输入编译命令的繁琐和出错概率
- 特别是在项目规模不断扩大,文件数量众多且相互依赖关系复杂的情况下,
make的优势更加明显
场景:
广泛应用于各种编译型语言的项目中,如:C、C++、Fortran 等
在 Linux、Unix 以及 Windows 下的 MinGW 等开发环境中都可以使用
make工具来管理项目的编译
Makefile
Makefile:是一个文本文件,用于定义make工具构建项目所需的规则和指令
- 它包含了项目中各个文件之间的依赖关系 以及生成目标文件的具体命令
内容:
- 目标(target):可以是生成的
可执行文件、目标文件,或者是执行某个操作(如:清除编译生成的中间文件等)
- 例如:在一个 C 项目中,可执行文件 myprogram 就是一个常见的目标
- 依赖(dependency):指定生成
目标所依赖的文件
- 比如:生成可执行文件 myprogram 可能依赖于多个
.o目标文件,而这些目标文件又分别依赖于对应的.c源文件和头文件- 命令(command):用于描述如何从依赖文件生成目标文件的具体操作,通常是
编译命令、链接命令等
编译命令:使用 gcc 编译器将.c文件编译为.o文件的命令gcc -c file.c -o file.o链接命令:将多个.o文件链接成可执行文件的命令gcc file1.o file2.o -o myprogram
简单来说 :make 是执行自动化构建的工具,而 Makefile 则是告诉make如何进行构建的说明书,两者紧密配合,在软件开发项目中发挥着关键作用。
2. 如何编写Makefile文件?
-------第一版-------
入门 :写一个简单的Makefile文件
一个最简
Makefile规则如下:
bashapp: code.c # 目标: 依赖文件 gcc code.c -o app # 依赖方法(必须以 Tab 开头)拆解三个关键部分:
| 组件 | 作用 |
|---|---|
目标 |
要生成的文件 (如:可执行文件 app、中间文件 code.o 或伪目标 clean) |
依赖 |
生成目标需要的文件 (如:code.c 是编译 app 的原材料) |
依赖方法 |
生成目标的具体命令 (如:用 gcc 编译代码) |


假设目录中有
code.c(内容是printf("hello word!");)和上述Makefile,执行流程:
执行make命令
bashmakemake解析Makefile
检查目标 app 是否存在,以及依赖 code.c 的修改时间
发现app不存在 或 code.c 的修改时间更晚,执行依赖方法:
gcc -o code code.c生成可执行文件 app,项目构建完成
注意 :
makefile中,依赖方法的命令必须以 Tab 键开头,不能用空格替代。这是
make的语法规则,违反会直接报错(如:Makefile:2: *** missing separator. Stop.)
(1)为什么 make 提示 app is up to date?
因为 :
app的Modify时间 ≥code.c的Modify时间,make认为无需重新编译。
1. 文件的时间属性
Linux 中,文件有三类时间戳(可通过
stat命令查看 ):
- Modify(内容修改时间) :文件 内容 变化时更新
- Change(属性修改时间) :文件 权限、所有者 变化时更新
- Access(访问时间) :文件 被读取 时更新(现代系统可能默认不更新,避免 IO 开销 )
2. Make 的判断逻辑
make通过对比目标文件和依赖文件的Modify时间,决定是否重新构建:
- 若目标文件 的 Modify 时间更晚 → 跳过构建
- 若依赖文件 的 Modify 时间更晚 → 执行命令

-------第二版-------
需求:使Makefile支持清理编译产物
基础
Makefile内容:
makefileapp: code.c # 目标文件: 依赖文件 gcc code.c -o app # 编译命令(必须以Tab开头) .PHONY: clean # 声明clean为伪目标,不受同名文件影响,确保命令总能执行 clean: # 目标为clean,无依赖文件,随时可执行 rm -f app # 清理命令:强制删除编译生成的可执行文件app(-f确保无文件时不报错)

(1)为什么直接输入 clean 会报错?
因为 :
clean是 Makefile 中定义的目标,不是系统命令,必须通过make clean执行:
bashmake clean # 正确用法,触发清理逻辑
(2)伪目标 clean 为什么 "总是被执行"?
因为 :
.PHONY: clean声明了它是伪目标,make会忽略文件时间戳,强制执行其命令。
若目录中存在名为
clean的文件:
- 未声明
.PHONY: clean时,make clean会认为 "clean文件 已存在且** 无更新** ",跳过清理命令声明伪目标
.PHONY: clean的意义:
- 强制 make 执行 clean 目标的命令,忽略同名文件的时间戳
- make 会忽略文件时间戳,强制执行其命令

-------第三版-------
进阶 :变量与自动化规则解析
- 在 Makefile 中,
变量定义和自动化变量是提升规则复用性、简化复杂项目构建的核心技巧
makefile
#--------------- 定义变量 ---------------#
BIN=app
CC=gcc
SRC=code.c
FLAGS=-o
RM=rm -f
#--------------- 构建规则 ---------------#
$(BIN):$(SRC)
$(CC) $(FLAGS) $@ $^
#--------------- 清理规则 ---------------#
.PHONY: clean
clean:
$(RM) $(BIN)
一、变量定义:让规则更灵活
示例中定义了 5 个变量,作用是
"将重复内容抽象化",方便修改和维护:
makefileBIN = app # 可执行文件名(目标文件) CC = gcc # 编译器(如:gcc、clang) SRC = code.c # 源文件(.c 文件) FLAGS = -o # 编译选项(-o 用于指定输出文件) RM = rm -f # 清理命令(强制删除文件)
- 若要修改编译器(如:换
clang),只需改CC = clang,无需逐个替换规则中的gcc- 变量名语义化(如:
BIN代表可执行文件),让 Makefile 逻辑更清晰
二、规则与自动化变量:简化依赖描述
1. 目标与依赖的声明
makefile$(BIN) : $(SRC) # 目标: $(BIN)(即:app),依赖: $(SRC)(即:code.c) $(CC) $(FLAGS) $@ $^ # 编译命令
$(BIN) : $(SRC):等价于app : code.c,声明目标文件app依赖code.c
2. 自动化变量的作用
命令中的
$@和$^是 自动化变量,由 make自动替换:
变量 含义 替换示例(当前规则) $@目标文件(规则左侧的文件) app$^所有 依赖文件(规则右侧) code.c命令展开后 :
gcc -o app code.c优势 :
无需硬编码目标 和依赖 的文件名(如:app、code.c),规则可复用(换其他 目标/依赖 时,变量自动适配 )
三、清理规则:伪目标与变量复用
makefile.PHONY: clean # 声明 clean 为伪目标(避免与同名文件冲突) clean: # 清理目标 $(RM) $(BIN) # 删除 $(BIN)(即:app)
通过变量抽象 和自动化变量,Makefile 实现了:
- 配置集中化:修改编译选项、文件名只需改变量,无需改动规则
- 规则复用性:一套规则适配多文件、多目标,减少重复代码
- 逻辑清晰化 :语义化变量名(如:
BIN、CC)让 Makefile 更易读、维护掌握这些技巧,就能写出适配复杂项目的高效 Makefile,告别 "硬编码文件名" 的繁琐~
(1)变量名一定要大写吗?
不一定,但大写变量是行业惯例,用于区分:
- 自定义变量(如:
BIN、CC)- 自动化变量(如:
$@、$^,小写加符号)- 命令(如:
gcc、rm)这种约定能让 Makefile 结构更清晰,其他开发者一眼就能识别哪些是可配置的参数。
(2)$符号是干什么用的?
$符号 :是变量和特殊字符的标识符 ,用于触发变量替换或调用特殊功能
- 它是 Makefile 语法的核心符号之一
- 它的作用可以分为两类:引用变量、特殊符号
一、引用变量:(变量名) 或 变量名
$最常用的场景是引用已定义的变量 ,告诉make工具:"这里需要替换为变量的值"
makefile# 定义变量 BIN = app CC = gcc # 引用变量 $(BIN): code.c $(CC) -o $@ code.c # $(CC) 会替换为 gcc
$(BIN)会被替换为app(目标名)$(CC)会被替换为gcc(编译器命令)两种写法:
- 推荐用
$(变量名)(如:$(CC)),兼容性更好,尤其适合长变量名- 短变量名也可以用
$变量名(如:$CC),但可读性较差,不推荐
二、特殊符号:
$@、$^等自动化变量
$后跟特定字符(如:@、^、<)时,代表 Makefile 预定义的 "自动化变量",用于动态获取目标、依赖等信息,避免硬编码常见自动化变量:
符号 含义 示例(目标 app: code.c utils.c)$@代表当前规则的目标文件 替换为 app$^代表当前规则的所有依赖文件 替换为 code.c utils.c$<代表当前规则的第一个依赖文件 替换为 code.c$*代表 目标文件名中去掉后缀的部分若目标是 app.o,则替换为app
makefileapp: code.c utils.c gcc -o $@ $^ # 等价于 gcc -o app code.c utils.c
$@自动替换为目标app$^自动替换为所有依赖code.c utils.c
三、转义 $ 符号:
- 如果需要在命令中输出实际的
$符号 (如:shell 中的变量引用),需要用$$转义(第一个$是转义符)
makefileprint: @echo "当前目录: $$PWD" # 输出 shell 变量 PWD 的值
- 执行
make print会显示:当前目录: /home/user/project- 这里
$$PWD会被 shell 解析为$PWD(当前目录路径)
总结:
$符号是 Makefile 的 "变量触发器"
- 配合变量名(如:
$(CC)):引用 自定义变量- 配合特殊字符(如:
$@):调用 自动化变量- 用
$$:输出实际的$符号掌握
$的用法,是写出简洁、灵活的 Makefile 的基础。
(3)为什么要用 @ 和 ^ 这样的自动化变量?
问题 1:直接写文件名不更直观吗?
如果不用自动化变量,命令会变成:
makefile$(BIN): $(SRC) $(CC) $(FLAGS) $(BIN) $(SRC)
这种写法有两个隐患:
一致性风险
若目标名与命令中的输出文件名不一致(如:目标写
app但命令写gcc -o app_v2 $(SRC)),会导致 make 误判 "目标已生成",实际却生成了错误的文件。$@ 能强制保证命令输出与目标名一致多源文件
若有多个源文件(如:
myproc.c、utils.c),可仅修改变量:SRC = myproc.c utils.c # 多源文件用空格分隔,而规则无需改动,$^会自动替换为所有依赖文件 :gcc -o proc.exe myproc.c utils.c
makefile$(BIN):$(SRC) $(CC) $(FLAGS) $@ $^
问题 2:@ 和 ^ 记不住怎么办?
可以通过 "语义联想" 记忆:
$@:@像 "目标靶心",代表目标文件(Target)$^:^像 "一堆文件",代表所有依赖文件(Dependencies)
make还提供其他常用自动化变量:
$<:代表第一个依赖文件(适合单文件编译)$*:代表目标文件名去掉后缀 (如:目标app.o对应app)
-------第四版-------
需求 :带调试信息的 Makefile
makefile
# ================ 基础变量定义 ================
BIN = app # 最终生成的可执行文件名
CC = gcc # 使用的编译器(GNU Compiler Collection)
SRC = code.c # 核心源文件(单个C文件场景)
FLAGS = -o # 编译器输出选项(-o用于指定输出文件)
RM = rm -f # 清理命令(强制删除文件)
# ================ 编译规则 ================
$(BIN): $(SRC)
@$(CC) $(FLAGS) $@ $^
@echo "linking ... $^ to $@" # 调试信息:显示链接过程
# ================ 清理规则 ================
.PHONY: clean
clean:
@$(RM) $(BIN)
@echo "remove ... $(BIN)" # 调试信息:显示清理过程
(1)@符号是干什么用的?
@ 符号:是一个命令前缀 ,作用是隐藏命令本身的输出,只显示命令执行的结果。
- 它能让构建过程的终端输出更简洁,突出关键信息
具体效果对比,假设 Makefile 中有一条编译命令:
如果命令不带@:
makefile# 不带@的命令 $(BIN): $(SRC) gcc -o app code.c # 无@符号 echo "编译完成"执行
make时,终端会同时显示命令本身和执行结果:
bashgcc -o app code.c # 命令本身被打印出来 编译完成 # echo 命令的输出
如果命令带上@:
makefile# 带@的命令 $(BIN): $(SRC) @gcc -o app code.c # 有@符号 @echo "编译完成"执行
make时,终端只显示命令的执行结果,不显示命令本身:
bash编译完成 # 只显示echo的输出,gcc 命令被"隐藏"了
使用场景:
- 简化输出 :对于
gcc、rm等工具命令,通常不需要在终端重复显示完整命令(尤其是长命令),用@可以减少冗余信息- 突出关键信息 :配合
echo命令时,@echo "正在编译..."只会显示提示文本,让开发者专注于流程进度,而不是命令细节- 按需调试 :如果需要排查问题,可以临时去掉
@,让 Makefile 打印出实际执行的命令,便于分析哪里出错(例如确认变量是否正确替换)
总结:
@符号的核心作用是控制命令的显示行为,平衡输出简洁性和调试需求- 它不影响命令的实际功能,只改变终端的输出效果
-------第五版-------
高阶 :Makefile 多文件编译自动识别源文件与对象文件
makefile
# ================ 基础配置 ================
BIN = app # 最终生成的可执行文件名
CC = gcc # 使用的编译器(GNU Compiler Collection)
# wildcard 是 Makefile 函数,用于匹配文件
SRC = $(wildcard *.c) # 自动获取当前目录下所有 .c 文件
# 语法:$(变量名:原后缀=新后缀)
OBJ = $(SRC:.c=.o) # 自动生成对应 .o 文件名(替换 .c 为 .o)
LFLAGS = -o # 链接选项(-o 用于指定输出文件)
FLAGS = -c # 编译选项(-c 表示只编译生成 .o 文件)
RM = rm -f # 清理命令(强制删除文件)
# ================ 链接规则 ================
$(BIN): $(OBJ)
# $@:自动替换为目标文件(app)
# $^:自动替换为所有依赖文件(*.o)
@$(CC) $(LFLAGS) $@ $^ # 链接命令(生成可执行文件)
@echo "linking ... $^ to $@" # 调试信息:显示链接过程
# ================ 编译规则 ================
# % 是通配符,匹配任意文件名前缀
%.o: %.c # 模式规则:自动将 .c 文件编译为 .o 文件
# $<:自动替换为当前 .c 文件(第一个依赖)
# $@:自动替换为目标 .o 文件
@$(CC) $(FLAGS) $< -o $@
@echo "compiling ... $< to $@" # 调试信息:显示编译过程
# ================ 清理规则 ================
.PHONY: clean # 声明伪目标(避免与同名文件冲突)
clean:
$(RM) $(OBJ) $(BIN) # 删除所有 .o 文件和可执行文件
@echo "cleaned: $(OBJ) $(BIN)" # 调试信息:显示清理完成
(1)$(wildcard *.c) 是什么意思?
$(wildcard *.c):是一个文件匹配函数 ,作用是自动查找当前目录下所有后缀为.c的源文件,并将它们的文件名以空格分隔的形式返回。
作用解析:
wildcard 是 Makefile 的内置函数:专门用于匹配文件路径,语法为:$(wildcard 匹配模式)
- 其中
匹配模式支持通配符(如:*代表任意字符序列)
\*.c 是匹配模式:表示 "所有以.c结尾的文件"整体效果:
- 假设当前目录有
main.c、utils.c、log.c三个源文件- 那么:
SRC = $(wildcard *.c)会自动将SRC赋值为:main.c utils.c log.c(空格分隔的文件名列表)
为什么要用
$(wildcard *.c)?手动写源文件列表(如
SRC = main.c utils.c)存在两个问题:
- 新增文件需手动维护 :当项目添加
net.c等新文件时,必须修改SRC变量,否则编译会遗漏- 容易拼写错误 :手动输入长文件名时可能写错(如:
util.c少写一个s),导致编译错误而
$(wildcard *.c)能 自动同步源文件列表 ,新增.c文件后无需修改 Makefile,直接执行make即可包含新文件。
扩展用法:
wildcard支持更复杂的匹配模式:
- 匹配特定前缀的
.c文件 :$(wildcard net_*.c)(如net_socket.c、net_client.c)- 匹配子目录的
.c文件 :$(wildcard src/*.c)(查找src目录下的.c文件)- 多模式匹配 :
$(wildcard *.c *.h)(同时匹配.c和.h文件)
搭配其他函数使用:
wildcard常与patsubst(字符串替换函数)配合,自动生成目标文件列表:
makefile# 查找所有 .c 文件 SRC = $(wildcard *.c) # 将 .c 后缀替换为 .o,生成对应的目标文件列表 OBJ = $(patsubst %.c, %.o, $(SRC))如果
SRC = main.c utils.c,则OBJ会自动变为main.o utils.o,实现源文件与目标文件的自动关联。
(2)$(SRC:.c=.o) 是什么意思?
$(SRC:.c=.o):是一种字符串替换语法 ,作用是将变量SRC中所有以.c为后缀的文件名,统一替换为.o后缀,生成对应的目标文件列表。
语法格式 :
$(变量名:原后缀=新后缀)这是 Makefile 中简化的字符串替换写法,等价于更完整的
$(patsubst 原后缀, 新后缀, $(变量名))函数。实际效果:
假设 SRC 变量的值是
main.c utils.c log.c(通过$(wildcard *.c)自动获取的.c文件列表)那么
OBJ = $(SRC:.c=.o)会将OBJ变量赋值为:main.o utils.o log.o(每个.c文件名都被替换成了.o)
为什么需要这种替换?
在 C 语言编译流程中,
.c源文件需要先编译为.o目标文件,再通过链接生成可执行文件。这种替换的核心价值是:
- 自动关联源文件与目标文件 :
无需手动编写OBJ = main.o utils.o log.o,而是通过SRC自动推导,确保.o文件列表与.c文件列表始终同步。- 适配增量编译 :
Makefile 的增量编译依赖.o文件(仅当.c文件修改时,才重新编译对应的.o文件)。通过这种替换,能自动维护所有.o文件的依赖关系。
(3)%符号是干什么用的?
%: 是通配符 ,用于匹配 "任意长度的字符串",主要作用是定义通用规则(模式规则),让一套规则适配用于多个文件,避免重复编写相似规则。
一、% 的核心用法:模式规则
%最常见的场景是在模式规则中,用于匹配文件名的 "前缀部分",实现 "一类文件对应一类目标" 的通用编译逻辑基本语法:
makefile# 模式规则:左边是目标模式,右边是依赖模式 目标模式: 依赖模式 命令
- 其中
目标模式和依赖模式都包含%,且%在两边代表相同的字符串(即 "前缀相同")
示例:编译 .c 文件为 .o 文件
makefile# 模式规则:所有 .o 文件依赖于同名的 .c 文件 %.o: %.c gcc -c $< -o $@ # 编译命令
%.o:表示 "所有以.o结尾的文件"(如:main.o、utils.o)%.c:表示 "所有以.c结尾的文件"(如:main.c、utils.c)- %在两边匹配相同的前缀:
- 当需要生成
main.o时:%匹配main,依赖自动定位到main.c- 当需要生成
utils.o时:%匹配utils,依赖自动定位到utils.c结合模式规则
%.o: %.c,这条命令能实现 "所有.c文件自动编译为同名.o文件",是多文件项目的核心编译逻辑。
二、% 的匹配逻辑:"一对一" 对应
%的匹配遵循 "相同前缀对应" 原则,确保目标文件与依赖文件的 "主体名称一致"反例:不匹配的情况,如果有规则
a%.o: b%.c,则:
a1.o会匹配b1.c(%对应1)a2.o会匹配b2.c(%对应2)- 但
a1.o不会匹配b2.c(前缀不对应)
三、% 与其他通配符的区别
在 Makefile 中,
*也是通配符,但与%的作用不同:
*:用于 匹配文件列表 (如:*.c表示所有.c文件),通常在变量定义中使用(如:SRC = $(wildcard *.c))%:用于 定义模式规则,实现 "目标与依赖的对应关系",仅在规则中使用
(4)@(CC) (FLAGS) \< -o @ 怎么理解?
这条命令是 Makefile 中编译 C 语言源文件的核心命令 ,结合了
变量、自动化变量和命令控制符,实现了 "将.c源文件编译为.o目标文件" 的自动化流程。
我们可以拆解为 5 个部分理解:
@$(CC) $(FLAGS) $< -o $@
1. @:隐藏命令本身的输出
- 作用 :执行命令时,终端只显示命令的结果(如:编译过程的报错信息),不显示命令本身
- 对比 :
- 不带
@:终端会打印完整命令(如:gcc -c main.c -o main.o)- 带
@:只显示编译过程中产生的 警告/错误(若有),终端输出更简洁
2. $(CC):引用编译器变量
- 含义 :
CC是自定义变量(通常定义为CC = gcc),$(CC)会替换为实际的编译器命令(如:gcc或clang)- 作用 :集中管理编译器,如需更换编译器,只需修改
CC变量(如:CC = clang),无需改动命令
3. $(FLAGS):引用编译选项变量
- 含义 :
FLAGS是自定义变量(通常定义为FLAGS = -c),$(FLAGS)会替换为编译选项- 选项 :
-c:表示 "只编译不链接",生成.o目标文件(而非可执行文件)- 可扩展其他选项:如:
FLAGS = -c -Wall -g(-Wall开启所有警告,-g生成调试信息)
4.
$<:自动化变量(第一个依赖文件)
- 含义 :自动替换为当前规则中的第一个依赖文件 (通常是
.c源文件)
- 示例 :若规则是
main.o: main.c,则$<替换为main.c
5. -o $@:指定输出文件
-o:编译器的输出选项,用于指定生成文件的名称$@:自动化变量,自动替换为当前规则的目标文件 (通常是.o目标文件)
- 示例 :若规则是
main.o: main.c,则$@替换为main.o,-o $@即-o main.o
3. make是如何工作的?
Makefile 代码
makefilemyproc:myproc.o gcc myproc.o -o myproc myproc.o:myproc.s gcc -c myproc.s -o myproc.o myproc.s:myproc.i gcc -S myproc.i -o myproc.s myproc.i:myproc.c gcc -E myproc.c -o myproc.i .PHONY:clean clean: rm -f *.i *.s *.o myproc编译执行的命令流程 (模拟
make执行时的分步命令 )
bash# 执行 make 后,实际依次触发的编译相关命令 gcc -E myproc.c -o myproc.i gcc -S myproc.i -o myproc.s gcc -c myproc.s -o myproc.o gcc myproc.o -o myproc

当我们在终端输入
make命令时,背后是一套依赖驱动的自动化构建流程
当只输入
make时,make会按以下步骤执行:1. 寻找构建规则文件
make会在当前目录 下查找名为Makefile或makefile的文件:
- 若找到,读取文件内容
- 若未找到,报错退出
2. 确定最终目标
make会扫描Makefile,找到第一个目标(即文件中最顶部的目标)作为 "最终构建目标":
makefilemyproc: myproc.o # 第一个目标 → 最终目标 gcc -o myproc myproc.o myproc.o: myproc.c gcc -c myproc.c此时,
myproc被选为最终目标,make的任务是确保它被正确构建。
3. 检查依赖与时间戳
make会对比目标文件 和依赖文件 的修改时间(Modify):
- 若目标文件(如:
myproc)不存在 → 需要构建- 若依赖文件(如:
myproc.o)的修改时间晚于目标文件 → 需要重新构建(确保目标是最新的)
4. 递归处理依赖(堆栈式查找)
如果目标依赖的文件(如:
myproc.o)不存在,make会在Makefile中递归查找该依赖的构建规则:
- 检查
myproc→ 依赖myproc.o,但myproc.o不存在- 在
Makefile中查找myproc.o的规则 → 发现myproc.o: myproc.c- 检查
myproc.o的依赖myproc.c→ 假设存在且是最新的- 执行
myproc.o的构建命令(gcc -c myproc.c),生成myproc.o
5. 执行构建命令,生成最终目标
当所有依赖都准备好(如:
myproc.o已生成),make执行最终目标的构建命令:
- 执行
gcc -o myproc myproc.o,生成最终可执行文件myproc

