CMake简单使用教程

CMake介绍

CMake 是一个用于管理源代码构建的工具,是平台无关的中间编译工具。同一个CMake编译规则在不同系统平台构建出不同的可执行构建文件。

CMake 广泛用于 C 和 C++ 语言,但它可能用于也可以构建其他语言的源代码。

CMake下载安装

下载

CMake官网下载地址:https://cmake.org/download/

根据自己需要版本进行下载

安装

双击.msi或直接解压.zip安装即可;

安装路径名建议为英文路径;

CMake 和 MinGW 安装好后,要确保环境变量path中存在cmake下bin的所在路径(直接解压安装不会自动添加路径)。

检查安装是否成功

管理员cmd窗口执行:cmake -version

image-20240130094530215

CMake基础

基本语法

  • 注释 使用井号(#)开头的行是注释行,会被 CMake 忽略。
1
# 这是一个注释
  • 变量 在 CMake 中,你可以使用 set() 命令定义变量:
1
set(VARIABLE_NAME value)
  • 读取变量的值时,使用 ${VARIABLE_NAME} 进行引用:
1
2
set(SOURCE_FILES main.cpp)
message("Source files: ${SOURCE_FILES}") # 输出:Source files: main.cpp
  • 控制结构 CMake 提供了类似于其他编程语言的控制结构,如条件语句、循环语句等。条件语句:
1
2
3
4
5
6
7
if(CONDITION)
# ...
elseif(OTHER_CONDITION)
# ...
else()
# ...
endif()
  • 循环语句:
1
2
3
foreach(item IN LISTS some_list)
# ...
endforeach()
  • 函数和宏 你可以定义自己的函数和宏,它们有类似的语法。函数:
1
2
3
function(FUNCTION_NAME arg1 arg2)
# ...
endfunction()
  • 宏:
1
2
3
macro(MACRO_NAME arg1 arg2)
# ...
endmacro()

常用命令

以下是一些常用的 CMake 命令:

  1. 设置 CMake 最低版本要求
1
2
# 设置 CMake 最低版本要求
cmake_minimum_required(VERSION 3.8)

cmake_minimum_required 指令用于指定项目所需的最低 CMake 版本。这个指令确保当前环境中的 CMake 版本满足项目的构建要求。如果 CMake 的版本低于指定的最低版本,CMake 会报错并终止构建过程。这个指令的主要作用是确保项目所使用的 CMake 功能和语法与当前 CMake 版本兼容。随着 CMake 的发展,有时会引入新功能、改进现有功能或者废弃旧功能。如果用户尝试使用 CMake 3.8 以下的版本来构建项目,将会收到错误消息。

  1. 定义项目名称和版本
1
2
# 定义项目名称和版本
project(MyApp VERSION 1.0.0 LANGUAGES CXX)

project指令先是定义了项目名称,再使用VERSION关键字并跟随具体版本号1.0.0,来指定当前项目版本,此处很好理解。而LANGUAGES关键字以及后面的参数值CXX,则代表C++语言,CMake 会根据当前操作系统、可用编译器和指定的编程语言自动选择合适的编译器。CMake 支持多种编译器,如 GCC、Clang、MSVC等。CMake 在选择编译器时会遵循一定的规则和优先级,匹配规则包括:

Unix-like 系统(如 Linux 和 macOS):对于 C++ 代码,默认情况下,CMake 会首先尝试使用系统上可用的 GCC 编译器。如果没有找到 GCC,CMake 会继续尝试查找其他可用的编译器,如 Clang。可以通过设置 CMAKE_CXX_COMPILER 变量来手动指定编译器。

Windows 系统:对于 C++ 代码,默认情况下,CMake 会尝试使用 MSVC作为编译器。如果没有找到MSVC,CMake 会继续尝试查找其他可用的编译器,如 MinGW 或 Clang。与 Unix-like 系统类似,也可以通过设置 CMAKE_CXX_COMPILER 变量来手动指定编译器。

1
set(CMAKE_CXX_COMPILER "/path/to/your/compiler")

某些情况下,CMake 可能无法自动检测到合适的编译器,或者需要使用特定版本的编译器,可以通过设置CMAKE_CXX_COMPILER 变量来实现。

  1. 设置 C++ 标准
1
2
3
4
# 设置 C++ 标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

这三行 CMake 指令用于设置 C++ 项目的编译选项。

set(CMAKE_CXX_STANDARD 17):这一行指令设置了项目使用的 C++ 标准版本。在这个例子中,我们选择了 C++17 标准。CMake 支持设置多种 C++ 标准版本,如 C++11、C++14、C++17、C++20 等。可以根据项目的需求,选择合适的 C++ 标准版本。

set(CMAKE_CXX_STANDARD_REQUIRED ON):这一行指令表示,如果编译器不支持指定的 C++ 标准,CMake 将报错并终止构建过程。如果将此选项设置为 OFF,则 CMake 会尽量使用所选 C++ 标准版本进行编译,但如果编译器不支持该版本,CMake 会自动降级到编译器支持的最接近的 C++ 标准版本。

set(CMAKE_CXX_EXTENSIONS OFF):这一行指令用于禁用编译器特定的 C++ 语言扩展。将此选项设置为 OFF 可确保项目遵循 C++ 标准,并具有更好的可移植性。如果将此选项设置为 ON,则 CMake 允许编译器使用其特定的 C++ 语言扩展,这可能导致项目在不同编译器之间的行为不一致。

  1. 定义用户可配置的选项
1
2
3
4
5
6
7
# 定义用户可配置的选项
option(ENABLE_DEBUG "Enable debug output" ON)

if(ENABLE_DEBUG)
#生成的工程中加入预处理器宏定义DEBUG_OUTPUT
add_definitions(-DDEBUG_OUTPUT)
endif()

option(ENABLE_DEBUG "Enable debug output" ON):此命令定义了一个名为 ENABLE_DEBUG 的用户可配置选项。option() 命令用于定义一个布尔型变量,可以在 CMake 生成构建系统时进行配置。命令的第二个参数是对可配置选项的描述。这个描述可以帮助其他开发者或用户理解这个选项的用途。当使用 CMake GUI 工具时,这个描述将作为提示显示在选项旁边。这个描述在命令行模式下不会出现。在这个例子中,ENABLE_DEBUG 的默认值为 ON。用户可以通过 CMake 命令行参数或 GUI 工具来改变这个选项的值。

1
cmake -D ENABLE_DEBUG=OFF ..

if(ENABLE_DEBUG)endif():这两个命令定义了一个条件语句。如果 ENABLE_DEBUG 选项为 ON,则条件为真,执行语句块中的命令。否则,不执行这些命令。

add_definitions(-DDEBUG_OUTPUT):此命令仅在 ENABLE_DEBUGON 时执行。add_definitions() 命令用于添加编译器定义。在这个例子中,-DDEBUG_OUTPUT 添加了一个名为 DEBUG_OUTPUT 的预处理器宏定义(是的,-D后面直接连接宏定义名,不需要加空格)。这个宏定义可以在源代码中使用,以便根据其值启用或禁用调试输出功能。例如,在 C++ 代码中,可以使用 #ifdef DEBUG_OUTPUT#endif 来包裹调试输出相关的代码。

1
2
3
4
#ifdef DEBUG_OUTPUT
//只在debug模式下运行的逻辑
std::cout << "debug mode" << std::endl;
#endif

​ 5. 自定义宏:添加 MSVC 常用编译选项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 自定义宏:添加 MSVC 常用编译选项
macro(add_msvc_options target)
if(MSVC)
target_compile_options(${target} PRIVATE
/W4 # 设置警告级别为 4
/WX # 将警告视为错误
/MP # 启用多处理器编译
/permissive- # 禁用不严格的语言 conformance
/Zc:__cplusplus # 启用正确的 __cplusplus 宏值
/Zc:inline # 移除未使用的函数
/Gm- # 禁用最小生成(minimal rebuild)
/EHsc # 指定异常处理模型
)
endif()
endmacro()

macro(add_msvc_options target) 定义了一个名为 add_msvc_options 的宏。在 CMake 中,宏用于封装一组命令,以便在多个地方重复使用。而add_msvc_options 宏接收一个参数 target。在宏内部,${target} 会被替换为实际传递的目标名称。

target_compile_options() 命令用于为特定的目标(如可执行文件或库)添加编译选项。其中的${target}是一个变量,表示调用宏时传递的项目名。而PRIVATE关键字表示这些编译选项只对当前目标生效。在这个例子中,编译选项仅影响 ${target} 的构建。如果有其他目标依赖于 ${target},这些编译选项不会传递给那些依赖目标。

  1. 链接静态库和动态库
1
2
3
4
5
6
7
8
# 链接静态库
find_library(STATIC_LIB libStatic.lib PATHS "${CMAKE_SOURCE_DIR}/libs/static")
target_link_libraries(MyApp PRIVATE ${STATIC_LIB})

# 链接动态库
find_library(DYNAMIC_LIB libDynamic.dll PATHS "${CMAKE_SOURCE_DIR}/libs/dynamic")
find_library(DYNAMIC_LIB_IMPORT libDynamic.lib PATHS "${CMAKE_SOURCE_DIR}/libs/dynamic")
target_link_libraries(MyApp PRIVATE ${DYNAMIC_LIB_IMPORT})

find_library() 命令用于在指定的路径中查找库文件。在这个例子中链接静态库的部分,它查找名为 libStatic.lib 的静态库,并将其路径存储在变量 STATIC_LIB 中。PATHS 参数用于指定搜索库文件的目录,这里设置为 "${CMAKE_SOURCE_DIR}/libs/static"

target_link_libraries() 命令用于将库链接到指定的目标。这里将 STATIC_LIB 链接到 MyApp 目标。PRIVATE 关键字表示这个库仅对当前目标(MyApp)可见,不会传递给其他依赖于 MyApp 的目标。

至于在链接动态库时,为什么使用 ${DYNAMIC_LIB_IMPORT} 而不是 ${DYNAMIC_LIB},原因是在 Windows 平台上,当链接到动态库时,需要链接到相应的导入库(.lib 文件),而不是直接链接到动态库(.dll 文件)。导入库包含了调用动态库函数所需的信息,编译器和链接器需要这些信息来正确生成可执行文件。运行时,可执行文件会自动加载相应的 .dll 文件。

  1. 修改链接标志,使用延迟加载机制
1
2
# 使用 Windows 的 DLL delay-load 机制
set_target_properties(MyApp PROPERTIES LINK_FLAGS "/DELAYLOAD:libDynamic.dll")

这个命令是用于设置目标(在这个例子中是 MyApp)的属性。set_target_properties() 命令允许你修改一个目标的一些属性,例如链接标志、输出名称等。在这个例子中,我们修改了 MyApp 的链接标志。具体来说,LINK_FLAGS 属性表示要传递给链接器的标志。在这里,我们将 /DELAYLOAD:libDynamic.dll 添加到链接标志中。这个标志用于指示链接器启用 DLL 延迟加载机制。

延迟加载机制允许程序在运行时按需加载 DLL,而不是在程序启动时立即加载。这可以降低程序启动时间,并且在某些情况下可以避免因缺少 DLL 导致的程序启动失败。当程序首次调用 DLL 中的函数时,系统会自动加载 DLL。/DELAYLOAD:libDynamic.dll 标志告诉链接器,我们希望在运行时延迟加载 libDynamic.dll。当程序需要使用 libDynamic.dll 中的函数时,才会加载这个 DLL。

  1. 根据目标架构定制编译选项和链接选项
1
2
3
4
5
6
7
8
9
10
# 根据目标架构定制编译选项和链接选项
if(CMAKE_GENERATOR_PLATFORM STREQUAL "Win32")
message("Building for Win32 (x86) architecture")
target_compile_options(MyApp PRIVATE /arch:SSE2)
elseif(CMAKE_GENERATOR_PLATFORM STREQUAL "x64")
message("Building for x64 architecture")
target_compile_options(MyApp PRIVATE /arch:AVX2)
else()
message(WARNING "Unknown architecture")
endif()

这段代码的目的是根据目标架构定制编译选项和链接选项。具体来说,它根据 CMAKE_GENERATOR_PLATFORM 变量的值来判断目标架构,并针对不同架构设置不同的编译选项。记得我们在输入cmake命令行时的-A参数吗?这里即是设置了这个变量的值。

1
cmake -G "Visual Studio 16 2019" -A Win32 ..

if(CMAKE_GENERATOR_PLATFORM STREQUAL "Win32")这个条件检查 CMAKE_GENERATOR_PLATFORM变量是否等于 “Win32”。如果是,说明我们正在为 Win32 (x86) 架构构建项目。target_compile_options(MyApp PRIVATE /arch:SSE2)告诉编译器为 x86 架构使用 SSE2 指令集。

1
cmake -G "Visual Studio 16 2019" -A x64 ..

若通过上面的CMake命令构建x64任务,则会走到elseif分支中,其中target_compile_options(MyApp PRIVATE /arch:AVX2)命令告诉编译器为 x64 架构使用 AVX2 指令集。

  1. 添加子项目
1
2
# 添加子项目
add_subdirectory(subproject)

使用 add_subdirectory 指令时,CMake 会在指定的子目录中查找 CMakeLists.txt 文件,并执行其中的命令,让 CMake 构建系统继续构建子项目。这使得你可以将一个大型项目分解为多个较小的子项目,从而使项目的组织结构更加清晰。

在我们的示例中,这个命令将导致 CMake 在构建过程中进入 subproject 目录,并执行 subproject/CMakeLists.txt 文件中的命令。子项目可以包含它自己的源文件、库、可执行文件等,并可以与主项目共享变量、目标和属性。

  1. 添加头文件目录
1
2
# 添加头文件目录
include_directories(includeDir)

将给定的目录添加到编译器(compiler)用于搜索包含文件的目录。相对路径被解释为相对于当前源目录。

  1. 添加头文件目录
1
2
# 添加头文件目录
include_directories(includeDir)

将给定的目录添加到编译器(compiler)用于搜索包含文件的目录。相对路径被解释为相对于当前源目录。

  1. 添加依赖项
1
2
# 添加对其它package消息的依赖,前提是已经通过find_package()引入了这个package
add_dependencies(my_target ${catkin_EXPORTED_TARGETS})

在项目中通常会遇见这样的情况:(例如一个项目中有:main,libhello.a, libworld.a),当项目过小的时候,编译顺序是*.a,然后是main,但是当一个项目的文件过于庞大,就会导致编译的顺序不会按照主CMAKE的add_subdirectory引入的先后顺序,为了解决这一问题,就需要使用add_dependencies进行依赖指定。

  1. 生成库文件
1
2
# 添加子项目
add_library(<name> [STATIC | SHARED | MODULE] [source1])

add_library()是用来添加一个库文件(静态库或动态库)的函数。它的作用是将一个或多个库文件添加到项目中,以供编译链接时使用。

  1. 生成可执行文件
1
2
# 生成可执行文件
add_executable(<name> [source1] [source2 ...])

使用指定的源文件来生成目标可执行文件。

name:可执行目标文件的名字,在一个cmake工程中,这个名字必须全局唯一;[source1] [source2 ...]:构建可执行目标文件所需要的源文件。

  1. 生成配置文件
1
2
# 在构建时生成配置文件
configure_file(config.h.in config.h @ONLY)

configure_file(config.h.in config.h @ONLY) 命令的实际意义在于根据模板文件 config.h.in 生成配置文件 config.h。在这个过程中,CMake 会将模板文件中的一些变量替换为其实际值。这对于生成项目中使用的配置文件非常有用,特别是当这些配置文件需要根据当前构建环境的某些属性进行调整时。

configure_file 命令的参数@ONLY表示只替换 @VARIABLE_NAME@ 形式的占位符。如果不使用这个选项,CMake 会尝试替换 ${VARIABLE_NAME} 形式的占位符。

举个例子,假设你有一个项目,其中的一些功能取决于编译时的选项。你可以使用 option() 命令定义这些选项,并使用 configure_file() 命令将这些选项的值写入一个配置文件。然后,在源代码中,你可以包含这个配置文件并根据选项值来启用或禁用某些功能。

config.h.in 文件内容示例:

1
#define ENABLE_FEATURE_X @ENABLE_FEATURE_X@

CMakeLists.txt 文件中,你可以使用以下命令为 ENABLE_FEATURE_X 定义一个可配置选项,并生成 config.h 文件:

1
2
option(ENABLE_FEATURE_X "Enable feature X" ON)
configure_file(config.h.in config.h @ONLY)

在这个例子中,当 ENABLE_FEATURE_X 选项被设置为 ON 时,生成的 config.h 文件将包含 #define ENABLE_FEATURE_X 1。当选项设置为 OFF 时,config.h 文件将包含 #define ENABLE_FEATURE_X 0。这样,源代码中就可以根据 ENABLE_FEATURE_X 的值来启用或禁用相关功能。

  1. 指定安装规则
1
2
3
# 指定安装规则
install(TARGETS MyApp RUNTIME DESTINATION bin)
install(FILES "${CMAKE_SOURCE_DIR}/libs/dynamic/libDynamic.dll" DESTINATION bin)

install 命令用于指定安装规则,它定义了在构建完成后如何将目标文件(可执行文件、库等)以及其他相关文件(如动态库、配置文件等)安装到指定的目录。这对于将构建好的项目打包成安装包或将项目部署到目标系统上非常有用。在我们的例子中,这两条 install 命令分别指定了将 MyApp 可执行文件和 libDynamic.dll 动态库安装到 bin 目录下。

以下是这两条 install 命令的详细解释:

  1. install(TARGETS MyApp RUNTIME DESTINATION bin)
  • TARGETS MyApp:指定要安装的目标,这里是 MyApp 可执行文件。
  • RUNTIME:表示我们要安装可执行文件的运行时组件。对于可执行文件,这通常指的就是可执行文件本身。
  • DESTINATION bin:指定安装目标的目录。这里,MyApp 可执行文件将被安装到 bin 目录下。
  1. install(FILES "${CMAKE_SOURCE_DIR}/libs/dynamic/libDynamic.dll" DESTINATION bin)
  • FILES:表示我们要安装的是文件,而不是目标。在这个例子中,我们要安装的文件是动态库 libDynamic.dll
  • "${CMAKE_SOURCE_DIR}/libs/dynamic/libDynamic.dll":指定要安装的文件的路径。${CMAKE_SOURCE_DIR} 是一个变量,表示项目的根目录。
  • DESTINATION bin:与第一个 install 命令相同,这里指定将 libDynamic.dll 安装到 bin 目录下。

CMake常用变量说明

在外部指令中使用 ${} 进行变量的引用。在 IF 等语句中,是直接使用变量名而不通过 ${} 取值。

  1. PROJECT_BINARY_DIR
    如果是in source编译,这个变量指得就是工程顶层目录,如果是out-of-source编译,指的是工程编译发生的目录。另外 _BINARY_DIR 和 CMAKE_BINARY_DIR 跟这个变量指代的内容是一致的。

  2. PROJECT_SOURCE _DIR
    不论采用何种编译方式,都是工程顶层目录。也就是在in source编译时,他跟PROJECT_BINARY_DIR等变量一致。另外_SOURCE_DIR和CMAKE_SOURCE_DIR跟这个变量指代的内容是一致的。

  3. CMAKE_CURRENT_SOURCE_DIR
    指的是当前处理的CMakeLists.txt所在的路径。

  4. CMAKE_CURRRENT_BINARY_DIR
    如果是in-source编译,它跟CMAKE_CURRENT_SOURCE_DIR一致,如果是out-ofsource编译,他指的是 target 编译目录。使用ADD_SUBDIRECTORY(src bin)可以更改这个变量的值。

  5. CMAKE_CURRENT_LIST_FILE
    输出调用这个变量的CMakeLists.txt的完整路径。

  6. CMAKE_CURRENT_LIST_LINE
    输出这个变量所在的行。

  7. CMAKE_MODULE_PATH
    这个变量用来定义自己的 cmake 模块所在的路径。如果你的工程比较复杂,有可能会自己编写一些 cmake 模块,这些 cmake 模块是随你的工程发布的,为了让 cmake 在处理CMakeLists.txt时找到这些模块,你需要通过 SET 指令,将自己的 cmake 模块路径设置一下。比如
    SET(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)
    这时候你就可以通过 INCLUDE 指令来调用自己的模块了。

  8. EXECUTABLE_OUTPUT_PATH 和 LIBRARY_OUTPUT_PATH
    前者用来重新定义目标二进制可执行文件的存放位置,后者用来重新定义目标链接库文件的存放位置。

  9. PROJECT_NAME
    返回通过 PROJECT 指令定义的当前项目名称。

  10. CMAKE_PROJECT_NAME
    返回根项目的项目名称,比如B项目是A项目的子项目,在B项目中获取10. CMAKE_PROJECT_NAME得到的结果是A,获取PROJECT_NAME得到的结果是B。

CMake构建最小项目

最基本的项目是将一个源代码文件生成可执行文件。对于简单的项目,只需要一个三行的 CMakeLists.txt 文件即可,首先新建一个HelloWorld的目录,在其中编辑一个简单的HelloWorld.cpp 文件,如下所示:

1
2
3
4
5
6
7
8
#include <iostream>
using namespace std;

int main() {
cout << "HelloWorld" << endl;
system("pause");
return 0;
}

cmake_minimum_required 指定使用 CMake 的最低版本号,project 指定项目名称,add_executable 用来生成可执行文件,需要指定生成可执行文件的名称和相关源文件。

注意,此示例在 CMakeLists.txt 文件中使用小写命令。CMake 支持大写、小写和混合大小写命令。

1
2
3
4
5
6
7
8
# 指定使用 CMake 的最低版本号
cmake_minimum_required (VERSION 2.6)

# 指定项目名称
project (HelloWorld)

# 生成可执行文件(生成可执行文件的名称,相关源文件)
add_executable (HelloWorld HelloWorld.cpp)

构建、编译和运行

命令行方式

现在就可以构建和运行我们的项目了,就是先运行 cmake 命令来构建项目,然后使用你选择的编译工具进行编译。

在HelloWorld的目录中打开cmd,并创建一个构建目录 build(将cmake生成的文件统一放置在build下,放置原路径文件混乱),接下来,进入 build 目录并运行 CMake 来配置项目,并生成构建系统:

1
2
3
mkdir build
cd build
cmake -G"MinGW Makefiles" ..

构建系统是需要指定 CMakeLists.txt 所在路径,此时在 build 目录下,所以用 .. 表示 CMakeLists.txt 在上一级目录。

Windows 下,CMake 默认使用微软的 MSVC 作为编译器,我想使用 MinGW 编译器,可以通过 -G 参数来进行指定,只有第一次构建项目时需要指定,之后使用cmake .. 即可。

此时在 build 目录下会生成 Makefile 文件,然后调用编译器来实际编译和链接项目:

1
cmake --build .

--build 指定编译生成的文件存放目录,其中就包括可执行文件,. 表示存放到当前目录,

在 build 目录下生成了一个 HelloWorld.exe 可执行文件,试着执行它:

1
2
HelloWorld
请按任意键继续. . .

该程序输出了HelloWorld,从输出结果看已经得到了正确的结果。

此时目录结构为:

1
2
3
4
HelloWorld/
build/
CMakeLists.txt
tutorial.cpp

cmake-gui方式

在HelloWorld目录中,手动创建build目录。

image-20240130113833434

打开cmake-gui.exe

image-20240130114145101

点击Configure开始执行配置,选择运行平台

image-20240130134719188

点击 Generate,开始生成 VS 项目

image-20240130134838794

ALL_BUILD:该目标会导致工程中所有项目被构建,类似 Visual Studio 的 Build All 或者 make 的 make all命令

HelloWorld:项目本身,就是在CMakeLists.txt文件中配置的project(HelloWorld)

ZERO_CHECK:该项目会检查生成工程的 CMake 配置文件( CMakeLists.txt )是否更新,如更新将运行 CMake 重新生成工程文件,如果确信 CMakeLists.txt 不会被更新,或者希望手工运行 CMake 重新生成工程文件,可以在 CMakeLists.txt 配置文件中添加 set(CMAKE_SUPPRESS_REGENERATION FALSE) 命令, ZERO_CHECK 目标将不会生成

6) 编译,点击cmake-gui中下放的 Open Project 按钮或者直接双击 build 目录下的 HelloWorld.sln,即可编译

外部构建与内部构建

这里创建了一个 build 目录存放编译产物,可以避免编译产物与代码文件混在一起,这种叫做外部构建。

还有一种内部构建,即直接在项目根目录下进行构建系统与编译,这时构建和编译命令就更改为:

1
2
cmake -G"MinGW Makefiles" .
cmake --build .

内部构建会使得项目文件很混乱,一般直接用外部构建即可。

添加库

现在我们将向项目中添加一个库,这个库包含计算数字加法的实现,可执行文件使用这个库。

我们把库放在名为 MathFunctions 的子目录中。此目录包含头文件 MathFunctions.h 和源文件 MathFunctions.cpp。源文件有一个名为 MyAdd 的函数,MathFunctions.h 则是该函数的声明。

在 MathFunctions 目录下创建一个 CMakeLists.txt 文件,并添加以下一行:

1
2
# MathFunctions/CMakeLists.txt
add_library(MathFunctions MathFunctions.cpp)

表示添加一个叫 MathFunctions 的库文件。

MathFunctions.h

1
extern int MyAdd(int a, int b);

MathFunctions.cpp

1
2
3
4
5
#include "MathFunctions.h"

int MyAdd(int a, int b) {
return a + b;
}

CMake 中的 target 有可执行文件和库文件,分别使用 add_executableadd_library 命令生成,除了指定生成的可执行文件名/库文件名,还需要指定相关的源文件。

此时文件结构为:

1
2
3
4
5
6
7
8
HelloWorld/
build/
MathFunctions/
CMakeLists.txt
MathFunctions.h
MathFunctions.cpp
CMakeLists.txt
HelloWorld.cpp

为了使用 MathFunctions 这个库,我们将在顶级 CMakeLists.txt 文件中添加一个 add_subdirectory(MathFunctions) 命令指定库所在子目录,该子目录下应包含 CMakeLists.txt 文件和代码文件。

可执行文件要使用库文件,需要能够找到库文件和对应的头文件,可以分别通过 target_link_librariestarget_include_directories 来指定。

使用 target_link_libraries 将新的库文件添加到可执行文件中,使用 target_include_directories 将 MathFunctions 添加为头文件目录,添加到 HelloWorld目标上,以便 MathFunctions .h 可以被找到。

顶级 CMakeLists.txt 的最后几行如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
# add the MathFunctions library
add_subdirectory(MathFunctions)

# add the executable
add_executable(${PROJECT_NAME} HelloWorld.cpp)

target_link_libraries(${PROJECT_NAME} PUBLIC MathFunctions)

# add the binary tree to the search path for include files
# so that we will find TutorialConfig.h
target_include_directories(${PROJECT_NAME} PUBLIC
${PROJECT_SOURCE_DIR}/MathFunctions
)

MathFunctions 库就算添加完成了,接下来就是在主函数使用该库中的函数,先在 HelloWorld.cpp 文件中添加头文件:

1
#include "MathFunctions.h"

然后使用 mysqrt 函数即可:

1
2
3
4
int a = 900;
int b = 100;
std::cout << MyAdd(a, b);
return 0;

将库设置为可选项

现在将 MathFunctions 库设为可选的,虽然对于本教程来说,没有必要这样做,但对于较大的项目来说,这种情况很常见。

第一步是向顶级 CMakeLists.txt 文件添加一个选项。

1
option(USE_MYMATH "Use tutorial provided math implementation" ON)

option 表示提供用户可以选择的选项。命令格式为:option(<variable> "description [initial value])

USE_MYMATH 这个选项缺省值为 ON,用户可以更改这个值。此设置将存储在缓存中,以便用户不需要在每次构建项目时设置该值。

下一个更改是使 MathFunctions 库的构建和链接成为条件。于是创建一个 if 语句,该语句检查选项 USE_MYMATH 的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if(USE_MYMATH)
add_definitions(-DUSE_MYMATH)
add_subdirectory(MathFunctions)
list(APPEND EXTRA_LIBS MathFunctions)
list(APPEND EXTRA_INCLUDES ${PROJECT_SOURCE_DIR}/MathFunctions)
endif()

# add the executable
add_executable(${PROJECT_NAME} ${SRC_LIST})

target_link_libraries(${PROJECT_NAME} PUBLIC ${EXTRA_LIBS})

# add the binary tree to the search path for include files
# so that we will find TutorialConfig.h
target_include_directories(${PROJECT_NAME} PUBLIC
${PROJECT_BINARY_DIR}
${EXTRA_INCLUDES}
)

在 if 块中,有 add_subdirectory 命令和 list 命令,APPEND表示将元素MathFunctions追加到列表EXTRA_LIBS中,将元素 ${PROJECT_SOURCE_DIR}/MathFunctions 追加到列表EXTRA_INCLUDES中。EXTRA_LIBS 存储 MathFunctions 库,EXTRA_INCLUDES 存储 MathFunctions 头文件。

变量EXTRA_LIBS用来保存需要链接到可执行程序的可选库,变量EXTRA_INCLUDES用来保存可选的头文件搜索路径。这是处理可选组件的经典方法,我将在下一步介绍现代方法。

接下来对源代码的进行修改。首先,在 HelloWorld.cpp 中包含 MathFunctions.h 头文件:

1
2
3
#ifdef USE_MYMATH
#include "MathFunctions.h"
#endif

然后,还在 HelloWorld.cpp 中,使用 USE_MYMATH 选择使用哪个平方根函数:

1
2
3
4
5
#ifdef USE_MYMATH
std::cout << MyAdd(a, b);
#else
std::cout << "ERROR";
#endif

资料下载

本文中涉及到的资料

百度网盘下载链接

提取码:fy6b