【Linux/C++进阶篇(二) 】超详解自动化构建 —— 日常开发中的“脚本” :Makefile/CMake

⭐️在这个怀疑的年代,我们依然需要信仰。
个人主页:YYYing.
⭐️Linux/C++进阶系列专栏:【从零开始的linux/c++进阶编程】
⭐️其他专栏:【linux基础】【数据结构与算法】【从零开始的计算机网络学习】
系列上期内容:【Linux/C++进阶篇 (一)】man手册、gdb调试、静态库与动态库
系列下期内容:暂无
目录
前言:
Makefile
一、什么是Makefile
🎯 直接答案
🧠 从底层原理理解
🔍 计算机视角
Makefile的核心机制:依赖关系检查和增量编译。
二、什么是make
🎯 直接答案
三、Makefile的必要性
🎯 直接答案
四、Makefile的简单入门
🎯 准备程序
🔍实际效果
五、Makefile的语法规则
🎯 规则(Rules)
1.1、规则是Makefile的核心,用于定义如何生成目标文件。规则的基本格式如下:
1.2、目标详解
1.3、目标依赖
1.4、命令
🎯 变量(Variables)
2.1、变量基础
2.2、变量的分类
2.3、变量的外部传递
🎯 条件判断(Conditional Directives)
3.1、关键字
3.2、使用
CMake
一、什么是CMake
🎯 直接答案
二、为什么要使用CMake?
🎯 直接答案
三、CMake的语法规则
四、重要的指令
1> cmake_minimum_required
2> project
3> set
4> add_executable
5> include_directories
6> link_directories
7> add_library
8> add_compile_options
9> target_link_libraries
五、CMake常用变量
1> CMAKE_C_FLAGS
2> CMAKE_CXX_FLAGS
3> CMAKE_BUILD_TYPE
六、CMake编译工程与代码实战
🔍 目录结构
🧠 两种构建方式
1> 内部构建
2> 外部构建
🎯 代码实战
同一目录下的文件编译
分文件编译
结语
---⭐️封面自取⭐️---

前言:
我们平时在玩游戏的时候通常都会遇到刷材料的需求,但自己刷起来又会感觉非常累,那么此时我们是不是会想着自己写个脚本什么的,那么其实我们CMake/Makefile就类似于我们的游戏脚本,他可以为我们实现自动化构建我们的程序。
Makefile
一、什么是Makefile
🎯 直接答案
简单来说:
Makefile就像一个自动化构建脚本,它告诉计算机如何编译和链接程序。你只需要在Makefile中定义好编译规则和依赖关系,然后运行make命令,它就会自动根据文件的修改情况,只编译那些需要重新编译的文件,最终生成可执行文件。
🧠 从底层原理理解
想象你要做一道菜(比如番茄炒蛋):
-
你需要知道这道菜由哪些材料组成(番茄、鸡蛋、油、盐)
-
你还需要知道做菜的步骤:先炒鸡蛋,再炒番茄,最后混合
-
如果某个材料更新了(比如换了新鲜的番茄),那么需要重新执行用到了这个材料的步骤
Makefile就是这样的“菜谱”,与上面对应它定义了:
-
目标(要做的菜)
-
依赖(需要的材料)
-
命令(具体的烹饪步骤)
🔍 计算机视角
我们在用编译器编译一个C++项目时:
-
目标:通常是可执行文件(比如
main.exe) -
依赖:源文件(
main.cpp、add.cpp)和头文件(add.h) -
命令:编译器和链接器的命令(
g++ -c main.cpp、g++ main.o add.o -o a.out)
Makefile的核心机制:依赖关系检查和增量编译。
那么什么叫做依赖关系检查呢?
我们想执行一个.cpp文件的时候,是不是得经过四个步骤:预处理 --> 编译 --> 汇编 --> 链接然后最后才能生成我们的可执行程序,那么我们把这四个步骤拆分成文件来看是不是对应着 .cpp -> .i -> .s -> .o 这四种文件。那么我们就可以顺势理解为:可执行程序 —依赖于—> .o文件 —依赖于—> .s文件 —依赖于—> .i文件 —依赖于—> .cpp文件 这一串关系链,那么这就是我们所谓的依赖关系检查,可能看到这,其实我相信大部分人都会觉得这不是一种很常识的常识吗,但其实看我们第二个特性就知道我在强调什么了。
那么什么叫做增量编译呢?
我们以一个小案例来看看什么是增量编译:
第一次构建:编译所有文件
1> 修改add.cpp后构建:只编译add.cpp和重新链接
2> 只修改头文件后构建:编译所有包含该头文件的源文件
3> 什么都没修改后构建:什么都不做那么其实,看到这我相信聪明的你一定能想明白了,也就是我们的依赖关系在此处是大有作为的,有了它,我们就不用傻傻的因为一个文件的改动,而动了所有的文件,我们只会根据我们的依赖项,去针对性的处理我们的中间操作,当然此处的文件时间戳检查实际是由我们的make指令来实现的,没事,我们马上就讲。
可不要小看了这简简单单的两个特性,这两个特性可能在小项目中的表现没有那么亮眼,但要在企业级的大项目中出现了下述问题麻烦可就不小了
该重新编译的没编译:当头文件被修改,但依赖它的源文件没有重新编译,导致使用了旧的声明。
不该重新编译的却编译了:没有依赖关系的文件被重新编译,浪费时间。
链接错误:依赖关系错误导致链接时找不到需要的符号,或者链接了错误的版本
可以发现,靠我们Makefile的这两个特性可以帮我们的程序节省很大一部分时间效率
下图就是我们makefile的执行逻辑:

二、什么是make
🎯 直接答案
make是一个执行Makefile的工具,是一个解释器用来对Makefile中的命令进行解析并执行一个shell指令 make 这个指令在 /usr/bin 中。
默认linux系统中都已经安装 如果没有安装 make,安装指令如下 sudo apt install make(此处为ubuntu,centos将apt改为yum即可)
查看是否安装成功:make --version
简单来说:
make就像一个智能的施工队长,它看着一张施工图(Makefile),知道:
-
要建什么(目标)
-
需要什么材料(依赖)
-
怎么建(命令)
-
哪些部分已经建好了(时间戳比较)
然后它指挥工人们(执行命令)高效地完成建设任务。
这就是我们make与makefile的羁绊展示图。

三、Makefile的必要性
不用怀疑,它就是Linux/Uinx环境下开发的必备技能,系统架构师、项目经理的核心技能,研究开源项目、Linux内核原码的必需品,那其实光看完上述两个小节,我相信大家一定能意识到此技能在我们实际开发中有多么重要,那么我就再次带大家梳理一下。
🎯 直接答案
1. 自动化重复构建步骤,避免手动输入冗长命令
2.智能增量编译,只重新编译修改过的文件,极大提升开发效率
3.管理复杂依赖关系,确保构建的正确性和一致性
4.统一团队构建流程,保证开发环境一致性
我们不妨可以想象一下——我们现在有一个含有100多个文件的项目,如果没有makefile我们将要做什么:
-
每个文件都需要预处理 --> 编译 --> 汇编 --> 链接4个步骤
-
文件之间有复杂的依赖关系
-
需要链接多个库
-
需要特定的编译选项
-
头文件修改需要触发相关文件重编译
我去,你看到这你头不大,那我只能说你是这个👍
可以说这个文件量的项目手动管理可不可能?当然不可能!更何况这100个文件本身光编译一次就需要不少时间
再其次我们每个开发者在自己机器上的开发环境都不一样,诸如此类的问题根本说不完。
四、Makefile的简单入门
那么在对Makefile有一定认知的情况下,我们现在就正式进入到了我们的实战环节。
🎯 准备程序
我们先来准备一下我们的程序:

此时我们要想执行它我们就得在终端进行下述的四种操作:

那么我们Makefile的代码块就应该如下:
# Makefile中的注释是以#开头
# 语法格式——目标:依赖
# 通过依赖生成目标的指令
# 注意:指令前面必须使用同一个tab键隔开,不能使用多个空格顶过来
hello:hello.o
g++ hello.o -o hello
hello.o:hello.s
g++ -c hello.s -o hello.o
hello.s:hello.i
g++ -S hello.i -o hello.s
hello.i:hello.cpp
g++ -E hello.cpp -o hello.i
而且我们此处能够再简化下我们的Makefile代码:
# Makefile中的注释是以#开头
# 语法格式——目标:依赖
# 通过依赖生成目标的指令
# 注意:指令前面必须使用同一个tab键隔开,不能使用多个空格顶过来
hello:hello.o
g++ hello.o -o hello
hello.o:hello.cpp
g++ -c hello.cpp -o hello.o
🔍实际效果

可以看到我们makefile直接帮我们完成了编译步骤,我们只需要运行可执行文件即可
但这里可能会有人会问?为什么此处最先打印出来的信息是 g++ -c hello.cpp -o hello.o 这一条指令而 g++ hello.o -o hello 发而在后面,没错事实上就是这样。
因为make命令内部维护了一个栈结构,正是因为栈是先进后出的,所以就有了这种情况:文件还不存在——入栈,找到文件——出栈。
五、Makefile的语法规则
🎯 规则(Rules)
1.1、规则是Makefile的核心,用于定义如何生成目标文件。规则的基本格式如下:
目标(target): 依赖(prerequisites)
命令(commands)
注意:
命令前面必须是一个制表符(tab),不能是空格。虽然有些make工具允许用空格,但为了可移植性,应该始终使用制表符。
一个规则中,可以无目标依赖,仅仅是实现某种操作
一个规则中可以没有命令,仅仅描述依赖关系
一个规则中,必须要有一个目标
# 示例:
hello.o:hello.cpp
g++ hello.cpp -o hello.o

1.2、目标详解
1)默认目标
一个Makefile里面可以有多个目标,一般会选择第一个当做默认目标也就是make默认执行的目标
2)多目标
一个规则中可以有多个目标,多个目标具有相同的生成命令和依赖文件
clean distclean: rm hello.[^cpp] hello
3)多规则目标
多个规则可以是同一目标
all:test1 all:test2 test1: echo "hello" test2: echo "world"
4)伪目标
其含义:并不是一个真正的文件名,可以看做是一个标签。无依赖,相比一般文件,不会重新生成、执行。可以无条件执行,相当于对应的指令
.PHONY: clean #设置clean为伪目标 clean: rm hello.[^cpp] hello
我们此处的clean就是在清理我们的项目,他不会跟我们的目标文件直接产生关联,也就是说目标文件执行时,他是不会动的,如果要用我们需要直接进行make clean指定目标。
但如果有人试过你就会发现,不加这个.PHONY:直接写个clean也是能直接运行的,但为什么此处要加上这个东西呢?那是为了将这个clean与同名文件区分开来,什么意思呢,如果我们该目录下有个名为clean的文件,那么我们此时再运行clean目标对应的指令将会失效,如下图:

那么这个时候,我们就需要让这个clean变为伪目标,也就是让其变为一种“标签”,这样即使有同名文件,那么我们的make也不会将clean看作是一个生成文件了。

1.3、目标依赖
1)文件时间戳
make每次运行都会根据时间戳来判断目标依赖是否要进行更新:
1> 所有文件都更改过,则对所有文件进行编译,生成可执行程序
2> 在上次make之后修改过的cpp文件,会被重新编译
3> 在上次make只写修改过的头文件,依赖该头文件的目标依赖也会重新编译
2)模式匹配
% ———> 通配符匹配
$@ ———> 目标
$^ ———> 依赖
$< ———> 第一个依赖
* ———> 普通通配符
注意:%是Makefile中的规则通配符,*是普通通配符
下面用一张图带大家练习练习:

可以看到我们右上角的Makefile文件,我敢肯定初学者看到这个图片时肯定会觉得看起来非常难受的,事实确实是这种写法的可读性很差,但我们的耦合性就大大增强了,也就是当我们想改上面的东西,那么我就只用对目标与目标依赖进行更改,而不用对命令进行更改,那有人可能会问:我真的懒得不行,那这不main.o swap.o两个目标依赖不我还得手改吗,那这俩能不能也做成个什么通用的标志,让我再轻松一下,有的兄弟有的,就是我们后面要讲的变量
1.4、命令
1)命令的组成
由shell命令组成,以tab键开头
2) 命令的执行
每条命令,make会开一个进程
每条命令执行完,make会检测这个命令的返回码
如果返回成功,则继续执行后面的命令
如果返回失败,则make会终止当前执行的规则并退出
3)并发执行命令
make -j4 ----->表示开辟4个线程执行
time make ----->执行make时,显示当前时间
🎯 变量(Variables)
2.1、变量基础
1)变量定义:变量名 = 变量值
2)变量的赋值:
追加赋值:+= --->在原有的基础上追加相关内容
条件赋值:?= --->如果之前没有值,则为变量赋值,如果之前有值,则不进行赋值
3)变量的使用:$(变量名)或者${变量名} (通常前者我们用的多些)
2.2、变量的分类
1)立即展开变量
使用:=操作符进行赋值
在解析阶段直接赋值常量字符串
2)延迟展开变量
使用=操作符进行赋值
将最后一次赋值的结果给变量名使用
3)注意事项
一般在目标、目标依赖中使用立即展开赋值
在命令中一般使用延迟展开赋值变量
2.3、变量的外部传递
我们可以通过命令行给变量进行赋值操作

🎯 条件判断(Conditional Directives)
3.1、关键字
ifeq 、else 、endif
ifneq 、else 、endif
其实不难看出,上下两对刚好相反,上面意为是否相等,那么下面就意为是否不相等。
3.2、使用
ifeq (要判断的量, 判断的值)
Makefile语句
else
Makefile语句
endif
注意:
条件语句从ifeq开始执行,括号与关键字自减使用空格隔开
括号里面挨着括号处,不允许加空格
ifeq后的空格也是一定要有的

CMake
一、什么是CMake
🎯 直接答案
CMake是一个跨平台的安装编译工具,可以使用简单的语句来描述所有平台的安装(编译过 程)它可以使用几行或者几十行的代码来完成非常冗长的Makefile代码
简单来说:
CMake就像一个"翻译官":
-
你用CMake的"语言"写一份菜谱(CMakeLists.txt)
-
CMake根据客人(Windows/Mac/Linux)的口味,翻译成具体食谱(Makefile/VS Project/Xcode Project)
-
客人按自己的食谱做菜(构建项目)
二、为什么要使用CMake?
🎯 直接答案
光使用Makefile在实际开发中会遇到很多问题,例如下:
当时开发者的痛苦:
# 项目要在Windows、Linux、macOS上都能构建
# Windows开发者:我需要Visual Studio的.sln文件
# Linux开发者:我需要Makefile
# macOS开发者:我需要Xcode的.xcodeproj
那么我们现在有两种解决方案的提供:
# 解决方案1:维护三套构建配置 ❌
# 解决方案2:写一个工具,生成所有平台的构建文件 ✅

三、CMake的语法规则
1> 基本语法:指令(参数1 参数2 ...)
参数使用括号括起来
参数之间使用空格或分号隔开
2> 注意:指令是大小写无关的,但是参数和变量是大小写相关的
# 定义一个变量名叫HELLO 变量的值为hello.cpp
set(HELLO hello.cpp)
# 通过main.cpp 和hello.cpp 编译生成 hello可执行程序
add_executable(hello main.cpp hello.cpp)
# 作用同上
ADD_EXECUTABLE(hello main.cpp ${HELLO})
3> 变量使用${}进行取值,但是在if控制语句中,是直接使用变量名的
if(HELLO) 是正确的
if(${HELLO}) 是不正确的
4> 语句不以分号结束
四、重要的指令
1> cmake_minimum_required
指定CMake的最小版本支持,一般作为第一条cmake指令
# CMake设置最小支持版本为 2.8
cmake_minimum_required(VERSION 2.8)
2> project
定义工程的名称,并可以指定工程支持的语言
# 指定工程的名称为HELLOWORLD
project(HELLOWORLD CXX) # 表示工程名为HELLOWORLD 使用的语言为C++
3> set
显式定义变量
# 定义变量 SRC 其值为 sayhello.cpp hello.cpp
set(SRC sayhello.cpp hello.cpp)
4> add_executable
通过依赖生成可执行程
# 编译main.cpp 生成main的可执行程序
add_executable(main main.cpp)
5> include_directories
向工程添加多个特定的头文件搜索路径吗,类似于g++编译指令中的 -I
# 将/usr/lib/mylibfolder 和 ./include添加到工程路径中
include_directories(/usr/lib/mylibfolder ./include)
6> link_directories
向工程中添加多个特定的库文件搜索路径,类似于g++编译指令的 -L选项
# 将将/usr/lib/mylibfolder 和 ./lib添加到库文件搜索路径中
link_directories(/usr/lib/mylibfolder ./lib)
7> add_library
生成库文件(包括动态库和静态库)
# 通过SRC 变量中的文件,生成动态库
# 该语句生成的是动态库
add_library(hello SHARED ${SRC})
# 该语句生成的是静态库
add_library(hello STATIC ${SRC})
8> add_compile_options
添加编译参数
# 添加编译参数: -Wall -std=c++11
add_compile_options(-Wall -std=c++11)
9> target_link_libraries
为target添加需要链接的共享库,类似于g++编译中的 -l 指令
# 将hello 动态库文件链接到可执行程序main中
target_link_libraries(main hello)
五、CMake常用变量
1> CMAKE_C_FLAGS
gcc编译选项的值
2> CMAKE_CXX_FLAGS
g++编译选项的值
# 在CMAKE_CXX_FLAGS编译选项后追加 -std=c++11
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
3> CMAKE_BUILD_TYPE
编译类型(Debug 、Release)
# 设定编译类型为Debug,调试时需要选择该模式
set(CMAKE_BUILD_TYPE Debug)
# 设定编译类型为Release,发布需要选择该模式
set(CMAKE_BUILD_TYPE Release)
六、CMake编译工程与代码实战
🔍 目录结构
CMake目录结构:项目主目录中会放一个CMakeLists.txt的文本文档,后期使用cmake指令时,依赖的就是该文档
1> 包含源文件的子文件夹中包含CMakeLists.txt文件时,主目录的CMakeLists.txt要通过add_subdirector添加子目录
2> 包含源文件的子文件夹中不包含CMakeLists.txt文件时,子目录编译规则,体现在主目录中的CMakeLists.txt
🧠 两种构建方式
1> 内部构建
不推荐使用
内部构建会在主目录下,产生一大堆中间文件,这些中间文件并不是我们最终所需要的,和工程源文件放在一起时,会显得比较杂乱无章
## 内部构建
# 1、在当前目录下,编译主目录中的CMakeLists.txt 文件生成Makefile文件
cmake . # . 表示当前路径
# 2、执行make命令,生成目标文件
make
2> 外部构建
推荐使用
将编译输出的文件与源文件放到不同的目录下,进行编译,此时,编译生成的中间文件,不会跟工程源文件进行混淆
## 外部构建步骤
# 1、在当前目录下,创建一个 build 文件,用于存储生成中间文件
mkdir build
# 2、进入build文件夹内
cd build
# 3、编译上一级目录中的CMakeLists.txt,生成Makefile文件以及其他文件
cmake .. # ..表示上一级目录
# 4、执行make命令,生成可执行程序
make
🎯 代码实战
同一目录下的文件编译
不废话,直接上图,保证看的明明白白的。

分文件编译
依旧直接上图,此处用的代码与我们刚makefile演示的swap一样。但值得一提的是,多文件编译更依赖于我们的CMake/Makefile,因为在vscode多文件编译时,我们的头文件是无法直接被读取的,必须要在同一目录下才能读取到,但这样就与我们多文件编译冲突了。

结语
那么关于Makefile与CMake的讲解就到这里了。
我是YYYing,后面还有更精彩的内容,希望各位能多多关注支持一下主包。
无限进步,我们下次再见。
---⭐️封面自取⭐️---










