Linux/C++进阶:Makefile 与 CMake 自动化构建详解
Makefile 是 Linux 环境下用于自动化构建程序的脚本工具,通过定义目标、依赖和命令实现增量编译,提升开发效率。CMake 则是跨平台的构建系统生成器,能根据源码生成不同平台的 Makefile 或项目文件。 Makefile 的核心机制、语法规则及变量使用,并讲解了 CMake 的基本指令、常用变量及工程构建方式,帮助开发者掌握 C++ 项目的自动化构建流程。

Makefile 是 Linux 环境下用于自动化构建程序的脚本工具,通过定义目标、依赖和命令实现增量编译,提升开发效率。CMake 则是跨平台的构建系统生成器,能根据源码生成不同平台的 Makefile 或项目文件。 Makefile 的核心机制、语法规则及变量使用,并讲解了 CMake 的基本指令、常用变量及工程构建方式,帮助开发者掌握 C++ 项目的自动化构建流程。

简单来说,Makefile 就像一个自动化构建脚本,它告诉计算机如何编译和链接程序。你只需要在 Makefile 中定义好编译规则和依赖关系,然后运行 make 命令,它就会自动根据文件的修改情况,只编译那些需要重新编译的文件,最终生成可执行文件。
想象你要做一道菜(比如番茄炒蛋):
Makefile 就是这样的'菜谱',与上面对应它定义了:
我们在用编译器编译一个 C++ 项目时:
main)main.cpp、add.cpp)和头文件(add.h)g++ -c main.cpp、g++ main.o add.o -o a.out)我们想执行一个.cpp 文件的时候,需要经过四个步骤:预处理 --> 编译 --> 汇编 --> 链接,然后最后才能生成我们的可执行程序。把这四个步骤拆分成文件来看对应着 .cpp -> .i -> .s -> .o 这四种文件。我们可以理解为:可执行程序依赖于.o 文件,.o 文件依赖于.s 文件,.s 文件依赖于.i 文件,.i 文件依赖于.cpp 文件。这一串关系链就是所谓的依赖关系检查。
以一个小案例来看看什么是增量编译:
有了依赖关系,我们就不用因为一个文件的改动而重编所有文件,只会根据依赖项去针对性处理中间操作。此处的文件时间戳检查实际是由 make 指令来实现的。
这两个特性可以帮我们的程序节省很大一部分时间效率。下图就是我们 makefile 的执行逻辑:

make 是一个执行 Makefile 的工具,是一个解释器用来对 Makefile 中的命令进行解析并执行一个 shell 指令。make 这个指令在 /usr/bin 中。
默认 linux 系统中都已经安装。如果没有安装 make,安装指令如下 sudo apt install make(此处为 ubuntu,centos 将 apt 改为 yum 即可)。
查看是否安装成功:make --version
简单来说,make 就像一个智能的施工队长,它看着一张施工图(Makefile),知道:
然后它指挥工人们(执行命令)高效地完成建设任务。

不用怀疑,它就是 Linux/Unix 环境下开发的必备技能,系统架构师、项目经理的核心技能,研究开源项目、Linux 内核源码的必需品。
我们不妨可以想象一下——我们现在有一个含有 100 多个文件的项目,如果没有 makefile 我们将要做什么:
可以说这个文件量的项目手动管理是不可能的!更何况这 100 个文件本身光编译一次就需要不少时间。
再其次我们每个开发者在自己机器上的开发环境都不一样,诸如此类的问题根本说不完。
我们先来准备一下我们的程序:

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

那么我们 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 命令内部维护了一个栈结构,正是因为栈是先进后出的,所以就有了这种情况:文件还不存在——入栈,找到文件——出栈。
目标(target): 依赖(prerequisites) 命令(commands)
注意:
# 示例:
hello.o:hello.cpp
g++ hello.cpp -o hello.o

我们此处的 clean 就是在清理我们的项目,他不会跟我们的目标文件直接产生关联,也就是说目标文件执行时,他是不会动的,如果要用我们需要直接进行 make clean 指定目标。
但如果有人试过你就会发现,不加这个 .PHONY: 直接写个 clean 也是能直接运行的,但为什么此处要加上这个东西呢?那是为了将这个 clean 与同名文件区分开来。什么意思呢,如果我们该目录下有个名为 clean 的文件,那么我们此时再运行 clean 目标对应的指令将会失效。

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

文件时间戳:make 每次运行都会根据时间戳来判断目标依赖是否要进行更新:
模式匹配:
% ——> 通配符匹配$@ ——> 目标$^ ——> 依赖$< ——> 第一个依赖* ——> 普通通配符注意:%是 Makefile 中的规则通配符,*是普通通配符。
下面用一张图带大家练习练习:

可以看到我们右上角的 Makefile 文件,我敢肯定初学者看到这个图片时肯定会觉得看起来非常难受的,事实确实是这种写法的可读性很差,但我们的耦合性就大大增强了,也就是当我们想改上面的东西,那么我就只用对目标与目标依赖进行更改,而不用对命令进行更改。
那有人可能会问:我真的懒得不行,那这不 main.o swap.o 两个目标依赖不我还得手改吗,那这俩能不能也做成个什么通用的标志,让我再轻松一下,有的兄弟有的,就是我们后面要讲的变量。
make -j4 ----->表示开辟 4 个线程执行。time make ----->执行 make 时,显示当前时间。+= --->在原有的基础上追加相关内容?= --->如果之前没有值,则为变量赋值,如果之前有值,则不进行赋值$(变量名)或者${变量名}(通常前者我们用的多些):= 操作符进行赋值。在解析阶段直接赋值常量字符串。= 操作符进行赋值。将最后一次赋值的结果给变量名使用。我们可以通过命令行给变量进行赋值操作。

ifeq、else、endififneq、else、endif其实不难看出,上下两对刚好相反,上面意为是否相等,那么下面就意为是否不相等。
ifeq (要判断的量,判断的值)
Makefile 语句
else
Makefile 语句
endif
注意:

CMake 是一个跨平台的安装编译工具,可以使用简单的语句来描述所有平台的安装(编译过程)。它可以使用几行或者几十行的代码来完成非常冗长的 Makefile 代码。
简单来说,CMake 就像一个'翻译官':
光使用 Makefile 在实际开发中会遇到很多问题,例如下:
当时开发者的痛苦:
那么我们现在有两种解决方案的提供:

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

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

那么关于 Makefile 与 CMake 的讲解就到这里了。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online