当前位置: 首页 > news >正文

第9课:Linux开发工具(四):make与makefile

第9课:Linux开发工具(四):make与makefile

一、为什么我们需要 Makefile?

1.1 IDE 背后的秘密

在使用 Visual Studio 等 IDE 时,我们只需按下 F5 或点击"编译"按钮,程序就会自动完成编译、链接并运行。但我们从未关心过这个过程是如何发生的。

IDE 实际做的事情:

  1. 将每个源文件(.c/.cpp)分别编译成目标文件(Windows 下是 .obj,Linux 下是 .o)
  2. 将所有目标文件和库文件链接在一起,形成最终的可执行程序(.exe)

核心结论:当项目只有1-2个源文件时,手动编译没问题。但当项目有100个、1000个甚至上万个源文件时,手动输入编译命令就变得不现实了。

1.2 什么是"构建"?

老师不喜欢抽象名词,用大白话解释:

构建 = 把你的所有源文件走一遍程序翻译的完整流程,最终形成可执行程序的整个过程。

1.3 Windows vs Linux 构建方式对比
系统自动化构建工具特点
WindowsVisual Studio 集成环境完全自动化,用户无感知
Linuxmake + Makefile工具独立,需要开发者自己维护构建规则

核心结论:make 是一条命令,Makefile 是一个文件。两者配合完成 Linux 下的项目自动化构建。

二、Makefile 的核心灵魂:依赖关系与依赖方法

2.1 第一个最简单的 Makefile

假设我们有一个test.c源文件,要生成test.exe可执行程序。

步骤:

  1. 在当前目录下新建一个文件,文件名必须是Makefilemakefile(推荐首字母大写)
  2. 在文件中写入以下内容:
test.exe: test.c gcc -o test.exe test.c
  1. 在命令行中执行make命令,即可自动编译生成test.exe
2.2 核心概念详解

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

  • 依赖关系目标文件: 依赖文件列表
    • 冒号左侧:要生成的目标文件
    • 冒号右侧:生成目标文件所需要的所有依赖文件
  • 依赖方法:以Tab 键开头的命令行,说明如何从依赖文件生成目标文件

易错警告:依赖方法必须以 Tab 键开头,不能用空格代替!这是 Makefile 的语法强制要求,没有为什么。

2.3 生活化理解:为什么需要两者同时存在?

老师用了一个非常形象的例子:

月底你给爸爸打电话:“爸,我是你儿子。”(只表明了依赖关系)

你爸会很奇怪:“这小子是不是疯了?”

正确的做法是:“爸,我是你儿子,今天中午12点前给我农行卡打1000块钱。”(同时表明了依赖关系和依赖方法)

结论:任何事情的完成,都必须同时具备合理的依赖关系和可行的依赖方法。Makefile 也不例外。

三、Makefile 的工作原理:推导栈与递归思想

3.1 完整的程序翻译过程

为了理解 Makefile 的推导过程,我们先回顾 C 程序的完整翻译步骤:

test.c → 预处理 → test.i → 编译 → test.s → 汇编 → test.o → 链接 → test.exe
3.2 模拟完整翻译过程的 Makefile

我们可以写出一个展示完整翻译过程的 Makefile:

test.exe: test.o gcc -o test.exe test.o test.o: test.s gcc -c test.s -o test.o test.s: test.i gcc -S test.i -o test.s test.i: test.c gcc -E test.c -o test.i
3.3 make 命令的解析过程

当我们执行make命令时,make 会:

  1. 从 Makefile 的第一个目标开始解析(这里是test.exe
  2. 检查目标文件是否存在,以及所有依赖文件是否都是最新的
  3. 如果某个依赖文件不存在,或者比目标文件更新,就会去查找该依赖文件的生成规则
  4. 这个过程会一直持续下去,直到找到一个已经存在的源文件(这里是test.c
  5. 然后从最底层开始,依次执行依赖方法,生成上一层的文件,直到最终生成目标文件

核心比喻:这个过程就像一个栈结构(先进后出),或者函数递归test.c就是递归的出口。

四、项目清理与伪目标 .PHONY

4.1 为什么需要清理项目?

一个完整的项目不仅要能构建,还要能清理。清理就是删除所有生成的中间文件(.i、.s、.o)和最终的可执行程序。

4.2 初步的清理目标

我们可以在 Makefile 中添加一个clean目标:

clean: rm -f test.exe test.i test.s test.o

执行make clean命令,就会自动删除这些文件。

4.3 问题出现了!

如果当前目录下恰好有一个名为clean的文件,那么执行make clean时,make 会认为clean文件已经是最新的,不会执行任何命令。

4.4 解决方案:伪目标 .PHONY

.PHONY是 Makefile 中的一个特殊关键字,用来声明伪目标

语法:

.PHONY: clean clean: rm -f test.exe test.i test.s test.o

核心结论:.PHONY修饰的目标,总是会被执行,无论当前目录下是否存在同名文件,也不会进行时间对比。

五、Makefile 高效编译的秘密:文件时间对比

5.1 为什么第二次 make 不会重新编译?

当我们第一次执行make生成test.exe后,再次执行make,会看到提示:

make: 'test.exe' is up to date.

这是因为 make 会比较源文件和目标文件的修改时间

  • 如果源文件的修改时间比目标文件晚 → 源文件被修改过,需要重新编译
  • 如果目标文件的修改时间比源文件晚 → 源文件没有被修改,不需要重新编译

核心价值:这种机制可以大大提高大型项目的编译效率。当你只修改了一个源文件时,make 只会重新编译这一个文件,然后重新链接,而不是编译整个项目。

5.2 Linux 文件的三个时间属性

使用stat 文件名命令可以查看文件的详细时间信息:

时间属性英文全称含义
AccessAccess Time最近一次访问文件内容的时间
ModifyModify Time最近一次修改文件内容的时间
ChangeChange Time最近一次修改文件属性的时间(如权限、大小等)

关键区别:

  • 修改文件内容 → Modify 时间和 Change 时间都会更新(因为内容改变会导致文件大小等属性改变)
  • 只修改文件属性(如chmod命令)→ 只有 Change 时间会更新
5.3 为什么 Access 时间不会每次访问都更新?

老师提出了一个深刻的问题:为什么我们多次cat同一个文件,Access 时间却不一定更新?

原因:

  1. 系统中读操作的频率远远高于写操作
  2. 如果每次访问文件都更新 Access 时间,就意味着每次读操作都要伴随一次写磁盘操作(更新文件属性)
  3. 磁盘 IO 是计算机系统中最慢的操作之一,频繁的写磁盘会严重降低系统性能

解决方案:现代 Linux 系统会对 Access 时间的更新进行优化,通常是每隔一段时间或累计一定次数的访问后才更新一次。

六、Makefile 语法进阶

6.1 禁止命令回显:@符号

默认情况下,make 会把它执行的每一条命令都打印到终端上。如果我们不想看到这些命令,可以在命令前加上@符号。

示例:

test.exe: test.c @echo "开始编译代码..." @gcc -o $@ $^ @echo "编译完成!" .PHONY: clean clean: @echo "清理工程..." @rm -f test.exe @echo "清理完毕!"

执行make时,只会看到我们自定义的提示信息,不会看到实际执行的 gcc 和 rm 命令。

6.2 自定义变量

当 Makefile 变得复杂时,使用变量可以让代码更简洁、更易维护。

定义变量:

BIN = test.exe SRC = test.c

使用变量:

$(BIN): $(SRC) @gcc -o $(BIN) $(SRC) .PHONY: clean clean: @rm -f $(BIN)

注意:

  1. 等号两侧可以有空格:定义变量时,等号左右两侧允许加空格,并且强烈推荐加上,以提高代码的可读性(如:BIN = test.exe)。
  2. 千万警惕“尾随空格”(致命陷阱):Makefile 中的所有变量本质上都是字符串。它会自动忽略等号前后的空格,但会把变量值后面的所有空格当成值的一部分!
    • 例如:如果写成 BIN = test.exe (末尾不小心敲了几个空格),那么 BIN 的实际值就是 "test.exe "。这会导致后续编译器找不到对应的文件而报错。
  3. 区分 Shell 脚本:如果你在写 Shell 脚本(.sh),等号两边才是绝对不能加空格的(必须写成 BIN=test.exe),不要和 Makefile 搞混。
6.3 内置自动变量

Makefile 提供了一些非常有用的内置自动变量,它们会根据当前的规则自动展开:

自动变量含义
$@表示规则中的目标文件
$<表示规则中的第一个依赖文件
$^表示规则中的所有依赖文件,以空格分隔

使用示例:

test.exe: test.c @gcc -o $@ $< # $@ 展开为 test.exe,$< 展开为 test.c

七、处理多文件项目

7.1 问题:100个源文件怎么办?

如果我们的项目有100个源文件:main.csrc1.csrc2.csrc100.c,难道我们要在 Makefile 中把它们一个个列出来吗?

7.2 解决方案一:使用 shell 命令获取源文件列表
SRC = $(shell ls *.c)

$(shell 命令)会执行括号中的 shell 命令,并将命令的输出结果作为变量的值。这里ls *.c会列出当前目录下所有的 .c 文件。

7.3 解决方案二:使用 wildcard 函数(推荐)

Makefile 内置的wildcard函数专门用来获取符合特定模式的文件名:

SRC = $(wildcard *.c)
7.4 将 .c 后缀替换为 .o 后缀

我们需要将所有的 .c 源文件编译成对应的 .o 目标文件。可以使用 Makefile 的变量替换功能:

OBJ = $(SRC:.c=.o)

这行代码的意思是:将SRC变量中所有以.c结尾的字符串,替换为以.o结尾。

7.5 模式规则:%.o: %.c

现在我们需要一个通用的规则,告诉 make 如何将任意一个 .c 文件编译成对应的 .o 文件:

%.o: %.c @gcc -c $< @echo "编译 $< 完成"

这里的%是一个通配符,它会匹配任意字符串。例如,当 make 需要生成main.o时,它会自动匹配这条规则,将%替换为main,然后执行gcc -c main.c

7.6 多文件项目的完整 Makefile
BIN = bite.exe SRC = $(wildcard *.c) OBJ = $(SRC:.c=.o) $(BIN): $(OBJ) @echo "链接所有目标文件..." @gcc -o $@ $^ @echo "生成可执行文件 $@ 完成!" %.o: %.c @echo "编译 $< ..." @gcc -c $< .PHONY: clean clean: @echo "清理工程..." @rm -f $(OBJ) $(BIN) @echo "清理完毕!"
7.6.1 符号详解

1. 变量操作符号 (=,$())

对应行:1, 2, 3, 5, 7 等

  • =(赋值号):最基本的变量赋值。如第 1 行BIN = bite.exe,将右边的字符串赋值给左边的变量。
  • $()(取值/展开符):用来获取变量的值,或者调用 Makefile 的内置函数。
  • 例如$(BIN)就是把BIN的值bite.exe提取出来。在 Makefile 中,只要想使用变量,就必须用$()包裹它(单字符变量除外,但建议全包)。

2. 内置函数与文本处理

对应行:2, 3

  • wildcard(通配符函数):* 语法:$(wildcard 匹配模式)

  • 解释:第 2 行$(wildcard *.c)的意思是,去当前目录下找所有以.c结尾的文件,并把它们的名字用空格拼成一长串字符串赋值给SRC(比如main.c utils.c)。

  • :.c=.o(模式替换语法):

  • 语法:$(变量名:原后缀=新后缀)

  • 解释:第 3 行$(SRC:.c=.o)是一个非常巧妙的文本替换。它会把SRC变量里所有的.c结尾的字符串,全部替换成.o。如果SRCmain.c utils.c,那么OBJ就会自动变成main.o utils.o

3. 规则定义与模式匹配 (:,%)

对应行:5, 10

  • :(规则分隔符):

  • 语法:目标 : 依赖

  • 解释:告诉 make,左边的文件是怎么来的(依赖于右边的文件)。

  • %(模式通配符):

  • 解释:第 10 行%.o: %.c称为“模式规则” (Pattern Rule)。这里的%就像一个占位符(Stem)。

  • 含义:它告诉 Make 一个通用的道理——“任何一个.o文件,都依赖于和它同名的.c文件”。当 Make 需要生成main.o时,它会自动套用这条规则,把%替换成main,去寻找main.c。这比你手动一行行写main.o: main.c要聪明得多。

4. 自动变量 ($@,$<,$^)

对应行:7, 8, 11, 12

  • $@:代表冒号左边的目标文件。在第 7 行里它就是bite.exe;在第 12 行里,它就是当时匹配到的那个.o文件。
  • $^:代表冒号右边的所有依赖文件。在第 7 行里,它代表所有的.o文件。所以gcc -o $@ $^实际上是在把所有.o文件链接成一个bite.exe
  • $<:代表冒号右边的第一个依赖文件。在第 12 行的编译命令gcc -c $<中,它代表当前正在被编译的那个.c源文件。

5. 命令控制符号 (@)

对应行:6, 7, 8, 11 等所有缩进的执行命令

  • @(静默执行符):* 解释:默认情况下,Make 在执行命令前,会先把这行命令原原本本地打印到屏幕上。如果在命令前面加上@,Make 就只会执行命令,而不会在屏幕上回显命令本身
  • 作用:让终端输出更干净。比如你只想看到@echo打印出来的中文提示语,而不想看到系统把echo "链接所有目标文件..."这句代码本身也打印一遍。

6. 特殊伪目标 (.PHONY)

对应行:14

  • .PHONY(声明伪目标):
  • 解释:第 14 行.PHONY: clean告诉 Make,clean不是一个真正的文件名,它只是一个“动作的代号”(Pseudo-target)。
  • 为什么要加?假设你的目录下刚好新建了一个叫clean的文件,如果你没加.PHONY,当你敲make clean时,Make 会发现“咦,clean文件已经存在了,且没有依赖项需要更新”,它就会罢工,不再执行下面的删除命令。加上.PHONY后,无论有没有同名文件,Make 都会强制执行clean标签下的命令。

总结这个 Makefile 的工作流:

  1. 收集当前所有的.c文件(第 2 行)。
  2. 推导出需要生成的.o文件列表(第 3 行)。
  3. 发现终极目标bite.exe需要所有的.o文件(第 5 行)。
  4. 于是自动利用模式规则(第 10 行),把一个个.c编译成.o
  5. 所有.o都准备好后,把它们链接成最终的bite.exe(第 7 行)。
7.6.2 增量编译

疑问:为什么不能像链接那样,把所有.c一次性全编译了?

这个想法在底层是可以实现的。gcc确实支持你一口气传入所有的源文件,比如执行gcc -c main.c utils.c,它也会乖乖吐出main.outils.o

但是,如果在 Makefile 里这么写,就完全毁了 Makefile 的“灵魂”!

Makefile 存在的最大意义叫作:增量编译(Incremental Build)

  • 如果按照这个设想,写成“全包”的形式:
# 假设我们这么写(反面教材) $(OBJ): $(SRC) @echo "一次性编译所有文件..." @gcc -c $^ # 把所有 .c 一起喂给 gcc

致命后果:假设你的项目有 1000 个.c文件。今天你只修改了其中1 个文件(比如utils.c)的一行代码。当你敲下make时,由于所有文件被绑在了一起,Make 会把这 1000 个文件全部重新编译一遍!本来 0.1 秒能搞定的事,你要等 5 分钟。

  • 现在的写法(分离式:%.o: %.c):
    Make 为每一个.o建立了独立的依赖关系。
    当你只修改了utils.c时,Make 会检查时间戳:
  1. main.cmain.o旧 -> 不需要重新编译。
  2. utils.cutils.o新(因为你刚改了它) ->只触发utils.o: utils.c这一条规则,只重新编译utils.c
  3. 最后再把现成的main.o和刚生成的utils.o重新链接成.exe

总结:链接(打包)必须大家一起上($^),但编译(加工)必须拆开单干($<),就是为了“谁改了就只编译谁”,极大提高大型项目的编译速度。

八、通用 Makefile 最佳实践

8.1 进一步变量化

为了让 Makefile 更加通用,我们可以把编译器、命令等也都变量化:

BIN = bite.exe SRC = $(wildcard *.c) OBJ = $(SRC:.c=.o) # 编译器 CC = gcc # 回显命令 ECHO = echo # 删除命令 RM = rm -rf $(BIN): $(OBJ) @$(ECHO) "linking $^ to $@ ... done" @$(CC) -o $@ $^ %.o: %.c @$(ECHO) "compiling $< to $@ ... done" @$(CC) -c $< .PHONY: clean clean: @$(ECHO) "cleaning project ..." @$(RM) $(OBJ) $(BIN) @$(ECHO) "clean done!"
8.2 这个通用 Makefile 的优势
  1. 高度通用:将这个 Makefile 复制到任何 C 语言项目目录下,基本都能直接使用
  2. 易于修改:如果需要切换到 C++ 编译器,只需将CC = gcc改为CC = g++
  3. 清晰易读:所有的配置都集中在文件开头,一目了然
  4. 高效编译:只重新编译被修改过的源文件
8.3 调试技巧:添加 test 目标

在编写 Makefile 的过程中,我们经常需要查看变量的值是否正确。可以添加一个test伪目标来帮助调试:

.PHONY: test test: @echo "SRC = $(SRC)" @echo "OBJ = $(OBJ)"

执行make test命令,就可以看到SRCOBJ变量的实际值。

http://www.jsqmd.com/news/824239/

相关文章:

  • 抖音去水印视频解析用什么工具?免费又安全的解析工具推荐,2026 亲测有效 - 爱上科技热点
  • 互联网大厂Java求职面试:从Spring Boot到微服务的探索
  • Agent从“能用“到“管好“,中间差了什么?
  • 2026年手机免费一键去水印App排行榜 | 手机免费一键去水印App推荐测评 - 爱上科技热点
  • 信道估计模块
  • 【机器人】基于QLearning强化学习的AGV智能搬运机器人快递搬运系统matlab仿真
  • 视频去水印无损工具推荐:去水印后和原视频一样,2026实测最有效的方法 - 爱上科技热点
  • 手机端视频转音频教程 几步搞定不用安装软件 - 爱上科技热点
  • 嵌入式开发利器:核心板如何加速硬件设计并降低风险
  • 基于模板与数据分离的自动化求职信生成工具实践
  • 制造业供应链从“各自为战”到“智能协同”
  • macOS开发者的端口管理利器:Porthole仪表盘的设计原理与实战指南
  • 抖音图片怎样去水印?2026 实测去水印方法与在线工具对比指南 - 爱上科技热点
  • 为什么传统情感分析工具在社交媒体上总是“误判“?VADER如何用词典+规则破解这一难题
  • Windows下基于Cygwin构建ESP32交叉编译工具链全攻略
  • 别再瞎忙活了!Paperxie 本科论文写作,直接把流程给你 “拆碎了喂”
  • Java程序员必看:拥抱AI,掌握大模型,收藏这份零基础进阶教程!
  • 图片去水印软件哪个好用?好用的去水印工具推荐,2026年最新排行榜实测 - 爱上科技热点
  • 【滤波跟踪】轨迹测量Poisson多伯努利混合(TM-PMBM)滤波器的Matlab代码
  • 2026年5月热门的睡篮推车二合一婴儿车/一键折叠婴儿车产品推荐唯乐宝 - 品牌鉴赏师
  • 利用 Taotoken 模型广场为不同智能体任务选择合适的模型
  • 如何用BallonsTranslator快速完成漫画翻译:AI辅助工具的完整指南
  • 打破 “论文焦虑” 怪圈:Paperxie 如何让本科毕业论文写作告别 “从零硬扛”
  • 为Claude Code寻找稳定替代方案,Taotoken接入配置指南
  • B站成分检测器:3分钟快速安装指南,智能识别评论区用户真实身份
  • 仅限高校心理实验室内部流通的NotebookLM提示词矩阵(含DSM-5v3.1结构化解析指令集)
  • 在线提取视频音频妙招,不用安装软件即刻可用 - 爱上科技热点
  • 你以为 PLC 只能控制传送带?我用西门子 1200 做了个打地鼠小游戏!
  • 【C++】--- 类和对象(上)
  • 车载以太网测试实战:从CAN到TSN的范式转移与工程实践