CMake学习笔记
本学习笔记的主要内容来自于大丙老师(博客地址:https://subingwen.cn/)的课程教授以及其官方网站的CMake教程,有兴趣的看官可以去自己去搜索学习下面贴一下相关课程和笔记地址:
课程地址:https://www.bilibili.com/video/BV14s4y1g7Zj/?spm_id_from=333.999.0.0
笔记地址:https://subingwen.cn/categories/CMake/
1.CMake的概述
CMake作为一种能够辅助项目开发中管理编译的工具能通过编写 CMakeLists.txt
的方式生成辅助生成Makefile文件,然后通过Makefile对项目中的源码进行 预处理、编译、汇编、链接 最终生成可执行的二进制文件。
2.CMake文件编写
2.1CMake文件编写内容概述:
在进行一个 CMakeLists.txt
文件编写的时候,需要通过指明所需编译的头文件 .h
以及 源文件 .cpp/.c
文件的路径来生成可执行文件、动态库后者静态库所需要的 Makefile 文件,最后通过 Make
指令来完成项目文件的编译。同时在这个过程中你可能也会涉及到对动态库以及静态库的一些链接,这个时候也需要通过CMake中的一些指令来完成。 所以为了能够阐述清楚CMake的一些基本操作本章将分为如下几个小结:制作可执行程序,制作动态库与静态库、嵌套Cmake编写:
2.2 制作可执行程序
在制作可执行程序之前假设我们拿到如下一个目录结构的一个工程文件 例子文件同样来自于课程
$ tree
.
├── add.c
├── div.c
├── head.h
├── main.c
├── mult.c
└── sub.c
其中 head.h
表示该项目的头文件 main.c
表示测试功能的函数 其余的文件均为描述函数功能的源文件,怎么样能够通过 CMake的方式将其生成可以编译的 Makefile文件呢? 下面将介绍两种方式, (1)直接添加文件的方式:这种方式通过将每个文件名添加在指令中,类似于直接使用简单的gcc指令编译方式,但是简单易懂但是泛用性不强;(2)搜索文件的方式,通过搜索同一目录下的所有源文件与头文件将他们编译在一起,这种做法较为常见,适用于文件数量较多的情况下。 下面我们都将一一介绍:
2.2.1 直接指定文件添加
a、直接输入的方式
首先创建一个名字为 CmakeLists.txt
的文件,在文件中输入如下内容
cmake_minimum_required(VERSION 3.0)
project(CALC)
add_executable(app add.c div.c main.c mult.c sub.c)
下面将介绍这几个指令:
cmake_minimum_required
: 指定使用的Cmake版本project
:定义工程名称add_executable
:定义生成一个可执行文件其语法如下:
add_executable(可执行程序名 源文件名称)
其中可执行程序名可以根据用户需求自己指定,源文件可以指定多个通过可以通过 ;
或者空格来实现具体而言如下
# 样式1
add_executable(app add.c div.c main.c mult.c sub.c)
# 样式2
add_executable(app add.c;div.c;main.c;mult.c;sub.c)
需要注意的是,这里不需要将头文件包含在内,因为头文件已经在源文件中进行了说明所以不需要再次添加,当然也会存在头文件的路径不对的情况,这种情况将放在后面说明。
b、变量设置的方式
当然我们也可以通过定义变量的方式来优化这种臃肿的方式具操作如下
set(SRC_LIST add.c;div.c;main.c;mult.c;sub.c)
add_executable(app ${SRC_LIST})
其中这里用到了 SET
指令,该指令的用途主要是创建一个变量来储存用户指定的内容,具体用法如下
# SET 指令的语法是:
# [] 中的参数为可选项, 如不需要可以不写
SET(VAR [VALUE] [CACHE TYPE DOCSTRING [FORCE]])
当然 SET
指令也可以对用于一些CMAKE中的预设宏进行定义比如 CMAKE_CXX_STANDARD
该宏表示需要编译的程序所需要的C++的版本,具体代码如下:
#增加-std=c++11
set(CMAKE_CXX_STANDARD 11)
#增加-std=c++14
set(CMAKE_CXX_STANDARD 14)
#增加-std=c++17
set(CMAKE_CXX_STANDARD 17)
常见的宏变量还有
CMAKE_PROJECT_NAME: 项目的名称。
CMAKE_CXX_STANDARD: 用于指定C++标准。
CMAKE_BINARY_DIR:表示构建输出的二进制目录的路径。
CMAKE_CURRENT_BINARY_DIR:表示当前构建的二进制目录的路径。
CMAKE_CURRENT_LIST_DIR:表示当前 CMakeLists.txt 文件的目录路径。
CMAKE_SOURCE_DIR:表示源代码的根目录路径。
CMAKE_CACHEFILE_DIR:表示当前缓存文件的目录路径。
CMAKE_CXX_FLAGS: 编译C++代码的标志。
CMAKE_C_FLAGS: 编译C代码的标志。
CMAKE_EXE_LINKER_FLAGS: 可执行文件链接器的标志。
CMAKE_SHARED_LINKER_FLAGS: 共享库链接器的标志。
CMAKE_SHARED_LIBRARY_SUFFIX: 共享库的后缀名。
CMAKE_LIBRARY_OUTPUT_DIRECTORY: 库文件输出的目录。
CMAKE_RUNTIME_OUTPUT_DIRECTORY: 可执行文件输出的目录。
这些均可以通过 set
指令来实现修改
2.2.2 搜索文件的方式添加
在2.2.1节中展示的方式针对相对简单的情况可以使用,但是当源文件数量较多的时候,我们需要手动输入的过程就会显得十分臃肿,所以CMake提供了自动搜索文件的功能,方便用户一键得到指定文件夹下所有的符合要求的文件
a、aux_source_directory的方式
还是之前的例子,现在我们输入如下指令
cmake_minimum_required(VERSION 3.0)
project(CALC)
include_directories(${PROJECT_SOURCE_DIR}/include)
# 搜索 src 目录下的源文件
aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/src SRC_LIST)
add_executable(app ${SRC_LIST})
在这段指令中我们使用了 aux_source_directory
指令来实现查找当前文件夹下所有的文件并将该文件放入变量 SRC_LIST
中,同时,需要注意的是在该指令中我们使用到了如下两个宏变量来得到文件位置的路径,{PROJECT_SOURCE_DIR} 以及 {CMAKE_CURRENT_SOURCE_DIR} 这两个指令第一个是获取当前项目所在的文件夹位置,第二个为获取当前编写的CMakeLists.txt所在的文件目录位置
b、file的方式
除了第一种方式以外,我们还可以通过 file
命令来实现文件的查找,其指令的说明为
file(GLOB/GLOB_RECURSE 变量名 要搜索的文件路径和文件类型)
- GLOB: 将指定目录下搜索到的满足条件的所有文件名生成一个列表,并将其存储到变量中。
- GLOB_RECURSE:递归搜索指定目录,将搜索到的满足条件的文件名生成一个列表,并将其存储到变量中。
所以使用file命令时可以对上述内容做如下修改
cmake_minimum_required(VERSION 3.0)
project(CALC)
include_directories(${PROJECT_SOURCE_DIR}/include)
# 搜索 src 目录下的源文件
file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)
add_executable(app ${SRC_LIST})
即可完成与 aux_source_directory
一样的效果
2.2.3 包含头文件:
在2.2.1节中我们提到如果当头文件 .h
文件与源文件不在同一个文件夹下的时候实际上我们运行上述指令会发生错误,错误信息会告诉我们找不到头文件所在目录,而往往我们在工程项目中头文件与源文件会放在不同的目录下所有遇到这种情况下我们需要添加头文件目录,例如我们将上面的文件结构修改问如下结构:
$ tree
.
├── build
├── CMakeLists.txt
├── include
│ └── head.h
└── src
├── add.cpp
├── div.cpp
├── main.cpp
├── mult.cpp
└── sub.cpp
其中在 include
文件夹下包含了我们的头文件,这个时候我们就需要使用到 include_directories
指令来指定我们的文件路径其用法如下
include_directories(headpath)
然后我们的 CMakeLists.txt
文件内容如下:
cmake_minimum_required(VERSION 3.0)
project(CALC)
set(CMAKE_CXX_STANDARD 11)
set(HOME /home/robin/Linux/calc)
set(EXECUTABLE_OUTPUT_PATH ${HOME}/bin/)
include_directories(${PROJECT_SOURCE_DIR}/include)
file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)
add_executable(app ${SRC_LIST})
在这里我们利用 inclu_directories
来实现了头文件的包含,同时我们对预设宏 EXECUTABLE_OUTPUT_PATH
进行了定义指明了可执行文件的输出路径,需要注意的是,我们无需要再去创建相关文件夹,如果没有该文件夹,在MakeFile编译时会给我生成并创建相关文件夹。
2.3 制作动态库与静态库
在c++里面库是一种包含一组函数和数据结构的文件集合,其本质是可阅读的一个或多个.cpp文件的二进制形式,该库文件需要在.h文件中进行声明。库文件的好处在于这样更加有利于保护源代码,库文件的存在相当于对源文件进行了编码这样让源代码更加安全,除此以外,库文件还可以被多个程序共享,方便移植,库的主要种类就包括了动态库与静态库
2.3.1 动态库与静态库的区别:
动态库与静态库的区别主要体现在链接的过程中,在链接过程对于静态库而言:对于编译后生成对象文件(.o)与静态库进行链接时,静态库会被完全复制到最终的可执行文件中,这导致了生成的可执行文件无需依赖外部库,可以直接运行,但是运行的时候库会被同时加载到物理内存中,对于电脑的内存大小有较高的要求,但是动态库能够解决静态库的这个问题,在链接过程中对于静态库而言:对于编译后生成的对象文件(.o)与动态库链接时,动态库不会被复制到最终的可执行文件中,而是在程序运行时被动态调用加载到内存中。这样让其可执行文件相对较小,在同等条件下运行时加载到物理内存中的大小要比静态库的编译结果小得多,且能够动态加载和卸载。但是缺点在于需要依赖外部库文件。
2.3.2 制作动态库与链接方式:
a、动态库的制作
在Cmake中如果需要生成动态库,需要如下指令
add_library(库名称 SHARED 源文件1 源文件2 ...)
假设我们的文件目录是这样
.
├── build
├── CMakeLists.txt
├── include # 头文件目录
│ └── head.h
├── main.cpp # 用于测试的源文件
└── src # 源文件目录
├── add.cpp
├── div.cpp
├── mult.cpp
└── sub.cpp
如果我们需要将src文件中编译为动态库 libcalc.so
用于测试源文件使用则我们需要进行如下指令
cmake_minimum_required(verison 3.0)
project(CALC)
include_directories({CMAKE_CURRENT_SOURCE_DIR}/include)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/lib)
aux_source_directory(SRC_LISTS {PROJECT_SOURCE_DIR}/src)
add_library(calc SHARED ${SRC_LISTS})
上述指令即可完成动态库的生成,可以使用 set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/lib)
的宏变量设置来改变库的输出位置
b、动态库的链接:
如果需要链接一个动态库的话我们需要利用 target_link_libraries
指令来将可执行文件与动态库链接起来,其语法如下
target_link_libraries(
<target>
<PRIVATE|PUBLIC|INTERFACE> <item1>...
[<PRIVATE|PUBLIC|INTERFACE> <item2>...]...)
这里的意思就是将可执行文件、源文件或者库文件 target
与 库 item1
、item2
链接起来生成可执行文件。下面是使用的例子,加入我们的文件排布是这样一种形式
$ tree
.
├── build
├── CMakeLists.txt
├── include
│ └── head.h # 动态库对应的头文件
├── lib
│ └── libcalc.so # 自己制作的动态库文件
├── main.cpp # 测试用的源文件
└── src # 源文件目录
├── add.cpp
├── div.cpp
├── mult.cpp
└── sub.cpp
那么我们要对测试文件使用我们之前生成的 libcal.so
则需要在Cmake输入如下内容
cmake_minimum_required(VERSION 3.0)
project(TEST)
file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
# 指定源文件或者动态库对应的头文件路径,只有main.cpp
include_directories(${PROJECT_SOURCE_DIR}/include)
# 指定要链接的动态库的路径
link_directories(${PROJECT_SOURCE_DIR}/lib)
# 添加并生成一个可执行程序
add_executable(app ${SRC_LIST})
# 指定要链接的动态库
target_link_libraries(app pthread calc)
在这里有几点需要说明:
首先如果需要指定需要连接库的位置,其指令为 link_directories
,其用法如下
link_directories(path)
在 CMake 中可以在生成可执行程序之前,通过命令指定出要链接的动态库的位置,指定静态库位置使用的也是这个命令
然后可看到在最后使用 target_link_libraries
指令时是这样的
target_link_libraries(app pthread calc)
这里的 app
表示可执行问文件,而 pthread
和 calc
分别表示动态库libpthread.so
以及libcalc.so
- 在指定的时候一般会掐头(lib)去尾(.so)
2.3.3 制作静态库与链接方式
a、静态库的制作
在CMake中,要制作静态库,需要的指令为
add_library(库名称 STATIC 源文件1 源文件2 ...)
如果我们得到项目目录如下:
.
├── build
├── CMakeLists.txt
├── include # 头文件目录
│ └── head.h
├── main.cpp # 用于测试的源文件
└── src # 源文件目录
├── add.cpp
├── div.cpp
├── mult.cpp
└── sub.cpp
那么我们在 CMakeLists.txt
中使用如下指令:
cmake_minimum_required(VERSION 3.0)
project(CALC)
include_directories(${PROJECT_SOURCE_DIR}/include)
file(GLOB SRC_LIST "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp")
add_library(calc STATIC ${SRC_LIST})
这样最终就会生成对应的静态库文件 libcalc.a
b、静态库的链接:
在Cmake中,链接静态库的命令如下:
link_libraries(<static lib> [<static lib>...])
其中参数表示需要链接的多个静态库的名字,中间用空格分开
- 可以是全名
libxxx.a
- 也可以是掐头(
lib
)去尾(.a
)之后的名字xxx
假设我们拿到的项目文件是这样的
$ tree
.
├── build
├── CMakeLists.txt
├── include
│ └── head.h
├── lib
│ └── libcalc.a # 制作出的静态库的名字
└── src
└── main.cpp
则我们在 Cmakelists.txt
中输入如下指令:
cmake_minimum_required(VERSION 3.0)
project(CALC)
# 搜索指定目录下源文件
file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)
# 包含头文件路径
include_directories(${PROJECT_SOURCE_DIR}/include)
# 包含静态库路径
link_directories(${PROJECT_SOURCE_DIR}/lib)
# 链接静态库
link_libraries(calc)
add_executable(app ${SRC_LIST})
其中同样包含了指令 link_directories
来指定库的路径
2.3.4指定输出路径:
a、EXECUTABLE_OUTPUT_PATH:
利用该宏设置适用于动态库的输出路径。
b、LIBRARY_OUTPUT_PATH:
该宏设置两者都适用。
2.4 嵌套CMake文件编写:
如果我们遇到这样一个项目目录
$ tree
.
├── build
├── calc
│ ├── add.cpp
│ ├── CMakeLists.txt
│ ├── div.cpp
│ ├── mult.cpp
│ └── sub.cpp
├── CMakeLists.txt
├── include
│ ├── calc.h
│ └── sort.h
├── sort
│ ├── CMakeLists.txt
│ ├── insert.cpp
│ └── select.cpp
├── test1
│ ├── calc.cpp
│ └── CMakeLists.txt
└── test2
├── CMakeLists.txt
└── sort.cpp
6 directories, 15 files
在这种相对较为复杂项目文件下需要管理的话,通常就需要用到嵌套的CMake文件管理,可以看到在项目目录下存在一个 CMakeLists.txt
文件然后每个文件夹下均存在一个 CMakeLists.txt
其中每个文件夹说明如下:
include 目录:头文件目录
calc 目录:目录中的四个源文件对应的加、减、乘、除算法
对应的头文件是include中的calc.h
sort 目录 :目录中的两个源文件对应的是插入排序和选择排序算法
对应的头文件是include中的sort.h
test1 目录:测试目录,对加、减、乘、除算法进行测试
test2 目录:测试目录,对排序算法进行测试
在上述文件中可以看到主目录的下的 CMakeLists.txt
可以作为一个根节点去管理各个分目录的 CMakeLists.txt
,每个分目录下的 CMakeLists.txt
是负责各自文件夹下的分目录的编译工作,这里就需要用到CMake指令来实现根节点的管理功能
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
source_dir
:指定了CMakeLists.txt
源文件和代码文件的位置,其实就是指定子目录binary_dir
:指定了输出文件的路径,一般不需要指定,忽略即可。EXCLUDE_FROM_ALL
:在子路径下的目标默认不会被包含到父路径的ALL
目标里,并且也会被排除在IDE工程文件之外。用户必须显式构建在子路径下的目标。
需要注意的是在: 根节点中的变量名称可以作为全局变量,全局有效,但是子节点的名称只能对当前的节点有效。
所在在项目中根节点的 CMakeLists.txt
文件内容如下:
cmake_minimum_required(VERSION 3.0)
project(test)
# 定义变量
# 静态库生成的路径
set(LIB_PATH ${CMAKE_CURRENT_SOURCE_DIR}/lib)
# 测试程序生成的路径
set(EXEC_PATH ${CMAKE_CURRENT_SOURCE_DIR}/bin)
# 头文件目录
set(HEAD_PATH ${CMAKE_CURRENT_SOURCE_DIR}/include)
# 静态库的名字
set(CALC_LIB calc)
set(SORT_LIB sort)
# 可执行程序的名字
set(APP_NAME_1 test1)
set(APP_NAME_2 test2)
# 添加子目录
add_subdirectory(calc)
add_subdirectory(sort)
add_subdirectory(test1)
add_subdirectory(test2)
在根节点中主要完成了两件事情:定义全局变量 以及 添加子目录 这样的话就能够将项目每个目录中的编译内容通过全局变量的形式统一起来,方便根节点对于整体项目进行编译与管理。
3.总结:
在上述学习总结了Cmake的一些简单的基础用法,更多用法仍然需要去查看Cmake的官方文档:https://cmake.org/documentation中总结学习。
特别感谢大丙老师的精彩讲解与演示,本文章中的主要内容均来自于老师的博客、视频以及自己的理解,若有不正之处还,请斧正!!!
老师的课程与博客:
课程地址:https://www.bilibili.com/video/BV14s4y1g7Zj/?spm_id_from=333.999.0.0
笔记地址:https://subingwen.cn/categories/CMake/