CMAKE学习笔记【Part1】
(零) 变量
一、基本概念
CMake 中的变量是 字符串类型(或列表),没有内置的数据结构如数组、字典等。变量的作用类似于 shell 脚本中的环境变量,但也有自己的作用域机制。
常见预定义变量(举例)
PROJECT_NAME
:当前项目的名称。CMAKE_CURRENT_SOURCE_DIR
:当前处理的源码目录。CMAKE_CURRENT_BINARY_DIR
:当前构建的二进制目录。CMAKE_SOURCE_DIR
:项目根目录。CMAKE_BINARY_DIR
:构建输出根目录。CMAKE_CXX_COMPILER
:C++ 编译器路径。CMAKE_BUILD_TYPE
:构建类型(Debug/Release)。
二、变量的设置
1. 使用 set()
设置变量
set(VAR_NAME value [CACHE TYPE DOCSTRING [FORCE]])
示例:
set(MY_VAR "Hello CMake")
message(STATUS "MY_VAR = ${MY_VAR}")
输出:
-- MY_VAR = Hello CMake
设置多个值(列表)
set(MY_LIST a b c)
message(STATUS "MY_LIST = ${MY_LIST}")
输出:
-- MY_LIST = a;b;c
如果你希望用空格分隔显示为字符串:
message(STATUS "MY_LIST = ${MY_LIST}")
# 或者转换为空格分隔
message(STATUS "MY_LIST = ${MY_LIST}")
2. 使用 option()
设置布尔变量
用于用户可配置的开关选项,默认值为 ON
或 OFF
。
option(USE_MYMATH "Use custom math library" ON)
你可以像普通变量一样使用它:
if(USE_MYMATH)
message(STATUS "Using custom math")
else()
message(STATUS "Not using custom math")
endif()
3. 使用 unset()
删除变量
unset(MY_VAR)
可用于清除某些变量的影响。
三、变量的引用
使用 ${VAR_NAME}
来引用变量值。
set(NAME "Alice")
message(STATUS "Hello, ${NAME}!")
输出:
-- Hello, Alice!
如果变量未定义,则会被替换为空字符串。
四、变量的作用域(Scope)
CMake 的变量作用域不像 C/C++ 那样严格,但它有以下几种行为:
1. 局部作用域(Local Scope)
默认情况下,set()
设置的变量只在当前 CMakeLists.txt
文件中有效,不会传递到父级或子级目录。
# CMakeLists.txt
set(MY_VAR "local")
add_subdirectory(subdir) # 进入 subdir 目录
# subdir/CMakeLists.txt
message(STATUS "MY_VAR = ${MY_VAR}") # 输出空,因为不在同一个作用域
2. PARENT_SCOPE
如果你想把一个变量设置回上一级作用域(比如你在 add_subdirectory()
内部设置变量返回给上级),可以使用:
# subdir/CMakeLists.txt
set(MY_VAR "from child" PARENT_SCOPE)
这样上级就能看到这个值了。
3. CACHE 变量(全局可见)
使用 set(... CACHE ...)
定义的变量是全局的,保存在 CMake 缓存中,可以在任何地方访问。
set(MY_CACHE_VAR "global value" CACHE STRING "Description of this variable")
这种变量通常用于跨目录通信或配置持久化(例如用户通过 -DMY_VAR=value
传参)。
你也可以使用 cache
类型来覆盖缓存值:
cmake .. -DMY_CACHE_VAR="new value"
五、常见问题与技巧
1. 如何查看所有变量?
你可以运行:
cmake --build . --target help
或者更直接地:
cmake -LAH ..
这会列出所有的缓存变量及其值。
2. 判断变量是否存在或是否为空
if(DEFINED VAR_NAME)
message(STATUS "${VAR_NAME} is defined.")
endif()
if(NOT VAR_NAME)
message(STATUS "${VAR_NAME} is empty or not set.")
endif()
3. 拼接字符串或路径
set(PATH1 "/usr/local")
set(PATH2 "bin")
set(FULL_PATH "${PATH1}/${PATH2}")
message(STATUS "Full path: ${FULL_PATH}")
输出:
-- Full path: /usr/local/bin
推荐使用 CMAKE_CURRENT_SOURCE_DIR
等宏拼接路径,避免硬编码。
4. 多个值如何处理?
set(SOURCES main.cpp utils.cpp helper.cpp)
add_executable(myapp ${SOURCES})
这是最常见的做法,适用于源文件列表、库名列表等。
六、最佳实践建议
场景 | 推荐做法 |
---|---|
设置本地变量 | set(VAR value) |
设置全局变量 | set(VAR value CACHE INTERNAL "") |
用户配置选项 | option(VAR "desc" ON/OFF) |
子目录返回变量 | set(VAR value PARENT_SCOPE) |
拼接路径 | 使用 ${CMAKE_CURRENT_SOURCE_DIR}/xxx |
判断变量是否存在 | if(DEFINED VAR) |
清理变量 | unset(VAR) |
实例总结
# 设置变量
set(MY_LIB mylib)
set(SOURCES main.cpp utils.cpp)
option(BUILD_TESTS "Build unit tests" ON)
# 引用变量
message(STATUS "Building with lib: ${MY_LIB}")
add_executable(app ${SOURCES})
# 控制逻辑
if(BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
(一)设置项目名 代码目录和构建目录
cmake_minimum_required(VERSION 3.1)
project(MyProject
VERSION 1.2.3
DESCRIPTION "A library for managing non-volatile memory"
HOMEPAGE_URL "https://example.com/libnvm"
LANGUAGES CUDA C CXX
)
# 打印 PROJECT_SOURCE_DIR 和 PROJECT_BINARY_DIR
message(STATUS "Source directory: ${PROJECT_SOURCE_DIR}")
message(STATUS "Binary directory: ${PROJECT_BINARY_DIR}")
一、LANGUAGES
- C: CMake 默认会启用的语言之一,适用于C源代码项目。
- CXX 或 C++: 另一个默认启用的语言,适用于C++源代码项目。
- CUDA: 从 CMake 3.8 版本开始正式支持 CUDA 作为一等公民,允许直接编写 CUDA 源文件(
.cu
)并将其集成到构建系统中。 - ASM: 支持汇编语言。需要注意的是,使用 ASM 需要特定的编译器支持,并且可能需要额外配置。
- 其他语言
二、代码目录和构建目录
变量名 | 含义 | 是否变化 |
---|---|---|
PROJECT_SOURCE_DIR | 顶级 CMakeLists.txt 所在目录(即源码目录) | 固定 |
PROJECT_BINARY_DIR | 你运行 cmake 的目录(即构建目录) | 根据你运行命令的位置而定 |
假设你的项目结构如下:
/home/user/my_project/
├── CMakeLists.txt
└── src/
└── main.cpp
情况一:就地构建(不推荐)
cd /home/user/my_project
cmake .
- 此时:
PROJECT_SOURCE_DIR
=/home/user/my_project
PROJECT_BINARY_DIR
=/home/user/my_project
⚠️ 这样会导致构建文件和源码混在一起,不利于维护。
情况二:外出构建out-of-source build( 推荐做法)
cd /home/user/my_project/
mkdir build
cd build
cmake ..
- 此时:
PROJECT_SOURCE_DIR
=/home/user/my_project
PROJECT_BINARY_DIR
=/home/user/my_project/build
✅ 所有构建产物都会被放在这个目录中,保持源码目录干净整洁。
三、添加子目录到构建系统中
add_subdirectory()
是 CMake 中用于组织大型项目的非常重要的命令,它允许你将一个子目录添加到构建系统中,并在该子目录中执行其 CMakeLists.txt
文件。告诉 CMake 进入 src/
子目录,读取并处理其中的 CMakeLists.txt
文件。 子目录中的目标(如库或可执行文件)会被构建,并被主项目所使用。如果你在子目录中使用了 add_subdirectory()
,每个子目录也有自己的 CMAKE_CURRENT_BINARY_DIR
,但 PROJECT_BINARY_DIR
始终指向顶层构建目录。子目录中也可以继续调用 add_subdirectory()
添加更深层的模块。
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
source_dir
: 必须参数,相对于当前CMakeLists.txt
的源代码子目录路径。binary_dir
: 可选参数,指定子目录构建输出的位置(默认是与源码同路径构建)。EXCLUDE_FROM_ALL
: 可选参数,表示子目录中的目标不会被默认构建(除非被其他目标依赖)。
使用示例
目录结构
my_project/
├── CMakeLists.txt # 主 CMakeLists.txt
├── main.cpp
└── src/
├── CMakeLists.txt # 子模块
└── mylib.cpp
src/CMakeLists.txt
示例
# 构建一个静态库 libmylib.a
add_library(mylib STATIC mylib.cpp)
# 导出头文件目录
target_include_directories(mylib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
主 CMakeLists.txt
示例
cmake_minimum_required(VERSION 3.14)
project(MyProject LANGUAGES CXX)
# 添加子目录 src
add_subdirectory(src)
# 添加主程序
add_executable(my_app main.cpp)
# 链接子目录中生成的库
target_link_libraries(my_app PRIVATE mylib)
关键变量说明
在子目录中可以使用的变量:
变量 | 含义 |
---|---|
CMAKE_CURRENT_SOURCE_DIR | 当前 CMakeLists.txt 所在的源码目录 |
CMAKE_CURRENT_BINARY_DIR | 对应的构建目录(即生成中间文件的地方) |
PROJECT_SOURCE_DIR | 顶层项目的源码目录(不变) |
PROJECT_BINARY_DIR | 顶层项目的构建目录(不变) |
指定独立构建目录
你可以为子目录指定一个单独的构建路径:
add_subdirectory(src build_src)
这会把 src/
目录下的构建产物放到 build_src/
路径下,而不是直接放在 src/
中。
⚠️ 注意:这个功能通常用于外部项目集成,比如结合
ExternalProject_Add
使用。
(二)定义可执行文件
手动添加源文件
add_executable()
是 CMake 中用于定义一个可执行文件目标的命令。它告诉 CMake 你想要从一组源代码文件构建出一个可执行程序,并允许你在后续步骤中为这个目标添加属性,比如链接库、编译选项等。
- 定义目标:指定要生成的可执行文件名称以及由哪些源文件构成。
- 组织构建:将相关的源文件组合成一个可执行文件,便于管理和构建。
add_executable(target_name source1 [source2 ...])
target_name
: 可执行文件的目标名称(即输出的可执行文件名)。source1, source2, ...
: 构建该可执行文件所需的源代码文件列表。
非递归自动查找源文件
# 作用:查找当前目录(. 表示当前目录)下所有的源文件(默认识别 .c, .cxx, .cpp, .cc 等),并将这些文件路径保存到变量 DIR_NAME 中。
# 不会递归子目录:只查找当前目录,不进入子目录。
# 常用于简单项目:适合小型项目或演示项目,快速将当前目录下所有源文件加入构建系统。
aux_source_directory(. DIR_NAME)
add_executable(target_name ${DIR_NAME})
递归查找源文件
file(GLOB_RECURSE ...)
不会自动检测新增的文件,在 IDE 中(如 CLion、Qt Creator),可能需要重新运行 CMake 才能识别新加入的文件。
# 递归查找 src 目录下所有 .cpp 文件(包括子目录)这里的 GLOB_RECURSE 表示递归搜索。
file(GLOB_RECURSE SRC_LIST
"${PROJECT_SOURCE_DIR}/*.cpp"
"${PROJECT_SOURCE_DIR}/*.cc"
"${PROJECT_SOURCE_DIR}/*.cxx"
)
# 添加可执行程序
add_executable(myapp ${SRC_LIST})
示例
假设有一个简单的项目结构如下:
my_project/
├── CMakeLists.txt
└── main.cpp
其中 main.cpp
包含了项目的入口代码。
CMakeLists.txt
示例
cmake_minimum_required(VERSION 3.10)
project(MyExecutable LANGUAGES CXX)
# 添加可执行文件目标
add_executable(my_app main.cpp)
在这个例子中,my_app
将是最终生成的可执行文件的名字,而 main.cpp
则是它的源文件。
使用场景与扩展
1. 多个源文件
如果你的项目包含多个源文件,可以像下面这样列出所有源文件:
add_executable(my_app main.cpp utils.cpp network.cpp)
或者,使用通配符简化输入(虽然不推荐,因为这可能会导致不必要的重新构建):
file(GLOB SOURCES "src/*.cpp")
add_executable(my_app ${SOURCES})
2. 链接外部库
你可以通过 target_link_libraries()
来链接你的可执行文件到其他库:
find_package(SomeLibrary REQUIRED)
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE Some::Library)
3. 设置编译选项
你可以通过 target_compile_options()
或者 target_compile_definitions()
等命令来设置特定于目标的编译器选项或宏定义:
add_executable(my_app main.cpp)
target_compile_options(my_app PRIVATE -Wall -Wextra)
target_compile_definitions(my_app PRIVATE DEBUG=1)
4. 设置包含目录
为了包含头文件路径,可以使用 target_include_directories()
:
add_executable(my_app main.cpp)
target_include_directories(my_app PRIVATE ${PROJECT_SOURCE_DIR}/include)
注意事项
- 目标名称唯一性:在一个 CMake 项目中,所有的目标名称必须是唯一的。
- 避免重复定义:不要在同一个 CMakeLists.txt 文件或不同的子目录中重复定义相同的目标名称。
- 现代 CMake 实践:尽量使用目标级别的命令(如
target_link_libraries
,target_include_directories
)而不是全局命令(如include_directories
,link_libraries
),以提高封装性和减少冲突的可能性。
(三)头文件搜索路径和指定链接库
一、目标级命令 vs 全局作用域命令
目标级命令 | 全局作用域命令 |
---|---|
target_link_libraries(...) | link_libraries(...) , link_directories(...) |
target_include_directories(...) | include_directories(...) |
target_compile_options(...) | add_compile_options(...) |
target_compile_definitions(...) | add_definitions(...) |
二、逐个讲解每个命令的作用和用法
1. target_include_directories(...)
作用 & 示例
为某个目标添加头文件搜索路径。在哪里搜索头文件。
target_include_directories(myapp PRIVATE ${PROJECT_SOURCE_DIR}/include)
PRIVATE
: 表示这些头文件路径只对myapp
生效。- 支持
PUBLIC
和INTERFACE
模式,用于控制是否将路径暴露给依赖者。
旧式写法(不推荐):
include_directories(${PROJECT_SOURCE_DIR}/include)
add_executable(myapp main.cpp)
⚠️ 缺点:
include_directories(...)
是全局生效的,会影响所有后续定义的目标,即使它们并不需要这些路径。
2. target_link_libraries(...)
作用 & 示例
指定某个目标(如可执行文件或库)在链接阶段需要使用的其他库。
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE Some::Library)
PRIVATE
: 表示这个依赖只用于myapp
,不会传递给使用myapp
的目标。Some::Library
: 是一个“导入目标”(imported target),通常通过find_package()
获得。
旧式写法(不推荐):
link_directories(/usr/local/lib)
link_libraries(Some::Library)
add_executable(myapp main.cpp)
⚠️ 缺点:
link_directories
和link_libraries
是全局生效的,影响所有后续目标,容易造成混乱。
3. target_compile_options(...)
作用 & 示例
为某个目标设置编译器选项(如 -Wall
, -O3
, /W4
等)。
target_compile_options(myapp PRIVATE -Wall -Wextra -O3)
- 只对
myapp
这个目标生效。 - 支持跨平台编译器选项判断:
target_compile_options(myapp PRIVATE
$<$<CXX_COMPILER_ID:GNU>:--pedantic>
$<$<CXX_COMPILER_ID:MSVC>:/W4>
)
旧式写法(不推荐):
add_compile_options(-Wall -Wextra)
add_executable(myapp main.cpp)
⚠️ 缺点:
add_compile_options(...)
是全局生效的,影响所有目标。
4. target_compile_definitions(...)
作用 & 示例
为某个目标定义宏定义(预处理器宏),相当于 -DNAME=VALUE
。
target_compile_definitions(myapp PRIVATE DEBUG=1 USE_FEATURE_X)
- 仅对
myapp
生效。 - 不会污染其他目标。
旧式写法(不推荐):
add_definitions(-DDEBUG=1 -DUSE_FEATURE_X)
add_executable(myapp main.cpp)
⚠️ 缺点:
add_definitions(...)
是全局生效的,影响所有目标。
三、进阶技巧:使用 INTERFACE 库封装接口
你可以创建一个纯接口的库,把公共配置集中起来:
add_library(mylib INTERFACE)
target_include_directories(mylib INTERFACE ${PROJECT_SOURCE_DIR}/include)
target_compile_definitions(mylib INTERFACE DEBUG=1)
target_link_libraries(mylib INTERFACE Some::Library)
# 使用该接口库
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE mylib)
(四)动态库和静态库
在 CMake 中,你可以通过 add_library()
命令来创建静态库或动态库(共享库)。这两个类型的库在编译、链接和最终部署时有不同的行为和用途。
-
静态库(Static Library):
- 文件扩展名通常为
.a
(Unix/Linux)或.lib
(Windows)。 - 在编译时,静态库的代码会被直接复制到生成的可执行文件中。
- 优点:部署简单,因为所有依赖都已包含在可执行文件内;启动速度快,因为没有额外的加载过程。
- 缺点:可能导致可执行文件体积较大;如果多个程序使用同一个静态库,则每个程序都会携带一份副本,浪费磁盘空间和内存。
- 文件扩展名通常为
-
动态库(Shared Library):
- 文件扩展名为
.so
(Unix/Linux)、.dylib
(macOS)或.dll
(Windows)。 - 在编译时仅记录对库的引用,在运行时才真正加载这些库。
- 优点:多个程序可以共享同一份库的副本,节省磁盘空间和内存;便于更新,无需重新编译依赖它的程序。
- 缺点:需要确保在运行时能够找到并加载相应的库文件;可能会导致启动时间增加,因为需要加载外部库。
- 文件扩展名为
创建静态库和动态库
在 CMake 中,使用 add_library()
来定义库,并通过指定库类型来决定是创建静态库还是动态库。如果没有指定 STATIC
或 SHARED
,CMake 将根据平台默认设置选择一种类型。但是为了明确性和控制力,建议总是显式地指定库类型。
创建静态库STATIC
add_library(my_static_lib STATIC source1.cpp source2.cpp)
STATIC
: 表示创建一个静态库。
创建动态库SHARED
add_library(my_shared_lib SHARED source1.cpp source2.cpp)
SHARED
: 表示创建一个动态库。
使用库
无论是静态库还是动态库,都可以通过 target_link_libraries()
来将它们链接到你的可执行文件或其他库中。
示例
假设你有一个项目结构如下:
my_project/
├── CMakeLists.txt
└── src/
├── main.cpp
└── mylib.cpp
└── mylib.h
CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(MyProject LANGUAGES CXX)
# 添加子目录 src
add_subdirectory(src)
# 添加主程序
add_executable(main_app main.cpp)
# 链接库
target_link_libraries(main_app PRIVATE mylib) # 假设 mylib 是上面添加的库
src/CMakeLists.txt
# 创建静态库
add_library(my_static_lib STATIC mylib.cpp)
# 创建动态库
add_library(my_shared_lib SHARED mylib.cpp)
# 导出静态库头文件目录
target_include_directories(my_static_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
# 导出动态库头文件目录
target_include_directories(my_shared_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
# 在 Windows 上构建 .dll 动态库时,默认情况下,CMake 不会自动导出任何符号(与 Linux/macOS 上的 .so/.dylib 行为不同)。你需要明确告诉编译器哪些符号应该被导出,供外部程序调用。Linux、macOS 上不需要加这一行代码
set_target_properties(my_shared_lib PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS TRUE)
# 安装库文件
install(TARGETS my_static_lib my_shared_lib
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin
INCLUDES DESTINATION include)
# 安装头文件(可选,如果 PUBLIC 没有包含全部头文件)
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/mylib.h
DESTINATION include)
# 可选:导出配置文件以支持 find_package()
install(EXPORT MyLibTargets
FILE MyLibTargets.cmake
NAMESPACE MyLib::
DESTINATION lib/cmake/MyLib)
特别注意事项
-
Windows 上的 DLLs:在 Windows 平台上,当创建动态库时,通常还需要提供一个“导入库”(.lib 文件),这个文件包含了对外部函数的引用信息。CMake 会自动生成这个导入库。
-
版本号与 ABI 兼容性:对于共享库,特别是在 Linux 系统上,考虑给库加上版本号以管理 ABI(应用程序二进制接口)兼容性问题。可以通过
set_target_properties()
设置属性来实现这一点。 -
安装规则:如果你计划分发你的库,可能需要定义安装规则,以便于正确地安装头文件和库文件到系统目录。
例如:
install(TARGETS my_shared_lib DESTINATION lib)
install(FILES mylib.h DESTINATION include)
这将把你的共享库安装到系统的 lib
目录下,头文件安装到 include
目录下。
(五)查找和加载外部依赖库
find_package()
是 CMake 中用于 查找和加载外部依赖库(第三方库或系统库) 的核心命令之一。它帮助你自动检测系统中是否安装了某个库,并设置好相应的变量(如头文件路径、库路径、版本号等),以便后续使用这些库进行编译和链接。
一、find_package
的作用
简单来说,它的主要功能是:
- 查找指定的库是否存在于系统中
- 设置相关变量:
<PackageName>_FOUND
: 是否找到该库<PackageName>_INCLUDE_DIRS
或<PackageName>_INCLUDE_DIR
: 头文件路径<PackageName>_LIBRARIES
或<PackageName>_LIBRARY
: 库文件路径<PackageName>_VERSION
: 版本信息(如果支持)
- 自动导入目标(现代用法):可以直接
target_link_libraries(my_target PRIVATE Some::Library)
二、基本语法
find_package(<PackageName> [version] [EXACT] [QUIET] [MODULE]
[REQUIRED] [[COMPONENTS] [components...]]
[OPTIONAL_COMPONENTS components...]
[NO_POLICY_SCOPE])
参数说明:
参数 | 含义 |
---|---|
<PackageName> | 要查找的包名,比如 Threads , OpenCV , CUDA , Protobuf 等 |
[version] | 可选,指定最低版本要求 |
EXACT | 可选,要求精确匹配版本 |
QUIET | 可选,禁止输出错误信息 |
MODULE | 可选,强制使用模块模式(Module mode)而不是配置模式(Config mode) |
REQUIRED | 可选,如果找不到包则报错并停止构建 |
COMPONENTS | 可选,指定需要查找的子组件(适用于有多个组件的库,如 Qt5Core, Qt5Gui 等) |
三、典型使用示例
示例 1:查找线程库(Threads)
find_package(Threads REQUIRED)
target_link_libraries(my_target PRIVATE Threads::Threads)
find_package(Threads REQUIRED)
:查找线程库Threads::Threads
:现代 CMake 推荐的导入方式,表示找到的线程库目标
示例 2:查找 CUDA
find_package(CUDA 11.7 REQUIRED)
if (CUDA_FOUND)
message(STATUS "Found CUDA ${CUDA_VERSION}")
endif()
示例 3:查找 OpenCV 并链接
find_package(OpenCV REQUIRED COMPONENTS core imgproc highgui)
include_directories(${OpenCV_INCLUDE_DIRS})
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE ${OpenCV_LIBS})
示例 4:查找可选库(非必须)
find_package(ZLIB QUIET)
if (ZLIB_FOUND)
target_compile_definitions(my_target PRIVATE HAVE_ZLIB)
target_link_libraries(my_target PRIVATE ZLIB::ZLIB)
else()
message(WARNING "ZLIB not found, some features will be disabled.")
endif()
四、两种查找模式
CMake 支持两种查找机制:
1. Module Mode(模块模式)
- 使用内置的
Find<PackageName>.cmake
文件来查找库 - 通常用于标准库或常见第三方库(如 Threads、OpenGL、SDL 等)
- 示例:
FindOpenCV.cmake
2. Config Mode(配置模式)
- 使用库自带的
<PackageName>Config.cmake
文件 - 更现代、更灵活的方式,推荐优先使用
- 一般在你安装了某个库之后会自动生成这些文件(如通过
make install
或vcpkg
,conan
安装)
你可以通过环境变量 CMAKE_PREFIX_PATH
来告诉 CMake 去哪些路径下查找这些配置文件。
五、如何查看 find_package 找到了什么?
你可以在 CMakeLists.txt
中加入以下代码来打印相关信息:
message(STATUS "<PackageName>_FOUND: ${PackageName}_FOUND")
message(STATUS "<PackageName>_INCLUDE_DIRS: ${PackageName}_INCLUDE_DIRS")
message(STATUS "<PackageName>_LIBRARIES: ${PackageName}_LIBRARIES")
message(STATUS "<PackageName>_VERSION: ${PackageName}_VERSION")
或者在运行 cmake 时加上 --debug-find
(某些版本支持)查看详细查找过程。
六、一些常见的可以 find_package 的库
包名 | 用途 |
---|---|
Threads | 多线程支持 |
CUDA | NVIDIA GPU 编程支持 |
OpenMP | OpenMP 并行编程支持 |
OpenCV | 计算机视觉库 |
Eigen3 | C++ 矩阵运算库 |
Boost | C++ 扩展库 |
Python3 | Python 支持 |
PkgConfig | 使用 pkg-config 查找其他库 |
Doxygen | 文档生成工具 |
Git | Git 工具支持 |
(六)配置文件configure_file
configure_file
是 CMake 中的一个命令,用于在构建过程的配置阶段生成新的文件或更新现有文件。这个命令通常用来处理模板文件,比如将一些项目配置或者版本信息嵌入到源代码中,或者根据构建环境的不同生成不同的配置文件。*.in
模板文件一般是用于在 cmake 执行阶段(准确来说,应该是配置阶段)将其嵌入的 CMake 变量、列表展开,并生成目标文件的过程中的。这一步通常是由 configure_file()
完成,会在【06】变量参与 C++ 的编译 中进行介绍。*.in
的后缀是约定俗成的,当然也可以使用其他形式的后缀。
基本用法
configure_file(<input> <output>
[NO_SOURCE_PERMISSIONS | FILE_PERMISSIONS <permissions>...]
[COPYONLY] [ESCAPE_QUOTES] [@ONLY])
<input>
: 模板文件的位置。可以使用相对路径或绝对路径。<input>
文件中可以包含 CMake 变量,它们会在执行configure_file
时被实际值替换。<output>
: 目标文件的位置和名称。如果目标文件已经存在,则会被覆盖。
示例
假设你有一个名为 config.h.in
的输入文件,内容如下:
#define PROJECT_SOURCE_DIR "@CMAKE_SOURCE_DIR@"
#define PROJECT_VERSION "@PROJECT_VERSION@"
然后,在你的 CMakeLists.txt
文件中,你可以这样使用 configure_file
:
cmake_minimum_required(VERSION 3.10)
project(MyProject VERSION 1.0)
configure_file(
${CMAKE_CURRENT_SOURCE_DIR}/config.h.in
${CMAKE_CURRENT_BINARY_DIR}/config.h
)
在这个例子中,@CMAKE_SOURCE_DIR@
和 @PROJECT_VERSION@
这样的标记会被对应的 CMake 变量值所替换,并生成一个新的 config.h
文件到构建目录下。
参数解释
NO_SOURCE_PERMISSIONS
: 不复制原始文件的权限设置给输出文件,默认情况下会复制。FILE_PERMISSIONS
: 设置输出文件的权限。COPYONLY
: 如果指定了此选项,configure_file
将不会替换任何变量,只会简单地复制文件。ESCAPE_QUOTES
: 转义输出中的引号(对于某些特定情况可能需要)。@ONLY
: 仅替换以@
开头和结尾的变量,忽略${}
格式的变量引用。
configure_file
是一个非常有用的工具,尤其是在你需要基于构建环境动态生成配置文件的时候。通过它,你可以轻松实现从简单的文本替换到复杂的配置文件生成等多种任务。
举例说明1:在代码中使用
背景
假设你正在开发一个C++项目,并希望在编译时根据项目的设置动态生成配置头文件(如 config.h
),以便于在代码中访问一些构建时确定的信息,比如项目版本号、源码目录等。
步骤 1: 创建模板文件
首先,在你的项目目录下创建一个名为 config.h.in
的模板文件。这个文件将包含一些占位符,这些占位符会在构建时被实际值替换。
例如,config.h.in
文件内容如下:
#pragma once
#define PROJECT_SOURCE_DIR "@CMAKE_SOURCE_DIR@"
#define PROJECT_VERSION "@PROJECT_VERSION@"
这里的 @CMAKE_SOURCE_DIR@
和 @PROJECT_VERSION@
是占位符,它们会被 CMake 中定义的实际值替换。
步骤 2: 修改 CMakeLists.txt
接下来,你需要修改或者创建 CMakeLists.txt
文件,告诉 CMake 在构建过程中如何处理这个模板文件。
cmake_minimum_required(VERSION 3.10)
# 定义项目名称和版本
project(MyProject VERSION 1.0)
# 使用 configure_file 命令处理模板文件
configure_file(
${CMAKE_CURRENT_SOURCE_DIR}/config.h.in
${CMAKE_CURRENT_BINARY_DIR}/config.h
)
这里我们使用了 configure_file
命令来指定输入文件(config.h.in
)和输出文件(config.h
)。${CMAKE_CURRENT_SOURCE_DIR}
和 ${CMAKE_CURRENT_BINARY_DIR}
分别表示源代码目录和构建目录。
步骤 3: 构建项目
现在,当你运行 CMake 来构建项目时,它会自动处理 config.h.in
文件,并生成一个新的 config.h
文件到构建目录下。
例如,如果你的源代码目录结构如下:
MyProject/
├── CMakeLists.txt
└── config.h.in
并且你在一个单独的构建目录中执行 CMake:
mkdir build
cd build
cmake ..
CMake 将会在 build/
目录下生成 config.h
文件,其内容类似于:
#pragma once
#define PROJECT_SOURCE_DIR "/path/to/your/source/directory"
#define PROJECT_VERSION "1.0"
这样,你就可以在代码中包含这个自动生成的 config.h
文件,并访问其中定义的宏了:
#include "config.h"
void printConfig() {
std::cout << "Source directory: " << PROJECT_SOURCE_DIR << "\n";
std::cout << "Version: " << PROJECT_VERSION << "\n";
}
通过这种方式,你可以轻松地在构建时生成包含特定信息的配置文件,这在大型项目中特别有用。
举例说明2:控制子目录编译和链接库设置
在 config.h.in
文件中使用 #cmakedefine USE_MYMATH
,然后根据 CMake 中是否定义了某个变量(例如 USE_MYMATH
),来决定生成的 config.h
文件中是否启用这个宏定义。
场景说明
你想控制是否启用某个功能模块(比如自定义数学库 mymath
),通过 CMake 配置时决定是否定义 USE_MYMATH
宏,并在源代码中通过预处理指令判断:
1. 创建 config.h.in
文件
内容如下:
#pragma once
#cmakedefine USE_MYMATH
⚠️ 注意:
#cmakedefine
是 CMake 的特殊语法。- 如果 CMake 中设置了
USE_MYMATH
变量(为ON
、TRUE
或非空值),则会在生成的config.h
中变成#define USE_MYMATH
。- 否则,这行会被注释掉或直接省略。
2. 修改 CMakeLists.txt
假设你有一个选项让用户选择是否启用 USE_MYMATH
:
cmake_minimum_required(VERSION 3.10)
project(MyProject VERSION 1.0)
# 定义 USE_MYMATH 开关,默认为 ON
option(USE_MYMATH "Use the custom math library" ON)
# 配置 config.h 头文件
configure_file(
${CMAKE_CURRENT_SOURCE_DIR}/config.h.in
${CMAKE_CURRENT_BINARY_DIR}/config.h
)
# 设置包含路径(如果需要让源码中 #include "config.h")
include_directories(${CMAKE_CURRENT_BINARY_DIR})
# 如果启用了 USE_MYMATH,则处理 math 子模块
if(USE_MYMATH)
include_directories("${PROJECT_SOURCE_DIR}/math")
add_subdirectory(math)
set(EXTRA_LIBS mathfun)
else()
unset(EXTRA_LIBS) # 可选:避免变量污染
endif()
# 添加主程序
add_executable(myapp main.cpp)
# 链接额外库(如果有)
if(DEFINED EXTRA_LIBS)
target_link_libraries(myapp PRIVATE ${EXTRA_LIBS})
endif()
option()
创建了一个可配置的开关,默认是ON
。configure_file()
根据这个变量是否存在,来决定是否在config.h
中写入#define USE_MYMATH
。
3. 在代码中使用这个宏
比如在 main.cpp
中:
#include "config.h"
#include <iostream>
int main() {
#ifdef USE_MYMATH
std::cout << "Using my custom math library." << std::endl;
#else
std::cout << "Using standard math functions." << std::endl;
#endif
return 0;
}
4. 构建项目并测试
默认构建(USE_MYMATH=ON)
mkdir build
cd build
cmake ..
make
./myapp
输出:
Using my custom math library.
关闭 USE_MYMATH
cmake .. -DUSE_MYMATH=OFF
make
./myapp
输出:
Using standard math functions.
查看生成的 config.h 内容
你可以查看构建目录下的 config.h
文件内容:
- 如果启用了
USE_MYMATH
,内容是:
#pragma once
#define USE_MYMATH
- 如果没有启用,则是:
#pragma once
/* #undef USE_MYMATH */
或者干脆不出现这一行(取决于 CMake 版本)。
(七) CMAKE 宏
在 CMake 中,宏(macro
) 是一种用于定义可重用代码块的机制。它允许你将一组 CMake 命令封装成一个“宏”,然后在多个地方调用,提高脚本的复用性和可维护性。
虽然 function()
在现代 CMake 中更推荐使用(因为它有更好的作用域控制),但理解 macro()
的工作方式对于阅读和维护旧项目、或者编写一些简单的构建逻辑仍然非常重要。
一、什么是宏(macro)
定义
macro(<name> [arg1 [arg2 [...]]])
# 一系列 CMake 命令
endmacro()
<name>
:宏的名字。[arg1 [arg2 [...]]]
:宏的参数列表,是可选的。
调用方式
直接写宏名并传入参数:
<name>(arg1 arg2 ...)
二、宏的作用
1. 代码复用
将重复使用的命令逻辑封装起来,在多个地方调用,减少冗余代码。
2. 简化配置逻辑
比如统一处理某些平台相关设置、编译器标志、依赖库查找等。
3. 快速原型开发
在调试或快速搭建阶段,宏可以快速封装一组命令,不需要考虑作用域问题。
三、宏的特性(重点!)
特性 1:宏没有作用域隔离
这是宏最重要的特性,也是最容易出错的地方。
- 宏内部定义的变量(如通过
set()
设置)都是全局变量。 - 宏执行完毕后,这些变量会留在当前作用域中,可能会影响后续代码。
示例:
macro(define_var)
set(MY_VAR "Inside macro")
endmacro()
define_var()
message(STATUS "MY_VAR = ${MY_VAR}")
输出:
-- MY_VAR = Inside macro
说明:宏内定义的 MY_VAR
是全局可见的。
特性 2:宏是文本替换
CMake 在解析宏时,并不是像函数那样创建一个新的作用域,而是进行文本级别的替换。
这意味着:
- 所有宏中的变量都会被替换为实际传入的值。
- 如果你传入的是变量名,宏内部对该变量的操作会直接影响该变量的值。
示例:
macro(set_value var value)
set(${var} ${value})
endmacro()
set(VALUE 0)
set_value(VALUE 42)
message(STATUS "VALUE = ${VALUE}")
输出:
-- VALUE = 42
说明:宏修改了外部变量 VALUE
的值。
特性 3:宏支持参数传递
宏可以接收任意数量的参数,参数通过 ${ARGV0}
, ${ARGV1}
等访问,也可以通过 ${ARGN}
获取所有参数。
示例:
macro(print_args)
message(STATUS "Number of arguments: ${ARGC}")
foreach(i RANGE ${ARGC})
message(STATUS "Arg ${i}: ${ARGV${i}}")
endforeach()
endmacro()
print_args(Hello World 123)
输出:
-- Number of arguments: 3
-- Arg 0: Hello
-- Arg 1: World
-- Arg 2: 123
四、宏的实际应用场景
虽然现代 CMake 推荐使用 function()
来替代 macro()
,但在以下场景中宏仍然非常有用:
1. 批量操作变量
macro(add_prefix prefix list)
set(tmp_list)
foreach(item IN LISTS ${list})
list(APPEND tmp_list "${prefix}_${item}")
endforeach()
set(${list} ${tmp_list})
endmacro()
set(MY_LIST A B C)
add_prefix("TEST" MY_LIST)
message(STATUS "MY_LIST = ${MY_LIST}")
输出:
-- MY_LIST = TEST_A;TEST_B;TEST_C
2. 统一添加编译选项
macro(add_compile_options_for_debug)
add_compile_options($<$<CONFIG:Debug>:-Wall -Wextra>)
endmacro()
add_compile_options_for_debug()
3. 生成源文件或配置头文件
macro(generate_config_header config_file output_dir)
configure_file(
${config_file}
${output_dir}/config.h
)
endmacro()
generate_config_header("${PROJECT_SOURCE_DIR}/config.h.in" "${PROJECT_BINARY_DIR}")
五、宏的缺点与注意事项
缺点 | 说明 |
---|---|
没有局部变量 | 宏内的所有变量都是全局的,容易造成命名冲突或副作用 |
难以调试 | 由于是文本替换,宏展开后的代码不易追踪和调试 |
影响可读性 | 过度使用宏会使 CMakeLists.txt 难以理解和维护 |
不推荐用于复杂逻辑 | 对于需要状态管理、返回值、错误处理等复杂逻辑,应使用 function() |
(八) CMAKE 函数
在 CMake 中,函数(function
) 是一种用于定义可重用代码块的机制。与宏(macro
)不同,函数提供了更好的封装性和作用域管理,使得代码更清晰、易于维护,并减少了意外副作用的风险。接下来,我们将详细探讨 CMake 函数的作用和用法。
一、什么是函数(function
)
✅ 定义
function(<name> [arg1 [arg2 [...]]])
# 一系列 CMake 命令
endfunction()
<name>
:函数的名字。[arg1 [arg2 [...]]]
:函数的参数列表,是可选的。
调用方式
直接写函数名并传入参数:
<name>(arg1 arg2 ...)
二、函数的作用
1. 提高代码复用性
通过将重复使用的命令逻辑封装成函数,可以在多个地方调用,减少冗余代码。
2. 增强模块化
函数可以用来组织复杂的构建逻辑,使得 CMakeLists.txt
文件更加清晰易读。
3. 提供作用域隔离
与宏不同,函数内部的变量默认是局部的,避免了意外修改外部变量的问题。
4. 支持返回值传递
虽然 CMake 函数不能像其他编程语言那样直接返回值,但可以通过参数引用(如设置父作用域中的变量)来实现类似的效果。
三、函数的特性
特性 1:有局部变量
函数内部的所有变量默认都是局部的,除非使用 set(... PARENT_SCOPE)
或 set(... CACHE ...)
显式声明为全局或缓存变量。
示例:
function(set_value var value)
set(${var} ${value}) # 局部作用域
set(${var} ${value} PARENT_SCOPE) # 修改父作用域
endfunction()
set(MY_VAR "Initial Value")
set_value(MY_VAR "Modified by function")
message(STATUS "MY_VAR = ${MY_VAR}") # 输出 Modified by function
说明:通过 PARENT_SCOPE
可以修改外部作用域中的变量。
特性 2:良好的作用域控制
函数创建了一个新的作用域,所有在函数内部定义的变量都不会影响外部同名变量,除非显式地使用 PARENT_SCOPE
或 CACHE
。
示例:
function(test_scope)
set(VAR_A "Inside Function")
message(STATUS "VAR_A inside function: ${VAR_A}")
endfunction()
set(VAR_A "Outside Function")
test_scope()
message(STATUS "VAR_A after function: ${VAR_A}") # 输出 Outside Function
说明:函数内部的 VAR_A
不会影响外部的 VAR_A
。
特性 3:支持参数传递
函数可以接收任意数量的参数,参数通过 ${ARGV0}
, ${ARGV1}
等访问,也可以通过 ${ARGN}
获取所有剩余参数。
示例:
function(print_args)
message(STATUS "Number of arguments: ${ARGC}")
foreach(i RANGE ${ARGC})
message(STATUS "Arg ${i}: ${ARGV${i}}")
endforeach()
endfunction()
print_args(Hello World 123)
输出:
-- Number of arguments: 3
-- Arg 0: Hello
-- Arg 1: World
-- Arg 2: 123
特性 4:不支持直接返回值
CMake 函数没有直接返回值的概念,但可以通过修改外部变量或使用 set(... PARENT_SCOPE)
来间接实现返回值的效果。
示例:
function(add_numbers a b result_var)
math(EXPR result "${a} + ${b}")
set(${result_var} ${result} PARENT_SCOPE)
endfunction()
add_numbers(5 3 RESULT)
message(STATUS "RESULT = ${RESULT}") # 输出 RESULT = 8
四、函数的实际应用场景
1. 统一处理编译选项
function(set_common_compile_options target)
target_compile_options(${target} PRIVATE
-Wall
-Wextra
-pedantic
)
endfunction()
add_executable(myapp main.cpp)
set_common_compile_options(myapp)
2. 自动化库生成
function(create_library target_name source_dir)
file(GLOB_RECURSE SRC_FILES "${source_dir}/*.cpp")
add_library(${target_name} STATIC ${SRC_FILES})
target_include_directories(${target_name} PUBLIC ${source_dir})
endfunction()
create_library(mathlib "${PROJECT_SOURCE_DIR}/math")
3. 简化依赖管理
function(find_and_link_dependency dep_name include_dirs lib_dirs)
find_package(${dep_name} REQUIRED)
if (${dep_name}_FOUND)
include_directories(${include_dirs})
link_directories(${lib_dirs})
target_link_libraries(mainapp ${${dep_name}_LIBRARIES})
endif()
endfunction()
find_and_link_dependency(MyDependency "/path/to/include" "/path/to/lib")
五、注意事项
注意事项 | 描述 |
---|---|
作用域管理 | 函数内的变量默认是局部的,除非使用 PARENT_SCOPE 或 CACHE 显式声明为全局或缓存变量。 |
参数传递 | 参数按位置传递,可以使用 ${ARGV} 和 ${ARGN} 访问所有参数。 |
调试困难 | 当函数嵌套较深时,调试可能会变得复杂,建议合理划分函数职责。 |
函数 vs 宏
特性 | 函数 (function) | 宏 (macro) |
---|---|---|
作用域 | 局部作用域,默认不影响外部变量 | 全局作用域,容易产生副作用 |
变量处理 | 支持局部变量,需要显式修改外部变量 | 简单文本替换,所有变量均为全局 |
适用场景 | 复杂逻辑、状态管理、需要作用域隔离 | 简单的任务、快速原型开发 |
(九) CMAKE TEST
示例项目:两数之和 + CTest 测试(不使用 GoogleTest)
项目结构
two_sum/
├── CMakeLists.txt
├── main.cpp
├── add.cpp
├── add.hpp
└── tests/
└── test_add.cpp
文件内容
add.hpp
- 函数声明
// add.hpp
#ifndef ADD_HPP
#define ADD_HPP
int add(int a, int b);
#endif // ADD_HPP
add.cpp
- 函数实现
// add.cpp
#include "add.hpp"
int add(int a, int b) {
return a + b;
}
main.cpp
- 主程序入口
// main.cpp
#include <iostream>
#include "add.hpp"
int main() {
int result = add(3, 4);
std::cout << "3 + 4 = " << result << std::endl;
return 0;
}
tests/test_add.cpp
- 自定义单元测试程序(不依赖任何测试框架)
// tests/test_add.cpp
#include "../add.hpp"
#include <iostream>
int main() {
int failed = 0;
if (add(2, 3) != 5) {
std::cerr << "[FAIL] add(2, 3) should be 5\n";
failed++;
}
if (add(-1, -1) != -2) {
std::cerr << "[FAIL] add(-1, -1) should be -2\n";
failed++;
}
if (add(0, 0) != 0) {
std::cerr << "[FAIL] add(0, 0) should be 0\n";
failed++;
}
if (add(-1, 1) != 0) {
std::cerr << "[FAIL] add(-1, 1) should be 0\n";
failed++;
}
if (failed == 0) {
std::cout << "[SUCCESS] All tests passed.\n";
return 0; // 成功
} else {
std::cerr << "[ERROR] Some tests failed.\n";
return 1; // 失败
}
}
CMakeLists.txt
- 配置项目 + 构建 + 注册 CTest 测试
# CMakeLists.txt
cmake_minimum_required(VERSION 3.14)
project(TwoSumProject)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 添加主程序
add_executable(two_sum main.cpp add.cpp)
# 启用测试支持
enable_testing()
# 添加测试子目录
add_subdirectory(tests)
tests/CMakeLists.txt
- 构建测试程序并注册为 CTest 测试
# tests/CMakeLists.txt
# 构建测试程序
add_executable(run_tests test_add.cpp)
# 注册为 CTest 测试
add_test(NAME AddFunctionTests COMMAND run_tests)
如何编译和运行?
构建项目:
mkdir build && cd build
cmake ..
make
运行主程序:
./two_sum
输出:
3 + 4 = 7
运行测试(CTest):
ctest
输出示例:
Test project /path/to/two_sum/build
Start 1: AddFunctionTests
1/1 Test #1: AddFunctionTests ................... Passed 0.01 sec
100% tests passed, 0 tests failed out of 1
如果某个测试失败,你会看到类似这样的信息:
[ERROR] Some tests failed.
...
Some tests failed!
- 如果你只是想做一个小型项目或快速验证函数逻辑,这种方式非常轻量。
- 对于中大型项目或团队协作,推荐使用成熟的测试框架(如 GoogleTest、Catch2 等),因为它们提供了更丰富的断言、测试组织、报告等功能。
(十) CMAKE 配置 Debug 版本
在 CMake 中配置 Debug 版本主要涉及到设置 CMAKE_BUILD_TYPE
变量为 Debug
,这会告诉 CMake 使用适合调试的编译器选项来构建项目。下面将详细介绍如何在 CMake 项目中配置 Debug 版本,以及这样配置的具体作用。
如何在 CMake 中配置 Debug 版本
1. 基本步骤
-
创建构建目录:建议为不同的构建类型(如 Debug 和 Release)创建独立的构建目录。
-
指定构建类型:通过
-DCMAKE_BUILD_TYPE=Debug
参数指定构建类型为 Debug。 -
生成构建系统并编译:使用 CMake 命令生成构建系统文件(如 Makefile),然后进行编译。
示例流程:
# 创建并进入 Debug 构建目录
mkdir build_debug && cd build_debug
# 使用 CMake 配置项目,并指定构建类型为 Debug
cmake .. -DCMAKE_BUILD_TYPE=Debug
# 编译项目
make
2. 自定义 CMakeLists.txt 文件
在你的 CMakeLists.txt
文件中,你可以根据构建类型应用不同的编译选项。现代 CMake 推荐使用目标相关的命令来设置这些选项,而不是直接修改全局的 CMAKE_CXX_FLAGS_*
变量。
以下是一个示例 CMakeLists.txt
文件的部分内容,展示了如何基于构建类型设置不同的编译选项:
cmake_minimum_required(VERSION 3.14)
project(TwoSumProject)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 查找 GoogleTest
find_package(GTest REQUIRED)
include_directories(${GTEST_INCLUDE_DIRS})
# 添加可执行文件
add_executable(two_sum main.cpp add.cpp)
# 根据构建类型设置不同的编译选项
target_compile_options(two_sum PRIVATE
$<$<CONFIG:DEBUG>:-g -O0> # 调试信息和禁用优化
$<$<CONFIG:RELEASE>:-O3> # 启用优化
)
# 启用测试支持
enable_testing()
# 添加测试子目录
add_subdirectory(tests)
在这个例子中,当构建类型是 Debug
时,编译器将会添加调试信息(-g
)并且不启用任何优化(-O0
)。而在 Release
构建类型下,则启用了高级别的优化(-O3
)。
这样配置的作用
1. 包含调试信息
当 CMAKE_BUILD_TYPE
设置为 Debug
时,通常会加上 -g
编译选项,这会在生成的二进制文件中包含调试符号。这些符号对于调试工具(如 GDB)来说是非常重要的,因为它们允许你查看源代码级别的变量值、调用栈等信息,从而更容易地定位问题。
2. 禁用或限制编译器优化
默认情况下,Debug
构建不会启用优化(例如 -O0
),这意味着编译器不会尝试对代码进行任何形式的优化。这有助于保持源代码与生成的机器码之间的直接对应关系,使得调试更加直观。相反,在 Release
模式下,通常会启用较高的优化级别(如 -O3
),以提高程序的执行效率和减少二进制文件的大小。
3. 启用额外的安全检查
一些编译器在 Debug
模式下会自动启用额外的安全检查,比如数组越界检查、未初始化变量检测等。虽然这些检查可能会稍微降低性能,但它们可以帮助捕捉潜在的问题,尤其是在开发阶段。
4. 更易于调试
由于上述原因,特别是包含了调试信息和禁用了优化,使得在 Debug
版本上进行调试变得更为简单。你可以轻松地设置断点、单步执行代码、查看变量值及其变化过程,这对于快速发现和修复错误至关重要。
5. 内存布局一致性
在 Debug
模式下,内存分配模式可能与 Release
模式有所不同,例如可能会分配更多的空间用于边界检查或堆管理,这有助于发现内存相关的问题,如缓冲区溢出或非法访问。
(十一) CMAKE 打包
CPack 是 CMake 提供的一个强大的工具,用于打包和分发你的软件项目。它支持多种不同的包格式,包括但不限于:
- 二进制包:如
.zip
,.tar.gz
- 安装包:如
.deb
(Debian/Ubuntu),.rpm
(Red Hat/Fedora),.nsis
(Windows installer) - 源代码包:如
.tar.gz
使用 CPack,你可以轻松地为不同的操作系统和平台创建合适的安装包,而无需手动编写复杂的脚本或命令。
CPack 的作用
- 简化打包过程:通过简单的配置即可生成适用于不同平台的安装包。
- 支持多种格式:可以根据目标平台的需求选择合适的打包格式。
- 集成于 CMake 流程:作为 CMake 生态系统的一部分,能够无缝集成到现有的构建流程中。
基本用法
要在你的 CMake 项目中使用 CPack,通常需要在 CMakeLists.txt
文件中添加一些特定的 CPack 设置,并在构建完成后调用 CPack 来生成包。
1. 准备 CMakeLists.txt
首先,在你的 CMakeLists.txt
中添加 CPack 相关的设置。这里是一个简单的例子:
cmake_minimum_required(VERSION 3.14)
project(MyProject)
# 添加你的源文件和可执行文件等
add_executable(myapp main.cpp)
# 设置 CPACK 变量
set(CPACK_PACKAGE_NAME "MyProject")
set(CPACK_PACKAGE_VERSION "1.0.0")
set(CPACK_GENERATOR "ZIP") # 或者其他的生成器,如 DEB, RPM, NSIS 等
# 包含 CPack 模块
include(CPack)
在这个例子中,我们设置了包的基本信息(名称和版本),并指定了要使用的生成器(这里选择了 ZIP)。你还可以根据需要添加更多配置,例如描述、维护者信息、依赖关系等。
2. 生成包
完成上述设置后,你可以按照正常的 CMake 流程进行构建:
mkdir build && cd build
cmake ..
make
之后,运行 CPack 来生成包:
cpack
这将基于你在 CMakeLists.txt
中指定的设置生成一个或多个包文件。
3. 选择不同的生成器
如果你想要生成不同类型的包,可以通过 -G
参数指定不同的生成器。例如,生成一个 Debian 包:
cpack -G DEB
或者生成一个 Windows 安装程序:
cpack -G NSIS
4. 高级配置
CPack 提供了大量的变量和选项,允许你对打包过程进行高度定制。例如:
-
设置包的描述信息:
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A brief description of my project")
-
设置维护者信息:
set(CPACK_PACKAGE_CONTACT "your.email@example.com")
-
设置版权信息:
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE.txt")
-
设置依赖关系(对于 DEB 和 RPM 包):
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libsomething (>= 1.0)")
这些只是 CPack 功能的一小部分。具体可以参考 CPack文档 获取更详细的信息。
原创作者: aslanvon 转载于: https://www.cnblogs.com/aslanvon/p/18920022