Re:Linux系统篇(十二)工具篇 · 四:make与Makefile:高效管理 C++ 工程项目构建


◆ 博主名称: 晓此方-CSDN博客 大家好,欢迎来到晓此方的博客。
⭐️Linux系列个人专栏: 【主题曲】Linux
⭐️ Re系列专栏:我们思考 (Rethink) · 我们重建 (Rebuild) · 我们记录 (Record)


文章目录


概要&序論

Hello 大家好,我是此方。 > 继上一篇对 g++ 编译指令的讲解,直接切入实战,重点解析 makefile 的编写规范与应用技巧。我们将从最基础的显式规则出发,逐步引入隐式模式与变量自动化,带你从头构建一套通用、可靠的自动化编译模板,提升 Linux 下的开发效率。 Well------正式开始。

一、背景与基本概念

1.1什么是make什么是makefile

先说结论,make是一条命令,makefile是一个文件,两个搭配使用,完成项目自动化构建。

1.1.1makefile是一个规范文件

一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中 ,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作

1.1.2make是一个命令工具

make是一个命令工具,是一个解释makefile中指令的命令工具 ,一般来说,大多数的IDE都有这个命令,比如:Delphi的make,Visual C++的nmake,Linux下GNU的make。可见,makefile都成为了一种在工程方面的编译方法。

1.2它们的意义

  • 会不会写makefile,从一个侧面说明了一个人是否具备完成大型工程的能力

  • makefile带来的好处就是------"自动化编译",一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。

二. 了解Make和Makefile

2.1基本语法

2.1.1整体结构

Makefile 的核心规则由 目标依赖命令 组成:

bash 复制代码
target: prerequisites
	command
  • 目标:通常是要生成的执行文件名或伪目标(别急,我们后面讲)。
  • 依赖:生成该目标所需的文件(源码或中间文件)。
  • 命令 :实际执行的编译指令。注意:命令前必须以 Tab 键开头。

2.2.2依赖关系与依赖方法

我找个例子解释给大家听,

bash 复制代码
myproc:myproc.c
	gcc -o myproc myproc.c
.PHONY:clean
clean:
	rm -f myproc
1. 依赖关系

在代码 myproc:myproc.c 中,冒号左边是结果 ,右边是原料

  • 本质 :它回答了"你是谁的儿子 "或者"你从哪里来"的问题。
  • 举例 :就像你对你爸说:"爸,我是你儿子"。这确立了一个合法的血缘/逻辑关系。
  • 错误示例 :如果你对着你叔叔说"爸,我是你儿子",这就是错误的依赖关系 。在 Makefile 中,如果你明明需要 main.c 却写成了 test.c,编译器就找不到正确的源头。
2. 依赖方法

在代码 gcc -o myproc myproc.c 中,这是具体的执行动作。

  • 本质 :它回答了"要怎么做"的问题。光有关系是不够的,还得有具体的行动。
  • 举例 :确立了父子关系后,你对你爸说:"爸,请给我打钱 "。"打钱"就是实现你生存目标的具体方法
  • 错误示例:你对你爸说"爸,请帮我考试"。虽然关系是对的,但这个方法(要求)是不合理的,现实中行不通,Makefile 里的命令写错了(比如语法错误)也会导致失败。

2.2.3项目清理

  • 工程是需要被清理的
  • 像 clean 这种,没有被第一个目标(你的Makefile写的第一个依赖)直接或间接关联,那么它后面所定义的命令将不会被自动执行,不过,我们可以显示要 make 执行。即命令------"make clean",以此来清除所有的目标文件,以便重编译。
  • 但是一般我们这种 clean 的目标文件,我们将它设置为伪目标 ,用 .PHONY 修饰,伪目标的特性是,总是被执行的。(后面讲)

2.2优先级问题

当你在终端输入 make 且不带任何参数时,make 解释器会按照以下顺序在当前目录下匹配文件。匹配到高优先级文件后将忽略后续文件。

优先级 文件名 推荐程度 使用场景建议
1 GNUmakefile 不推荐 仅在包含 GNU Make 特有扩展语法且不考虑跨平台时使用。
2 makefile 一般 较少见,通常是个人习惯或旧项目遗留。
3 Makefile 强烈推荐 工程实践标准。首字母大写使其在文件列表中置顶,易于识别。

2.3 执行原理:为什么有时候 make 会失败?

当我们执行 make 命令时,它并不是盲目运行的,而是有一套逻辑。

执行make命令直接走第一条,执行make xxx执行对应的一条

2.3.1 扫描策略

  • 默认目标make 会从 Makefile 的第一行开始扫描,默认执行遇到的第一个目标文件。
  • 递归查找(入栈与出栈) :如果第一个目标的依赖项不存在,make 会向下搜索有没有生成该依赖项的规则。这就像一个入栈 的过程,直到找到最底层的源文件,再层层向上返回(出栈 )完成编译。这个过程中的步骤链接起来我们称之为------依赖链
  • 如果你人为的将这个调用链的一段删除掉,
    makefile类似有一种缺省操作。任然可以执行。

  • 如果你认为的将这个调用链的一段修改成错误的。
    makefile则无法执行

2.3.2 为什么重复 make 会报错 "up to date"?

你可能会发现,如果不修改代码,连续输入两次 make,第二次会提示:make: 'myproc' is up to date.。这是因为 make 非常"聪明",它会自动对比目标文件源文件的新旧。

2.3.2.1关键机制:Modify 时间

我们可以通过 stat [文件名] 指令查看文件的三个重要属性:

  • Access :最后一次查看文件内容的时间。
  • Modify :最后一次修改文件内容的时间。
  • Change :最后一次修改文件属性的时间(如权限、所有者)。

判定标准

  • 如果 源文件.c 的 Modify 时间 < 目标文件.bin 的 Modify 时间 → \rightarrow → 代码没改,不执行编译。
  • 如果 源文件.c 的 Modify 时间 > 目标文件.bin 的 Modify 时间 → \rightarrow → 代码更新了,重新执行 编译。

    每一次修改都是重置.c的时间, 到可执行程序前面的时候就知道, 需要修改

这个时候,我们发现,以前讲的touch是不是就有了新的用途了?它可以让我们重新编译某些文件。

2.3.2.2修改时间的细节(了解)

修改Modify时间和change时间:

修改access时间:

因为查看文件在所有文件操作中占比最大,如果每查看一次时间就修改一次:会产生大量的隐形成本。于是可能修改n次后这个access时间才可能修改一次。

2.4 伪目标:.PHONY 的作用

2.4.1什么是伪目标

Makefile 中,我们经常会看到 .PHONY 这个关键字。

bash 复制代码
.PHONY: clean
clean:
	rm -f myproc
  • 什么是伪目标? 伪目标不代表一个真实生成的文件。
  • 核心作用总是被执行
  • 为什么需要? 即使目录下恰好有一个叫 clean 的文件,make clean 依然会强制执行删除命令,而不会因为时间戳对比而失效。

2.4.2为什么不建议给编译生成可执行程序设置伪目标

简单来说:伪目标会破坏 make 的核心价值------"按需编译"。一个项目中有1000个文件,我只修改了一两个,理论上我没有必要对所有的文件都重新编译

  • 非伪目标(正常情况)make 会对比 Modify 时间。如果源文件没改,make 就不执行编译。这在大型项目中能节省海量的编译时间。
  • 伪目标 :一旦声明为 .PHONYmake不再检查时间戳 。无论你的代码改没改,每次输入 make 都会重新编译一遍。对于只有几个文件的小作业没感觉,但对于有成千上万个文件的工程,这简直是灾难。

2.5 进阶:变量自动化与通用模板

在编写大型工程的 Makefile 时,如果每次增加源文件都要手动修改规则,那太痛苦了。为此,我们需要引入变量和自动化符号。

2.5.1 变量与特殊符号基础

1. 定义与引用变量
  • 定义变量 :通常使用 变量名=值 的形式。 注意 :在 Makefile 中,:== 效果基本一致,但在处理复杂引用时有细微差别。建议初学者统一使用 =
  • 引用变量 :使用 $(变量名) 来取值。这就像 C 语言里的宏替换,修改一处,全局生效。
    • 好处:只需修改顶部的变量定义(如可执行程序名),后续所有的调用都会自动跟着修改。
2. 特殊符号:隐藏回显(@)
  • 在 Makefile 的命令前加上 @ 符号,执行时终端不会回显这条原始指令,只会输出命令运行的结果。
  • 这样可以让你的编译输出界面更干净。
3. 自动化变量

这是 Makefile 最精妙的地方,能让你少写很多重复代码:

  • $@ :代表规则中的目标文件(冒号左边的东西)。
  • $^ :代表规则中所有的依赖文件列表(冒号右边所有的东西)。
bash 复制代码
# 例子:
$(BIN):$(SRC)
	$(CC) $(FLAGS) $@ $^  
# 这里 $@ 自动替换为 $(BIN) 的值,$^ 自动替换为 $(SRC) 的值
4. 高级自动识别:wildcard 与模式替换

如果项目里有 100 个 .c 文件,手动写变量名会累死。这时候我们需要两个"自动扫货"的神器:

  • wildcard (自动扫货)
    • 写法SRC=$(wildcard *.c)
    • 作用 :它会把当前目录下所有的 .c 文件名全部抓取出来,存进 SRC 变量里。
  • 模式替换 (批量改名)
    • 写法OBJ=$(SRC:.c=.o)
    • 作用 :它会把 SRC 变量里所有以 .c 结尾的文件名,统统替换成 .o。这样我们就自动得到了目标文件列表。
5. 模式规则:%.o:%.c

有了文件列表,我们还需要告诉 make 怎么把每一个 .c 生成 .o

  • 写法

    bash 复制代码
    %.o:%.c
    	$(CC) $(CFLAGS) $<
  • 含义% 是通配符。这句话的意思是:所有的 .o 文件,都依赖于同名的 .c 文件

  • 新变量 $<

    • $< :代表依赖文件列表中的第一个
    • 为什么用它? 因为编译单个 .o 时,我们只需要那一个对应的 .c,用 $< 最精准。
6.include包含内容
  • 功能:类似于 C 语言的 #include,它会把指定文件的内容原地展开到当前 Makefile 中。
  • 用途:实现模块化管理。你可以把公共的变量定义或复杂的规则提取到独立文件中,让主 Makefile 保持简洁。
bash 复制代码
# 1. 瞬间将 config.mk 里的变量(如 CC=gcc) 拷贝到这里
include config.mk

target: main.c
	$(CC) $(CFLAGS) main.c -o target

clean:
	rm -f target

2.5.4 一个超级全的Makefile参考

bash 复制代码
BIN=proc.exe      # 定义生成的目标程序名,'=='和'='效果一致,但是建议使用'=',具体原因不讲,我放在加餐
CC=gcc            # 定义编译器
#SRC=$(shell ls *.c)  # 采⽤shell命令⾏⽅式,获取当前所有.c⽂件名
SRC=$(wildcard *.c)          # 自动获取当前目录下所有 .c 文件 
OBJ=$(SRC:.c=.o)  # 将所有的 .c 结尾替换为 .o
LFLAGS=-o         # 链接选项
CFLAGS=-c         # 编译选项

# 生成可执行程序
$(BIN):$(OBJ)  
	@$(CC) $(LFLAGS) $@ $^
	@echo "linking ... $^ to $@"

# 模式规则:把所有的 .c 编译成 .o
%.o:%.c
	@$(CC) $(CFLAGS) $<
	@echo "compling ... $< to $@"

.PHONY:clean
clean:
	rm -f $(OBJ) $(BIN)
	@echo "clean project ... done"

.PHONY:test
test:
	@echo "Source files: $(SRC)"
	@echo "Object files: $(OBJ)"

2.5.5最后重新理一下make的完整工作过程

make 是如何工作的, 在默认的方式下, 也就是我们只输入 make 命令。那么:

  1. make 会在当前目录下找名字叫 "Makefile" 或 "makefile" 的文件。
  2. 如果找到, 它会找文件中的第一个目标文件 (target), 在上面的例子中, 他会找到 myproc 这个文件, 并把这个文件作为最终的目标文件。
  3. 如果 myproc 文件不存在, 或是 myproc 所依赖的后面的 myproc.o 文件的文件修改时间要比 myproc 这个文件新 (可以用 touch 测试), 那么, 他就会执行后面所定义的命令来生成 myproc 这个文件。
  4. 如果 myproc 所依赖的 myproc.o 文件不存在, 那么 make 会在当前文件中找目标为 myproc.o 文件的依赖性, 如果找到则再根据那一个规则生成 myproc.o 文件。(这有点像一个堆栈的过程)
  5. 当然, 你的 C 文件和 H 文件是存在的啦, 于是 make 会生成 myproc.o 文件, 然后再用 myproc.o 文件声明 make 的终极任务, 也就是执行文件 hello 了。
  6. 这就是整个 make 的依赖性, make 会一层又一层地去找文件的依赖关系, 直到最终编译出第一个目标文件。
  7. 在找寻的过程中, 如果出现错误, 比如最后被依赖的文件找不到, 那么 make 就会直接退出, 并报错, 而对于所定义的命令的错误, 或是编译不成功, make 根本不理。
  8. make 只管文件的依赖性, 即, 如果在我找了依赖关系之后, 冒号后面的文件还是不在, 那么对不起, 我就不工作啦。

好的本期内容就到这里,如果对你有帮助,还不要忘记点赞三联支持。我是此方,我们下期再见。bye!

相关推荐
隔窗听雨眠1 小时前
读懂AI自动化的两种范式
运维·人工智能·自动化
老陈聊架构1 小时前
『DevOps运维』从零搭建企业微信告警机器人:接口对接、消息模板与自动化通知
运维·企业微信·devops·消息·群机器人
枳实-叶1 小时前
【Linux驱动开发】第7天:总线-设备-驱动三大核心模型:通俗讲解+完整流程图
linux·驱动开发·流程图
BS_Li1 小时前
【Linux网络编程】应用层自定义协议与序列化
linux·服务器·网络
手揽回忆怎么睡1 小时前
京东云Ubuntu22..04安装jdk21、MySQL8、nginx
运维·nginx·京东云
泓博1 小时前
docker ubuntu源码安装openclaw的常见问题
java·linux·开发语言·ai
小此方1 小时前
Re:Linux系统篇(十一)工具篇 · 三:三分钟学会gcc/g++编译工具&&初步认识动静态库&&重温编译基本原理
linux·运维·服务器·开发工具
小吴伴学者1 小时前
Linux TX报文处理流程解析
linux
云小逸1 小时前
【Codex 使用教程:从项目规则、Skills、Rules 到 Hooks】
c++·人工智能·ai·codex