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

⭐️在这个怀疑的年代,我们依然需要信仰。

个人主页:YYYing.

⭐️Linux/C++进阶 系列专栏:【从零开始的linux/c++进阶编程】

⭐️ 其他专栏:【linux基础】【数据结构与算法】【从零开始的计算机网络学习】

系列上期内容:【Linux/C++进阶篇 (一)】man手册、gdb调试、静态库与动态库

系列下期内容:暂无


目录

前言:

Makefile

一、什么是Makefile

[🎯 直接答案](#🎯 直接答案)

[🧠 从底层原理理解](#🧠 从底层原理理解)

[🔍 计算机视角](#🔍 计算机视角)

Makefile的核心机制:依赖关系检查和增量编译。

二、什么是make

[🎯 直接答案](#🎯 直接答案)

三、Makefile的必要性

[🎯 直接答案](#🎯 直接答案)

四、Makefile的简单入门

[🎯 准备程序](#🎯 准备程序)

🔍实际效果

五、Makefile的语法规则

[🎯 规则(Rules)](#🎯 规则(Rules))

1.1、规则是Makefile的核心,用于定义如何生成目标文件。规则的基本格式如下:

1.2、目标详解

1.3、目标依赖

1.4、命令

[🎯 变量(Variables)](#🎯 变量(Variables))

2.1、变量基础

2.2、变量的分类

2.3、变量的外部传递

[🎯 条件判断(Conditional Directives)](#🎯 条件判断(Conditional Directives))

3.1、关键字

3.2、使用

CMake

一、什么是CMake

[🎯 直接答案](#🎯 直接答案)

二、为什么要使用CMake?

[🎯 直接答案](#🎯 直接答案)

三、CMake的语法规则

四、重要的指令

[1> cmake_minimum_required](#1> cmake_minimum_required)

[2> project](#2> project)

[3> set](#3> set)

[4> add_executable](#4> add_executable)

[5> include_directories](#5> include_directories)

[6> link_directories](#6> link_directories)

[7> add_library](#7> add_library)

[8> add_compile_options](#8> add_compile_options)

[9> target_link_libraries](#9> target_link_libraries)

五、CMake常用变量

[1> CMAKE_C_FLAGS](#1> CMAKE_C_FLAGS)

[2> CMAKE_CXX_FLAGS](#2> CMAKE_CXX_FLAGS)

[3> CMAKE_BUILD_TYPE](#3> CMAKE_BUILD_TYPE)

六、CMake编译工程与代码实战

[🔍 目录结构](#🔍 目录结构)

[🧠 两种构建方式](#🧠 两种构建方式)

[1> 内部构建](#1> 内部构建)

[2> 外部构建](#2> 外部构建)

[🎯 代码实战](#🎯 代码实战)

同一目录下的文件编译

分文件编译

结语

---⭐️封面自取⭐️---



前言:

我们平时在玩游戏的时候通常都会遇到刷材料的需求,但自己刷起来又会感觉非常累,那么此时我们是不是会想着自己写个脚本什么的,那么其实我们CMake/Makefile就类似于我们的游戏脚本,他可以为我们实现自动化构建我们的程序

Makefile

一、什么是Makefile

🎯 直接答案

简单来说:

Makefile就像一个自动化构建脚本它告诉计算机如何编译和链接程序 。你只需要在Makefile中定义好编译规则和依赖关系,然后运行make命令,它就会自动根据文件的修改情况,只编译那些需要重新编译的文件,最终生成可执行文件。


🧠 从底层原理理解

想象你要做一道菜(比如番茄炒蛋):

  • 你需要知道这道菜由哪些材料组成(番茄、鸡蛋、油、盐)

  • 你还需要知道做菜的步骤:先炒鸡蛋,再炒番茄,最后混合

  • 如果某个材料更新了(比如换了新鲜的番茄),那么需要重新执行用到了这个材料的步骤

Makefile就是这样的"菜谱",与上面对应它定义了:

  • 目标(要做的菜)

  • 依赖(需要的材料)

  • 命令(具体的烹饪步骤)


🔍 计算机视角

我们在用编译器编译一个C++项目时:

  • 目标:通常是可执行文件(比如main.exe

  • 依赖:源文件(main.cppadd.cpp)和头文件(add.h

  • 命令:编译器和链接器的命令(g++ -c main.cppg++ 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指令来实现的,没事,我们马上就讲。

可不要小看了这简简单单的两个特性,这两个特性可能在小项目中的表现没有那么亮眼,但要在企业级的大项目中出现了下述问题麻烦可就不小了

  1. 该重新编译的没编译:当头文件被修改,但依赖它的源文件没有重新编译,导致使用了旧的声明。

  2. 不该重新编译的却编译了:没有依赖关系的文件被重新编译,浪费时间。

  3. 链接错误:依赖关系错误导致链接时找不到需要的符号,或者链接了错误的版本

可以发现,靠我们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的代码块就应该如下:

cpp 复制代码
# 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代码:

cpp 复制代码
# 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工具允许用空格,但为了可移植性,应该始终使用制表符。

一个规则中,可以无目标依赖,仅仅是实现某种操作

一个规则中可以没有命令,仅仅描述依赖关系

一个规则中,必须要有一个目标

cpp 复制代码
# 示例:
hello.o:hello.cpp
    g++ hello.cpp -o hello.o

1.2、目标详解

1)默认目标

一个Makefile里面可以有多个目标,一般会选择第一个当做默认目标也就是make默认执行的目标


2)多目标

一个规则中可以有多个目标,多个目标具有相同的生成命令和依赖文件

cpp 复制代码
clean distclean:
    rm hello.[^cpp] hello

3)多规则目标

多个规则可以是同一目标

cpp 复制代码
all:test1
all:test2
test1:
    echo "hello"
test2:
    echo "world"

4)伪目标

其含义: 并不是一个真正的文件名,可以看做是一个标签。无依赖,相比一般文件,不会重新生成、执行。可以无条件执行,相当于对应的指令

cpp 复制代码
.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、使用
cpp 复制代码
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> 注意:指令是大小写无关的,但是参数和变量是大小写相关的

cpp 复制代码
# 定义一个变量名叫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控制语句中,是直接使用变量名的

cpp 复制代码
    if(HELLO) 是正确的
    if(${HELLO}) 是不正确的

4> 语句不以分号结束


四、重要的指令

1> cmake_minimum_required

指定CMake的最小版本支持,一般作为第一条cmake指令

cpp 复制代码
# CMake设置最小支持版本为 2.8
cmake_minimum_required(VERSION 2.8)

2> project

定义工程的名称,并可以指定工程支持的语言

cpp 复制代码
# 指定工程的名称为HELLOWORLD
project(HELLOWORLD CXX)   # 表示工程名为HELLOWORLD  使用的语言为C++

3> set

显式定义变量

cpp 复制代码
# 定义变量 SRC 其值为 sayhello.cpp hello.cpp
set(SRC sayhello.cpp hello.cpp)

4> add_executable

通过依赖生成可执行程

cpp 复制代码
# 编译main.cpp 生成main的可执行程序
add_executable(main main.cpp)

5> include_directories

向工程添加多个特定的头文件搜索路径吗,类似于g++编译指令中的 -I

cpp 复制代码
# 将/usr/lib/mylibfolder 和 ./include添加到工程路径中
include_directories(/usr/lib/mylibfolder ./include)

向工程中添加多个特定的库文件搜索路径,类似于g++编译指令的 -L选项

cpp 复制代码
# 将将/usr/lib/mylibfolder 和 ./lib添加到库文件搜索路径中
link_directories(/usr/lib/mylibfolder ./lib)

7> add_library

生成库文件(包括动态库和静态库)

cpp 复制代码
# 通过SRC 变量中的文件,生成动态库
# 该语句生成的是动态库
add_library(hello SHARED ${SRC})
# 该语句生成的是静态库
add_library(hello STATIC ${SRC})

8> add_compile_options

添加编译参数

cpp 复制代码
# 添加编译参数: -Wall -std=c++11
add_compile_options(-Wall -std=c++11)

为target添加需要链接的共享库,类似于g++编译中的 -l 指令

cpp 复制代码
# 将hello 动态库文件链接到可执行程序main中
target_link_libraries(main hello)

五、CMake常用变量

1> CMAKE_C_FLAGS

gcc编译选项的值

2> CMAKE_CXX_FLAGS

g++编译选项的值

cpp 复制代码
# 在CMAKE_CXX_FLAGS编译选项后追加 -std=c++11
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")

3> CMAKE_BUILD_TYPE

编译类型(Debug 、Release)

cpp 复制代码
# 设定编译类型为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> 内部构建

不推荐使用

内部构建会在主目录下,产生一大堆中间文件,这些中间文件并不是我们最终所需要的,和工程源文件放在一起时,会显得比较杂乱无章

cpp 复制代码
## 内部构建

# 1、在当前目录下,编译主目录中的CMakeLists.txt 文件生成Makefile文件
cmake .     # . 表示当前路径

# 2、执行make命令,生成目标文件
make

2> 外部构建

推荐使用

将编译输出的文件与源文件放到不同的目录下,进行编译,此时,编译生成的中间文件,不会跟工程源文件进行混淆

cpp 复制代码
## 外部构建步骤

# 1、在当前目录下,创建一个 build 文件,用于存储生成中间文件
mkdir build

# 2、进入build文件夹内
cd build

# 3、编译上一级目录中的CMakeLists.txt,生成Makefile文件以及其他文件
cmake ..   # ..表示上一级目录

# 4、执行make命令,生成可执行程序
make

🎯 代码实战

同一目录下的文件编译

不废话,直接上图,保证看的明明白白的。

分文件编译

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


结语

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

我是YYYing,后面还有更精彩的内容,希望各位能多多关注支持一下主包。

**无限进步,**我们下次再见。


---⭐️ 封面自取 ⭐️---

相关推荐
范纹杉想快点毕业2 小时前
嵌入式实时系统架构设计:基于STM32与Zynq的中断、状态机与FIFO架构工程实战指南,基于Kimi设计
c语言·c++·单片机·嵌入式硬件·算法·架构·mfc
玖釉-2 小时前
核心解构:Cluster LOD 与 DAG 架构深度剖析
c++·windows·架构·图形渲染
wdfk_prog2 小时前
[Linux]学习笔记系列 -- [drivers][gpio[[gpiolib]
linux·笔记·学习
砚上有墨2 小时前
问题记录:云平台计算节点内存故障,热迁移失败导致系统重启。
linux·运维·云计算
程序员敲代码吗2 小时前
C++运行库修复指南:解决游戏办公软件报错问题
开发语言·c++·游戏
Diros1g2 小时前
ubuntu多网卡网络配置
网络·ubuntu·php
bloglin999992 小时前
ubuntu系使用root用户登录显示密码错误
linux·运维·ubuntu
孞㐑¥2 小时前
算法—哈希表
开发语言·c++·经验分享·笔记·算法
70asunflower2 小时前
[特殊字符] Flameshot 完全指南:Ubuntu 下的终极截图工具
linux·运维·ubuntu