CMake 配置构建
流程
1 2 3 4 5 6
| # 创建 build 文件夹并进行配置(configure) cmake -B build # 根据生成的构建工具进行构建(build)(统一了不同平台的不同构建工具的命令) cmake --build build -j4 # 安装(可以类比 make install, 构建 install 目标) sudo cmake --build build --target install
|

配置阶段的参数
-G 指定生成器,CMake 可以生成不同类型的构建系统(比如 Makefile MSBuild,所以可以跨平台) cmake -B build -G "Unix Makefiles"

-D 指定配置变量,配置后会保存在build/CMakeCache.txt中,下次配置仍会保留之前设置的值(删除缓存信息可以只删除CMakeCache.txt而非整个build)。
1 2 3
| cmake -B build -DCMAKE_INSTALL_PREFIX=/opt/myapp # 设置安装路径 cmake -B build -DCMAKE_BUILD_TYPE=Release # 设置构建模式为发布模式 cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON # 设置生成 compile_commands.json 文件
|
CMake 文件
模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| cmake_minimum_required(VERSION 3.15)set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) project(prj LANGUAGES C CXX) if (PROJECT_BINARY_DIR STREQUAL PROJECT_SOURCE_DIR) message(WARNING "The binary directory of CMake cannot be the same as source directory!") endif() if (NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Release) endif() if (WIN32) add_definitions(-DNOMINMAX -D_USE_MATH_DEFINES) endif() if (NOT MSVC) find_program(CCACHE_PROGRAM ccache) if (CCACHE_PROGRAM) message(STATUS "Found CCache: ${CCACHE_PROGRAM}") set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ${CCACHE_PROGRAM}) set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK ${CCACHE_PROGRAM}) endif() endif()
|
生成可执行程序
1 2 3 4 5 6 7 8 9 10
| add_executable(main main.cpp hello.cpp)# 先指定可执行程序,后添加 add_executable(main) target_sources(main PUBLIC main.cpp hello.cpp) # 使用 GLOB 根据扩展名批量查找,替换成 GLOB_RECURSE 则会包含所有子文件夹中的匹配,CONFIGURE_DEPENDS 保证增减文件后自动更新变量 add_executable(main) file(GLOB sources CONFIGURE_DEPENDS *.cpp *.h) target_sources(main PUBLIC ${sources})
|
生成库
1 2 3 4 5 6 7 8
| # 静态库 add_library(mylib STATIC mylib.cpp)# 动态库 add_library(mylib SHARED mylib.cpp) # OBJ库 add_library(mylib OBJECT mylib.cpp)
|
项目配置变量
CMAKE_BUILD_TYPE: 构建类型
Debug 调试模式,生成调试信息
Release 发布模式,优化程度最高
MinSizeRel 最小体积发布,生成的文件比 Release 更小
RelWithDebInfo 带调试信息发布
与 project 相关的变量 project(helloprj)
PROJECT_SOURCE_DIR 若无project,向上一级找,找到最近的调用project的 CMakeLists.txt 所在的源码目录;也就是找到字意的项目目录,从子模块里直接获得项目最外层目录的路径。
CMAKE_CURRENT_SOURCE_DIR 当前 CMakeLists.txt 所在的源码目录。
CMAKE_SOURCE_DIR 最外层 CMakeLists.txt 的源码根目录,不建议使用,若项目作为别人的子项目则会直接代表调用项目的根目录。
PROJECT_BINARY_DIR 与PROJECT_SOURCE_DIR对应,是二进制产物路径。
CMAKE_BINARY_DIR 与CMAKE_SOURCE_DIR对应,是二进制产物路径。
PROJECT_NAME 当前项目名
CMAKE_PROJECT_NAME 根项目项目名

C++ 一些要求的配置
1 2 3
| set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) # 设置是否启用 GCC 特有的功能,关闭以兼容其他编译器
|
target的相关描述
target的一些属性也有相应的全局变量,改变全局变量相当于改变了各个属性的初始默认值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| # 设置target的属性 add_executable(main main.cpp) set_property(TARGET main PROPERTY CXX_STANDARD 17) # 设置 C++ 标准 set_property(TARGET main PROPERTY CXX_STANDARD_REQUIRED ON) # 编译器不支持则报错 set_property(TARGET main PROPERTY WIN32_EXECUTABLE ON) # 在 Windows 系统中,运行时不启动控制台窗口 set_property(TARGET main PROPERTY LINK_WHAT_YOU_USE ON) # 告诉编译器不要自动剔除没有引用符号的链接库 set_property(TARGET main PROPERTY LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/lib) # 设置动态链接库的输出路径 set_property(TARGET main PROPERTY ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/lib) # 设置静态链接库的输出路径 set_property(TARGET main PROPERTY RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin) # 设置可执行文件的输出路径# 批量设置 add_executable(main main.cpp) set_target_properties(main PROPERTIES CXX_STANDARD 17 # 设置 C++ 标准 CXX_STANDARD_REQUIRED ON # 编译器不支持则报错 WIN32_EXECUTABLE ON # 在 Windows 系统中,运行时不启动控制台窗口 LINK_WHAT_YOU_USE ON # 告诉编译器不要自动剔除没有引用符号的链接库 LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/lib # 设置动态链接库的输出路径 ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/lib # 设置静态链接库的输出路径 RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin # 设置可执行文件的输出路径 ) # 设置相应的全局变量 set(CMAKE_CXX_STANDARD 17) # 设置 C++ 标准 set(CMAKE_CXX_STANDARD_REQUIRED ON) # 编译器不支持则报错 set(CMAKE_WIN32_EXECUTABLE ON) # 在 Windows 系统中,运行时不启动控制台窗口 set(CMAKE_LINK_WHAT_YOU_USE ON) # 告诉编译器不要自动剔除没有引用符号的链接库 set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/lib) # 设置动态链接库的输出路径 set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/lib) # 设置静态链接库的输出路径 set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin) # 设置可执行文件的输出路径 add_executable(main main.cpp)
|
使用针对target的选项(头文件搜索目录等),避免添加到所有target
1 2 3 4 5 6 7 8 9 10 11
| target_sources(myapp PUBLIC hello.cpp other.cpp) # 添加源文件 target_include_directories(myapp PUBLIC include) # 添加头文件搜索目录 target_link_libraries(myapp PUBLIC hellolib) # 添加链接库 target_add_definitions(myapp PUBLIC -DMY_MACRO=1) # 添加宏定义 MY_MACRO=1 target_compile_options(myapp PUBLIC -fopenmp) # 添加编译选项# 避免使用 include_directories(include) # 添加头文件搜索目录 link_directories(/opt/cuda) # 添加链接库搜索目录 add_definitions(MY_MACRO=1) # 添加宏定义 MY_MACRO=1 add_compile_options(-fopenmp) # 添加编译选项
|
第三方库引入方法
作为纯头文件引入 target_include_directories
适用于那些只有头文件的库,例如一些轻量级的模板库。这些库不需要编译,因为它们的实现代码都在头文件中,通常是通过模板或者宏等方式实现功能。比如C++的标准模板库就是纯头文件。
以fmt库为例,该库介绍说明可以通过纯头文件引入,此时只需要项目中的include文件夹。

按照要求,在纯头文件引入时需要定义FMT_HEADER_ONLY,此时项目结构如下:

在CMakeLists.txt中通过target_include_directories引入第三方库头文件目录。
1 2 3 4 5 6 7
| cmake_minimum_required(VERSION 3.30) project(prj)set(CMAKE_CXX_STANDARD 20) add_executable(prj main.cpp) target_include_directories(prj PUBLIC include)
|
但是直接引入头文件,函数实现在头文件里,没有提前编译,每次需要重复编译同样的内容,编译时间长。
作为子模块引入 add_subdirectory
这种方式将第三方库的源代码直接包含到项目中,第三方库通常有自己的CMakeLists.txt文件,通过add_subdirectory指令,可以将这个库的构建过程集成到主项目的构建过程中。
以fmt库为例,这个开源库可以直接将该项目作为用户项目的子项目引入,直接clone源码,目录结构如下:

在CMakeLists.txt中通过add_subdirectory引入第三方库的项目子目录,再通过target_link_libraries链接第三方项目库。
1 2 3 4 5 6 7 8 9
| cmake_minimum_required(VERSION 3.30) project(prj)set(CMAKE_CXX_STANDARD 20) add_subdirectory(fmt) add_executable(prj main.cpp) target_link_libraries(prj fmt::fmt)
|
FetchContent
FetchContent 是 CMake 的一个模块,可以在配置阶段获取外部依赖库,允许配置步骤使用这些内容进行 add_subdirectory()、include() 或 file() 操作。
这与上文所述基本相同,都是直接将第三方项目引入,但通过 FetchContent 可以直接将依赖项目写在 CMakeLists.txt 中,在配置阶段从远程库中下载依赖项目,而无需手动下载。
FetchContent_Declare() 函数用于指定如何获取外部项目,比如仓库地址等。
1 2 3 4 5 6 7 8
| FetchContent_Declare( <name> <contentOptions>... [EXCLUDE_FROM_ALL] [SYSTEM] [OVERRIDE_FIND_PACKAGE | FIND_PACKAGE_ARGS args...] )
|
- FetchContent_MakeAvailable
FetchContent_MakeAvailable命令确保依赖项已经被获取。在获取时,它还会将它们添加到主构建中,以便主构建可以使用这些项目的目标等。
1
| FetchContent_MakeAvailable(googletest)
|
具体使用步骤:
包含FetchContent模块
声明外部项目
1 2 3 4 5
| FetchContent_Declare( fmt GIT_REPOSITORY https://github.com/fmtlib/fmt.git GIT_TAG 11.1.1 )
|
这一步声明了一个外部项目fmt,并指定了其下载和配置的详细信息(此时并不会立即下载或配置项目)。
确保外部项目可用
1
| FetchContent_MakeAvailable(fmt)
|
这一步确保声明的外部项目fmt已经被下载、配置、构建,并且可以使用。
链接第三方库
1
| target_link_libraries(prj PRIVATE fmt::fmt)
|
这一步使用 target_link_libraries 将第三方库 fmt 链接到项目中。
使用该种途径时,项目如下,可以发现build文件夹中的_deps文件夹存放了获取的第三方项目,在main.cpp中可以直接使用。

在CMakeLists.txt中通过FetchContent引入第三方项目,再通过target_link_libraries链接第三方项目库。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| cmake_minimum_required(VERSION 3.30) project(prj)set(CMAKE_CXX_STANDARD 20) include(FetchContent) FetchContent_Declare(fmt GIT_REPOSITORY https://github.com/fmtlib/fmt.git GIT_TAG 11.1.1) FetchContent_MakeAvailable(fmt) add_executable(prj main.cpp) target_link_libraries(prj PUBLIC fmt::fmt)
|
引用系统中安装的第三方库 find_package
在存在菱形依赖的情况下,即项目A依赖于B和C,而B和C又同时依赖于D,使用子模块引用(add_subdirectory),可能会导致D被定义两遍,从而引发错误。
而通过find_package使用系统预安装的库则可以有效避免这个问题。当使用find_package查找库时,CMake会记录已经找到的库。因此,即使多个模块依赖同一个库,find_package也只会引入一次。例如,当找到库B和D时,再找C时不会将D重复引入。
不同操作系统可以通过各自的包管理器来安装所需的库。以Ubuntu为例,可以使用apt包管理器来安装库。比如安装fmt库:
1
| sudo apt install libfmt-dev
|
此时由于头文件等已经在系统查找路径中(比如/usr/include),可以直接在文件中导入相关的头文件,此时,项目结构如下:

在CMakeLists.txt中则需要先find_package找到fmt包,再通过target_link_libraries链接第三方项目库。
1 2 3 4 5 6 7 8 9
| cmake_minimum_required(VERSION 3.30) project(prj)set(CMAKE_CXX_STANDARD 20) find_package(fmt) add_executable(prj main.cpp) target_link_libraries(prj fmt::fmt)
|
在CMake中,一个项目可以包含多个库。CMake允许一个包(package)提供多个库,这些库也被称为组件(components)。因此,在使用target_link_libraries指令链接库时,应采用包名::组件名的格式。
例如,在上文中提到的fmt::fmt,其中fmt是包名,第二个fmt是该包提供的一个组件名。再比如,TBB这个包,就包含了tbb、tbbmalloc和tbbmalloc_proxy这三个组件。当需要链接这些组件时,可以分别使用TBB::tbb、TBB::tbbmalloc和TBB::tbbmalloc_proxy。
find_package时可以指定必要的组件:
1 2
| find_package(TBB REQUIRED COMPONENTS tbb tbbmalloc REQUIRED) target_link_libraries(myexec PUBLIC TBB::tbb TBB::tbbmalloc)
|
CMake 项目结构
一个典型的 C++ 项目可以采用以下结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| prj ├── CMakeLists.txt ├── cmake │ └── MyFuncs.cmake ├── subprj1 │ ├── CMakeLists.txt │ ├── include │ │ └── subprj1 │ │ └── Animal.h │ └── src │ └── Animal.cpp ├── ... └── mainprj ├── CMakeLists.txt ├── include │ └── mainprj │ └── utils.h └── src └── main.cpp
|
在这个结构中,项目根目录包含了一个 CMakeLists.txt 文件以及多个子项目文件夹。
一个子项目作为可执行文件,负责与用户交互,其他子项目则作为不同的库文件,编写实际的业务逻辑。可执行文件仅作为入口,所有的功能实现都在库文件中,这样的分离使得代码逻辑的库也能被其他软件组合和复用。
另外,根项目还可能包含其他文件夹,比如上面的cmake文件夹用于存放CMake配置脚本或工具函数。
每个子项目的组织格式为:
subprj/CMakeLists.txt
subprj/include/subprj/module.h
subprj/src/module.cpp
根项目 CMakeLists.txt
在根项目中的 CMakeLists.txt 中,我们进行基本的 C++ 版本设置等选项,并使用 project 命令初始化项目。
之后,通过 add_subdirectory 将子项目逐一添加到根项目中,这样根项目就能够调用子项目中的 CMakeLists.txt 文件。
1 2 3 4 5 6 7 8 9 10 11 12 13
| cmake_minimum_required(VERSION 3.30)set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake;${CMAKE_MODULE_PATH}") project(prj LANGUAGES CXX) include(MyFuncs) add_subdirectory(subprj1) add_subdirectory(mainprj)
|
子项目 CMakeLists.txt
根项目的 CMakeLists.txt 主要负责全局配置,而子项目中的 CMakeLists.txt 则只关注该子项目自身的设置,如头文件目录、需要链接的库等。
子项目中,通常会使用 add_library 或 add_executable 来生成target,并配置target的选项,如链接的库和包含的头文件等。
在链接库时,由于 PUBLIC 的传播作用,某项目链接了其他项目的库后,也可以自动包含相应的头文件。
1 2 3 4
| file(GLOB_RECURSE srcs CONFIGURE_DEPENDS src/*.cpp include/*.h) add_executable(mainprj ${srcs}) target_include_directories(mainprj PUBLIC include) target_link_libraries(mainprj PUBLIC subprj1)
|
在上面的例子中,mainprj 是一个可执行子项目,它通过 add_executable 生成可执行文件,并链接了其他子项目 subprj1。
在子项目中,可以使用GLOB_RECRUSE来获取文件夹中所有的.h文件和.cpp文件,只是编译的话只需要.cpp文件,将.h也写出可以使头文件也被纳入IDE的项目资源浏览器,比如在头文件中引用头文件也可以使用<>写法搜索得到。
在上面的例子中,mainprj 是一个可执行子项目,它通过 add_executable 生成可执行文件,并链接了其他子项目 subprj1。
在子项目中,可以使用 GLOB_RECURSE 来获取文件夹中所有的 .cpp 文件和 .h 文件。虽然编译时只需要 .cpp 文件,但将 .h 文件也一并列出可以使头文件被纳入 IDE 的项目资源浏览器。比如在头文件中引用其他头文件时,也可以使用 <> 写法,可以直接跳转到目标头文件。
子项目头文件
子项目头文件的例子如下,每个头文件使用#pragma once,防止重复导入;之后将代码使用namespace subprj{}包裹,这样如果两个子库有相同标识符在使用时也不会出现冲突。
如果没有#pragma once,在头文件中定义了一个类,在实现文件中重复导入两次则会造成重复定义的编译错误,#pragma once可以保证一个编译单元中不会因为某个头文件出现重复定义。
1 2 3
| // 如果下面的头文件没有防重复导入,则会出现错误 # include <subprj1/Animal.h> # include <subprj1/Animal.h>
|
而如果没有namespace,我在两个子库中都一个同名的类,如果某个文件同时导入这两个头文件,也会有重复定义的编译错误;
如果有相同的函数名,引入两个头文件,编译该单元时没有错误,但要链接时无法确定是哪一个函数实现,出现链接错误。
所以将每个子项目使用不同的namespace先进行包裹,“把自己先圈起来”,防止和未知的头文件或库发生冲突,namespace相当于延长了标识符,人为地划定模块,防止标识符冲突。
1 2 3
| // 虽然是不同的头文件,但有相同的类标识符,一起复制过来的话就会重复定义,使用namespace即可隔离 # include "subprj1/example.h" # include "subprj2/example.h"
|
另外,如果要在头文件中写函数定义,需要使用inline或static修饰,它们可以使函数定义限制在该编译单元里,防止不同项目都引入函数定义后,链接时出现重复定义。
虽然在头文件中会有namespace,但解决不了这个问题,因为每个导入该头文件的都是原封不动的将头文件复制过来,相当于这个函数的全名(namespace::func)在多个文件里都被编译了一遍,那多个编译单元链接时,这个函数名字还是有多个意思(定义),从而造成冲突。
1 2 3 4 5
| // 如果没有修饰,两个cpp编译后进行链接时,utils中的函数就会有多个定义造成冲突 // a.cpp # include <subprj1/utils.h> // b.cpp # include <subprj1/utils.h>
|
从上面可以总结的是:在同一个编译单元中,可以有同名的声明(函数声明、类声明、变量声明),但定义都只能有一个(多次导入同一头文件造成的类冲突;导入不同头文件造成的类冲突)。
在多个编译单元中,依旧可以有同名的声明,复制于同一个头文件的不同编译单元的类定义是可行的(每个编译单元中会有自己的类副本),但函数定义和变量定义不行,但函数和变量的定义只能有一个副本(使用static和inline)。
一个典型的头文件如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| // prj/subprj1/include/subprj1/Animal.h #pragma oncenamespace subprj1 { struct Animal { virtual void speak() const = 0; virtual ~Animal() = default; }; struct Dog final : Animal { void speak() const override; }; struct Cat final : Animal { void speak() const override; }; }
|
子项目源文件
一般源文件和相应的头文件成对出现,在源文件中,include相应的头文件,并在namespace中进行头文件的实现。
1 2 3 4 5 6 7 8 9 10 11 12 13
| // prj/subprj1/src/Animal.cpp # include <subprj1/Animal.h> # include <iostream>namespace subprj1 { void Cat::speak() const { std::cout << "Cat::speak" << std::endl; } void Dog::speak() const { std::cout << "Dog::speak" << std::endl; } }
|
每个cpp文件是一个编译单元,一般在写新功能时,会新建一对头文件和源文件,视为一个模块。
上面提到的namespace可以为每个模块搞成一块命名空间,但一般将每个子项目作为分隔不同命名空间的尺度就可以,项目内需要人为的保证每个小模块不会发生重复定义。
如果一个模块的头文件中仅仅声明了其他模块中的类,而没有直接使用或解引用该类的成员(例如调用成员函数或访问成员变量),那么头文件中不需要包含该类对应的头文件,而只需提供一个前向声明。例如,可以使用 struct ClassName; 或 class ClassName; 来声明类。只有在实际需要使用该类成员的实现文件(如 .cpp 文件)中,才需要包含完整的头文件。
1 2 3 4 5 6 7 8 9 10
| // Animal类在其他头文件定义,但该头文件中无需引用 // 我们只需要声明一下Animal是一个`struct`而不是一个函数之类的,因为此处并没有解引用Animal类 #pragma oncenamespace subprj1 { struct Animal; struct Another { void use(Animal *a) const; }; }
|
cmake/ 文件夹
与 C/C++ 中的 #include 类似,CMake 也有一个 include 命令。使用 include(XXX) 时,CMake 会在 CMAKE_MODULE_PATH 列表中的所有路径下查找名为 XXX.cmake 的文件。
通过这种方式,可以将一些常用的函数、宏或变量写在独立的 XXX.cmake 文件中,存放在项目的 cmake/ 文件夹中。然后在需要使用这些功能的地方,通过 include 引入相应的 .cmake 文件,从而实现代码的复用和模块化管理。
前面的根项目CMakeLists.txt中,以下部分就是设置CMAKE_MODULE_PATH以及include相应的.cmake。
1 2
| set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake;${CMAKE_MODULE_PATH}") include(MyFuncs)
|
比如说在.cmake文件中写一些常用的函数或宏等:
1 2 3 4 5 6 7 8 9 10 11
| # 用法:my_add_target(prj EXECUTABLE) # 通过该宏可以直接简化子项目生成target的CMake代码 macro (my_add_target name type) file(GLOB_RECURSE srcs CONFIGURE_DEPENDS src/*.cpp src/*.h) if ("${type}" MATCHES "EXECUTABLE") add_executable(${name} ${srcs}) else() add_library(${name} ${type} ${srcs}) endif() target_include_directories(${name} PUBLIC include) endmacro()
|
macro 和 function:
- **
macro**:相当于将代码直接粘贴到调用者的位置。
- **
function**:创建了一个闭包,它优先访问定义者的作用域。
include 和 add_subdirectory
- **
include**:相当于将代码直接粘贴到调用者的作用域中。
- **
add_subdirectory**:会在子目录中创建一个新的作用域。
可以类比 C++ 中的#define和函数。
参考:
- 小彭老师的并行课
- CMake Tutorial
- FetchContent