1.cmake的概述
CMake 是一个跨平台的构建系统生成器,不直接编译代码,而是根据简洁的 CMakeLists.txt`描述文件,为不同平台生成原生的构建文件(如 Unix 的 Makefile、Windows 的 Visual Studio 解决方案、Ninja 文件等),再由这些原生工具完成编译。
CMake 的主要优点:
- 跨平台
同一份 CMakeLists.txt,可在 Linux、macOS、Windows、嵌入式等平台上生成对应的构建系统,几乎无需改动。
- 不牺牲构建速度
它生成的是高效的原生构建文件(如 Ninja),直接享受原生的并行与增量编译,构建速度不拖后腿。
- 现代、清晰的构建逻辑
提倡基于目标(target)的"现代 CMake"风格,使用 target_link_libraries 等自动传播编译要求,避免全局污染,让大型项目结构清晰、易于维护。
- 强大的依赖与包管理集成
内置 find_package,支持通过 FetchContent 直接下载源码构建,也能无缝对接 vcpkg、Conan 等包管理器,方便集成第三方库。
- 功能完善的全流程支持
从编译、测试(CTest)、打包(CPack)、安装、导出到交叉编译,CMake 提供一整套解决方案,而不仅是"编译"这一环。
2. cmake的使用
2.1 注释
2.1.1 行注释
CMake使用 " # " 进行行注释
cpp
CMAKE
#这是注释部分
cmake_minimum_require(VERSION 3.0.0)
2.1.2 块注释
用 # \[ ] 进行块注释
cpp
CMAKE
#[[注释
注释
注释]]
cmake_minimum_required(VERSION 3.0.0)
2.2 非常常用的cmake命令
2.2.1 cmake_minumum_required
作用:指定使用的cmake的最低版本。
该命令作为可选选项,不是必须添加的命令,但是如果不加可能会有警告。可以看到上面的命令部分展示出来的就是本命令的用法。
2.2.2 project
作用:定义工程名称,并可指定工程的版本、工程描述、web主页地址、支持的语言,如果不需要都可以忽略只需要指定出工程名字即可
cpp
CMAKE
cmake_minumum_required(VERSION 3.0.0)
project(name)
name的地方是填你的项目名字不是真的填 "name" 。
2.2.3 add_executable
作用:定义工程会生成一个可执行程序,add_executable(可执行程序名 源文件名称)
源文件可以是一个也可以是多个,如有多个可以用空格或者 ;间隔
执行Cmake命令,将CMakeLists.txt文件编辑好之后,就可以执行 cmake 命令了
cpp
CMAKE
cmake_minumum_required(VERSION 3.0.0)
project(name)
add_executable(exe_name file1_name.cpp file2_name.cpp)
add_executable(exe_name;file1_name.cpp;file2_name.cpp)
该命令的编写如上所示,第一个写要生成的可执行程序的名字,后面用空格或者 ;隔开每个要进行编译的源文件。
2.2.4 cmake
cmake CMakeLists.txt文件所在路径
执行完cmake命令之后再执行make命令就可以运行我们的程序了。
2.3 将执行cmake命令之后会产生的文件放到一个文件夹下
使用 cmake 命令之后,因为其特性会自动生成一系列的文件,这些文件你现在可能不明白有什么用或者是什么意思。但是生成的一系列文件干扰我们对自己编写的源文件的搜索,所以我们可以将这些生成出来的文件放到一个文件夹中。
可以给这些生成的文件创建一个对应的目录,将其放到一个目录里面,通常会命名为build。
这里需要注意的是,上面所说的是先通过cmake生成了一系列的文件,然后再转移进build中。但是如果一开始我们就创建了 build 文件,想直接通过cmake将文件生成在build中,那么此时我们进入的目录就是 build。但是直接使用 cmake命令是不行的,因为cmake后面跟着的是CMakeLists.txt的路径,所以如果你写的是 cmake . 的话是编译不成功的,可以写绝对路径或者 cmake .. 找上级目录的。
2.4 set的多种用法
2.4.1 设置变量
在上面的 add_executable 语句中你可能会发现一个问题,当后面的源文件太多的时候同时这些源文件又要多次的被使用的话,你单靠复制粘贴也是很麻烦的。所以可以将多个源文件的名字组合成一个变量名,这样就方便使用了。
实现这一操作需要用到 set 命令。
cpp
CMAKE
#[]中的参数是可选项,如不需要可以不写
SET(VAR [VALUE] [CACHE TYPE DOCSTRING [FORCE]])
VAR:变量名,VALUE:变量值。
cpp
CMAKE
set(source_files_name1 file1.cpp file2.cpp file3.cpp)
set(source_files_name2 file1.cpp;file2.cpp;file3.cpp)
add_executable(exe_name ${source_files_name1})
在使用 set 设置好的变量名后,要想使用必须通过取值来取得这个名字背后的意义,取值的写法是 ${变量名}。
2.4.2 指定使用的C++标准
编写C++程序时,大概率是会用到不同版本的C++的特性,特别是一些方便好用的新特性。但是又因为兼容老版本的问题,所以一般要使用新特性的话就需要先进行声明。
在命令行里可以直接通过 g++ 来设置,但是也可以通过编写 CMakeLists.txt 文件来设置。
cpp
CMAKE
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD 17)
分别对应 C++11、C++14、C++17。
2.4.3 指定输出的路径
你可能有这样的问题,我不想让生成的可执行程序声称在外面一堆的文件中,那么也可以设置将其输出在某个指定的文件中。
cpp
CMAKE
#示例
set(exe_path 此处为exe将要生成到的文件的绝对路径)
#源码
set(EXECUTABLE_OUTPUT_PATH ${exe_path}/bin)
可以看到第二行编写的 cmake 命令对 exe_path 进行取值之后后面写了一个 bin ,那如果我没有 bin 文件呢?在 cmake 中如果路径中的子目录不存在,那么 cmake 会自动生成一个,所以最后生成的文件就在 exe_path 这个变量名所包含的路径下的由 cmake 创建的 bin 文件夹下。

2.5 搜索文件
上述的set并没有实质性的解决问题,因为将文件名都交给set和将文件名都放在add_executable中是一样的,而且如果是一个非常大的项目的话,把每个文件都列出来真是一件很麻烦的事,所以需要其他方法来改进。
我们肯定希望有一种办法能够直接将全部需要的文件都给添加进来,观察之前的例子可以发现添加的文件的后缀都是 .cpp 。联想到平常的搜索引擎中搜索东西,可以指定某一个目录下将其中后缀为 .cpp 的文件全部搜索出来,这样就符合标题搜索文件了。
搜索文件有两种方法
2.5.1 方法一
cpp
CMAKE
aux_source_directory(dir variable)
dir:要搜索的目录
variable:将 dir 目录下搜索的源文件列表存储到该变量中
cpp
CMAKE
#示例
aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/src SRC_LIST)
add_executable(exe ${SRC_LIST})
2.5.2 方法二
cpp
CMAKE
file(GLOB/GLOB_RECURSE 变量名 要搜索的文件路径和文件类型)
GLOB:将指定目录下搜索到的满足条件的所有文件名生成一个列表,并将其存储到变量中。
GLOB_RECURSE:递归搜索指定目录,将搜索到的满足条件的文件名生成一个列表,并将其存储到变量中。
其实 file 的用法还有很多,不止可以搜索文件。
cpp
CMAKE
#示例
file(GLOB Main_Source ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)
CMAKE_CURRENT_SOURCE_DIR 宏表示当前访问的 CMakeLists.txt 文件所在的路径。
要搜索的文件路径和类型可以加双引号,也可以不加。
2.5.3 搜索文件的缺点
这里需要补充的是,虽然上述的搜索文件看起来是很方便的,但是实际上你可能会遇到之前手动添加文件时遇不到的问题。
手动加入文件的时候你进行 cmake 和 make 是不会遇到任何问题的。但是当你进行搜索的时候就会发现,如果这个项目进行了一系列的拓展,添加了新的文件。尽管你可能认为我把新加的文件都加到对应的目录下了,而且类型也是同一类型,搜索文件肯定能搜到,编译链接不是轻而易举吗?
实际上在编译链接的时候单纯的 cmake .. 配置的是当前的搜索文件情况,也就是说在你第一次进行 cmake .. 的时候只搜索这一次的文件,然后生成一系列的 cmake 文件。在你添加了一些文件之后再用 make 的时候就不再重新生成一系列的 cmake 文件,所以每次添加新文件的时候都需要手动的再次进行 cmake 命令。
当然还有其他的办法可以解决,那就是在搜索文件的命令中加上 CONFIGURE_DEPENDS。加上 CONFIGURE_DEPENDS 后,CMake 会告诉生成的构建系统:"在每次构建前,检查一下 GLOB 的结果有没有变,如果变了就自动重新运行 cmake 配置。"
cpp
CMAKE
#示例
file(GLOB Main_Source CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)
2.6 PROJECT_SOURCE_DIR 和 CMAKE_CURRENT_SOURCE_DIR的区别
PROJECT_SOURCE_DIR 这个宏定义就是在执行cmake命令的时候后面携带的路径,对应的就是 CMakeLists.txt 这个文件所在的路径。也就是说如果没有 project() 命令的影响其所指的一直就是顶层的路径

嵌套图如上所示
若无 project 那么一直指向 top
CMAKE_CURRENT_SOURCE_DIR则是无论如何都指向嵌套中最内层的CMakeLists.txt的路径
2.7 包含头文件
做项目的时候一般会将头文件和源文件分开编写和放置,所以为了在编译的过程中原文件能够正常的找到头文件,所以就需要在编写CMAKE文件的时候将其标注出来。
cpp
CMAKE
include_directories(headpath)
不过在写头文件的路径的时候建议将路径写成绝对路径。
2.8 CMAKE中制作库文件
2.8.1 制作静态库
制作静态库需要使用的命令如下:
cpp
CMAKE
add_library(库名称 STATIC 源文件1 [源文件2] ...)
静态库在 Linux 中的名字组成为 lib + 库名称 + .a ,在使用该命令的时候仅填写库名称剩下的两部分会自动补充上。
Windows系统的静态库虽然风格不同但是也只用填写库名称即可。
第二个参数明显的 STATIC 就是要生成静态库的标志。
2.8.2 制作动态库
制作动态库和制作静态库用的命令是基本一样的
cpp
CMAKE
add_library(库名称 SHARED 源文件1 [源文件2] ...)
仅仅只改了第二个参数而已。
2.8.3 指定输出的路径
和之前 set 指定输出的路径的原因一样,生成出来的静态库或者动态库文件不能乱放,所以就需要指定一个生成的路径。
不过指定生成的路径仍然是通过 set 命令。
cpp
CMAKE
# 设置动态库/静态库生成路径
set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)
# 生成动态库
add_library(库名称 SHARED ${SRC_LIST})
# 生成静态库
add_library(库名称 STATIC ${SRC_LIST})
2.9 包含库文件
在做项目的时候有时候不仅仅需要用自己写的库文件,还有可能用别人的库文件,所以cmake中必须有这样去链接其他库文件的功能。
2.9.1 链接静态库
在 cmake 中,链接静态库的命令如下:
cpp
CMAKE
link_libraries(库名称 [库名称...])
第一个参数:指定要链接的静态库的名称。这个名称可以是静态库的全名,既带上 lib 和 .a ,也可以是去掉 lib 和 .a 的名称。
第二个参数:指定要连接的更多的静态库的名称。
如果要链接的静态库不是系统提供的,而是自己制作的或者使用第三方的静态库,可能会出现静态库找不到的情况,此时需要将静态库的路径制定出来。
cpp
CMAKE
link_directories(lib path)
2.9.2 对库文件的一点补充和理解
如果想要使用制作的库文件,就需要先把制作的库文件发布出去,发布的时候需要提供两个文件,一个是库文件,一个是头文件。因为头文件里面有库文件里面定义的一系列函数的声明,为什么要有声明?因为如果没有声明,就不知道库里面有什么东西。如果不知道怎么调用库里面给我们提供的函数呢?其实库就是一个源文件,只不过是把若干个源文件进行了打包操作,生成了一个二进制文件,生成的这个二进制文件,我们给他改了个名字叫做库文件。其实如果把它变成文本格式就是源文件了,如果只有原文件,那么咱们能够看懂,能够知道里面有什么函数,能够调用函数。但是现在变成库文件之后就看不懂了,所以必须要提供一个头文件,才能够知道这个库里面有什么东西。
开发的过程中,声明的头文件里面的一些函数的声明在其对应的cpp文件里面是一模一样的,只不过是逻辑写在了cpp文件里面。所以头文件不仅仅是对cpp文件中函数的声明,更是对外的一个接口。
2.9.3 链接动态库
在编写程序的时候不仅会用到静态库还会用到动态库,所以当然要学习如何链接动态库。
cpp
CMAKE
target_link_libraries
(
<target>
<PRIVATE|PUBLIC|INTERFACE> <item>...
[<PRIVATE|PUBLIC|INTERFACE> <item>...]...
)
target:指定要加载的库文件的名字,文件可以是一个源文件,可以是一个动态库文件,可以是一个可执行文件。
PRIVATE | PUBLIC | INTERFACE:动态库的访问权限,默认是PUBLIC。
一般来说熟悉C++的人对这个也不陌生。唯一的区别就是,当有多个库的时候并且不同的库之间产生了依赖关系。
依赖关系就是假设有三个及以上的动态库A、B、C,A调用了B中的内容,B又调用了C中的内容,此时A就相当于调用了 B 和 C 中的内容。当然前提条件是链接的时候是 PUBLIC。
PUBLIC:就是在产生依赖关系的时候一个库可以被间接链接的库所使用,包括库中的符号。
PRIVATE:顾名思义就是私有的,在上面A、B、C的例子中如果 B 链接 C 是通过私有的方式,那么A 就无法间接链接 C。
INTERFACE:这种情况比较特殊,虽然后面的库不会被链接到前面的库中,但是会导出符号。这个符号指的是被链接的动态库 的源文件中的一些函数转换成库文件 之后的一些二进制符号。意思就是前面动态库只知道有一定的符号,交接给了他,但是不知道有后面这个动态库的存在。也就是只知道有这么一个函数,但是不知道从哪里来的。如果是private和public的话就是知道这个函数是属于谁的。
注意:动态库的链接要放在可执行程序生成的后面,这跟动态库在可执行程序中实际的使用是有关系的,生成了可执行程序之后,动态库不一定被使用。
2.10 日志
cmake 中用户还可以设置显示一条消息
cpp
CMAKE
message([STATUS|WARNING|AUTHOR_WARNING|FATAL_ERROR|SEND_ERROR] "message to display" ...)
什么都不写:重要消息
STATUS:非重要消息
WARNING:CMake 警告, 会继续执行
AUTHOR_WARNING:CMake 警告 (dev), 会继续执行
SEND_ERROR:CMake 错误, 继续执行,但是会跳过生成的步骤
FATAL_ERROR:CMake 错误, 终止所有处理过程
cmake的命令行工具会在 stdout 上显示 STATUS 消息,在 stderr 上显示其他所有消息。cmake的GUI会在它的 log 区域显示所有消息。
cmake警告和错误消息的文本显示使用的是一种简单的标记语言。文本没有缩进,超过长度的行会回卷,段落之间以新行做为分隔符。
cpp
CMAKE
# 输出一般日志信息
message(STATUS "source path: ${PROJECT_SOURCE_DIR}")
# 输出警告信息
message(WARNING "source path: ${PROJECT_SOURCE_DIR}")
# 输出错误信息
message(FATAL_ERROR "source path: ${PROJECT_SOURCE_DIR}")
2.11 追加
有时候项目中的源文件并不一定都在同一个目录中,比如在一般得游戏项目中你的敌人源文件单独放在 enemies 文件夹中,而玩家源文件放在 player 文件夹中。但是这些源文件最终需要一起进行编译来生成最终的可执行文件或者库文件。
如果我们通过 file 命令对各个目录下的源文件进行搜索,最后还需要做一个字符串拼接的操作,因为 cmake 的操作本质字符串,关于字符串拼接可以使用 set 命令也可以使用 list 命令。
2.11.1 用 set 拼接
使用 set 进行拼接的格式如下
cpp
CMAKE
set(变量名1 ${变量名1} ${变量名2} ...)
很明显这个拼接就是把后面的变量名代表的文件拼接到第一个参数的变量名中,如果第一个变量名中已经有参数了,就将其覆盖掉。
2.11.2 用 list 拼接
使用 list 拼接的命令的格式是
cpp
CMAKE
list(APPEND <list> [<element> ...])
由于 list 不止有拼接字符串的功能,所以第一个参数先写APPEND代表此处使用 list 是作为拼接字符串的,后面的部分和 set 是一样的。
注意:后面的变量名如果不加 ${} 假设 abc 中存储了 helloworld 那么拼接的就是 abc 本身,也即原字符串 + "abc"。加上取值符号之后拼接的才是 原字符串 + "helloworld" 。
list追加字符串的过程中实际上每个子字符串之间是由 ";" 隔开的,但是通过message打印的时候是看不出来的。这是为了方便后续通过list进行字符串的删除操作。
2.12 字符串移除
有时候我们的文件夹中一下子包含了很多种相同类型的文件,但是在生成库的时候有一些文件是不需要的。但是文件的类型又确实是当前的类型,那么需要将不需要的文件从搜索中去除。
实现这个功能用的也是 list
cpp
CMAKE
list(REMOVE_ITEM <list> <value> [<value> ...])
仔细观察可以发现和上面追加时的区别就是第一个参数从APPEND改成了REMOVE_ITEM。
注意:通过 file 命令搜索源文件的时候得到的是文件的绝对路径(在list中每个文件对应的路径都是一个item,并且都是绝对路径),那么在移除的时候也要将该文件的绝对路径指定出来才可以,否是移除操作不会成功。
list 命令还有很多其他的用法,只不过不常用,在这里就不再一一赘述了。