CMake学习篇二:CMake 核心概念

💡 学习前提


1. CMakeLists.txt

📝 CMakeLists.txt 是什么:这是 CMake 的配置文件,描述了项目的构建规则。每个项目至少需要一个根目录的 CMakeLists.txt。

1.1 为什么需要 CMakeLists.txt?

在 Makefile 中,你需要手写每个文件的编译规则:

# Makefile 方式:繁琐
main.o: main.c
    gcc -c main.c -o main.o
add.o: add.c
    gcc -c add.c -o add.o

CMakeLists.txt 只需要告诉 CMake"我要编译什么",CMake 自动生成规则:

# CMake 方式:简洁
add_executable(main main.c add.c)

1.2 基本结构

# 1. 版本要求(确保 CMake 功能可用)
cmake_minimum_required(VERSION 3.10)

# 2. 项目定义(设置项目名称、版本、语言)
project(ProjectName VERSION 1.0.0 LANGUAGES CXX)

# 3. 编译选项(设置 C++ 标准等)
set(CMAKE_CXX_STANDARD 17)

# 4. 添加目标(可执行文件或库)
add_executable(app main.cpp)

# 5. 链接库(如果需要)
target_link_libraries(app PRIVATE some_library)

1.3 CMakeLists.txt 的执行流程

1. cmake .. (配置阶段)
   ↓
2. 读取 CMakeLists.txt
   ↓
3. 解析指令,设置变量
   ↓
4. 生成构建系统文件(Makefile/Ninja/VS项目)
   ↓
5. cmake --build . (构建阶段)
   ↓
6. 调用底层构建工具(make/ninja/msbuild)
   ↓
7. 编译源文件,生成目标
⚠️ 重要:配置阶段 vs 构建阶段

  • 配置阶段cmake ..):读取 CMakeLists.txt,生成构建文件
  • 构建阶段cmake --build .):调用编译器,实际编译代码
  • CMakeLists.txt 中的指令在配置阶段执行,不是在编译时执行

2. Target(目标)

⚠️ 重要:Target 是 CMake 的核心概念。Target 代表构建的产物(目标),是 CMake 管理的基本单元。理解 Target 是掌握现代 CMake 的关键。

2.1 为什么需要 Target?

在传统的 Makefile 中,你需要手动管理每个文件的编译规则和依赖关系:

# Makefile:手动管理依赖
libA.o: libA.c libA.h
    gcc -c libA.c -I./include

libB.o: libB.c libB.h libA.h
    gcc -c libB.c -I./include -I./libA

app: main.c libA.o libB.o
    gcc main.c libA.o libB.o -o app

这种方式的问题:

  • 依赖关系不清晰,容易遗漏
  • 编译选项分散,难以维护
  • 无法自动传递依赖信息

CMake 的 Target 是一个"智能对象",它通过属性(Properties)封装了构建所需的所有信息:

  • 需要哪些源文件
  • 依赖哪些库
  • 头文件在哪里
  • 需要什么编译选项
  • 需要定义哪些宏

创建 Target 后,我们通过 target_xxx 命令为它配置这些属性。这样 CMake 就能自动处理复杂的依赖关系,让构建配置更清晰、更易维护。

2.2 创建 Target

Target 可以是四种类型:

# 1. 可执行文件
add_executable(my_app main.cpp utils.cpp)

# 2. 静态库(.a 或 .lib)
add_library(my_static_lib STATIC lib.cpp)

# 3. 动态库(.so 或 .dll)
add_library(my_shared_lib SHARED lib.cpp)

# 4. 接口库(仅头文件,无编译产物)
add_library(my_header_lib INTERFACE)
📝 接口库的用途:接口库(INTERFACE)常用于:

  • 纯头文件库(header-only library)
  • 仅传递编译选项或依赖关系
  • 组织和管理一组相关的配置

2.3 配置 Target 的属性

创建 Target 后,需要为它配置各种属性。现代 CMake 使用 target_xxx 命令来配置:

add_library(mylib src/lib.cpp)

# 1. 设置头文件搜索路径
target_include_directories(mylib PUBLIC include/)

# 2. 链接其他库
target_link_libraries(mylib PUBLIC other_lib)

# 3. 设置编译选项
target_compile_options(mylib PRIVATE -Wall -Wextra)

# 4. 设置宏定义
target_compile_definitions(mylib PUBLIC VERSION=1.0)

# 5. 设置 C++ 标准(CMake 3.8+)
target_compile_features(mylib PUBLIC cxx_std_17)

CMake 为每个 Target 维护一套独立的属性

属性类型 存储内容 设置命令
INCLUDE_DIRECTORIES 头文件搜索路径 target_include_directories
LINK_LIBRARIES 链接的库 target_link_libraries
COMPILE_OPTIONS 编译选项(如 -Wall) target_compile_options
COMPILE_DEFINITIONS 宏定义(如 DEBUG=1) target_compile_definitions
COMPILE_FEATURES 语言特性(如 C++17) target_compile_features

2.4 Target 之间的依赖关系

Target 可以依赖其他 Target,形成依赖链:

# CMakeLists.txt
# 创建库 A
add_library(libA src/a.cpp)
target_include_directories(libA PUBLIC include/a)

# 创建库 B,依赖库 A
add_library(libB src/b.cpp)
target_include_directories(libB PUBLIC include/b)
target_link_libraries(libB PUBLIC libA)  # B 依赖 A

# 创建可执行文件,依赖库 B
add_executable(app main.cpp)
target_link_libraries(app PRIVATE libB)  # app 依赖 B

# 依赖链:app → libB → libA
# app 会自动获得 libA 和 libB 的 PUBLIC 属性

依赖传递的魔法

  • app 链接 libB 时,自动获得 libB 的 PUBLIC 属性
  • libB 的 PUBLIC 属性包括它依赖的 libA 的 PUBLIC 属性
  • 因此 app 无需显式链接 libA,也能访问 libA 的公共头文件
💡 Target 的优势

  • 自动依赖传递:链接 B 时自动获得 A 的 PUBLIC 属性
  • 清晰的依赖关系:一目了然谁依赖谁
  • 模块化:每个 Target 独立配置,互不干扰
  • 类型安全:CMake 会检查 Target 是否存在

2.5 作用域:PRIVATE vs PUBLIC vs INTERFACE

在上面的例子中,你可能注意到了 PUBLICPRIVATE 关键字。为什么需要它们?

2.5.1 为什么需要作用域?

问题场景

# CMakeLists.txt
add_library(mylib src/lib.cpp src/internal.cpp)

# 这个库有两类头文件:
# 1. include/mylib.h - 公共 API,用户需要
# 2. src/internal.h - 内部实现,用户不需要

# 如果不区分作用域:
target_include_directories(mylib PUBLIC include/ src/)

# 问题:链接 mylib 的用户也能看到 src/internal.h
# 这暴露了内部实现细节,破坏了封装性

解决方案:使用作用域关键字

# CMakeLists.txt
target_include_directories(mylib
    PUBLIC include/      # 公共接口,依赖者需要
    PRIVATE src/         # 内部实现,仅自己用
)

这样:

  • mylib 自己可以访问 include/src/
  • 链接 mylib 的用户只能访问 include/,看不到 src/

2.5.2 三种作用域的本质

关键字 当前 Target 使用 依赖者继承 典型场景
PRIVATE ✅ 是 ❌ 否 内部实现细节
PUBLIC ✅ 是 ✅ 是 对外接口的一部分
INTERFACE ❌ 否 ✅ 是 仅用于接口(如头文件库)

形象比喻

把 Target 想象成一个公司

  • PRIVATE:公司内部的管理制度和流程(员工知道,外部客户不知道)
  • PUBLIC:公司的产品和服务标准(员工要遵守,客户也能看到)
  • INTERFACE:公司对外的服务承诺(仅客户需要知道,内部可能不同实现)

2.5.3 target_include_directories 的底层原理

# CMakeLists.txt
target_include_directories(mylib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)

作用:给 mylib 这个 Target 添加头文件搜索路径-I 路径),告诉编译器去哪里找头文件。

底层发生了什么?

  1. CMake 在内部为 mylib 这个 Target 维护一个属性列表
  2. 把路径记录mylib Target 的 INCLUDE_DIRECTORIES 属性中
  3. 根据你写的是 PUBLIC、PRIVATE 还是 INTERFACE,决定这个路径是否要传播给依赖它的其他 Target

三种可见性的底层效果

可见性 含义 底层效果
PRIVATE 只有自己用 只加到 mylib 自己的编译命令中
PUBLIC 自己和别人都要用 加到 mylib 自己 + 所有链接 mylib 的 Target
INTERFACE 只有别人用(接口库常用) 只传播给链接它的 Target,自己不使用

实际编译命令

# CMakeLists.txt
add_library(mylib src/lib.cpp)
target_include_directories(mylib
    PUBLIC include/
    PRIVATE src/internal/
)

add_executable(app main.cpp)
target_link_libraries(app PRIVATE mylib)

生成的编译命令:

# 编译 mylib
g++ -c src/lib.cpp -I include/ -I src/internal/ -o lib.o

# 编译 app
g++ -c main.cpp -I include/ -o main.o  # 只有 PUBLIC 的 include/

2.5.4 target_link_libraries 的底层原理

# libB/CMakeLists.txt
target_link_libraries(libB PUBLIC libA)  # B 依赖 A

# app/CMakeLists.txt
target_link_libraries(app PRIVATE libB)  # app 依赖 B

作用

  • 建立链接关系:告诉 CMake 这个 Target 在链接阶段需要哪些库(生成 -l 参数)
  • 建立依赖关系:让 CMake 知道构建顺序(先构建被依赖的库)
  • 传播接口属性:根据 PUBLIC/PRIVATE/INTERFACE,决定是否把被依赖库的 PUBLIC 属性(头文件路径、编译定义等)传递给当前 Target

底层发生了什么?

CMake 会做三件事:

  1. 链接关系记录:把 libA 加入 libB Target 的 LINK_LIBRARIES 属性
  2. 传递头文件和编译选项:如果 libAPUBLICinclude_directoriescompile_definitions,会自动传递给 libB
  3. 依赖顺序控制:CMake 会根据依赖关系自动计算正确的链接顺序(拓扑排序),避免链接错误

属性传播图解

库 A 的属性:
┌─────────────────────────────────┐
│  PRIVATE: src/a/internal        │ ← 仅 A 自己用
├─────────────────────────────────┤
│  PUBLIC: include/a              │ ← A 用,依赖 A 的也用
├─────────────────────────────────┤
│  INTERFACE: interface/a         │ ← A 不用,仅依赖 A 的用
└─────────────────────────────────┘
           ↓ target_link_libraries(B PUBLIC A)
库 B 继承的属性:
┌─────────────────────────────────┐
│  PUBLIC: include/a              │ ✅ 继承
│  INTERFACE: interface/a         │ ✅ 继承
│  PRIVATE: src/a/internal        │ ❌ 不继承
└─────────────────────────────────┘
           ↓ target_link_libraries(app PRIVATE B)
app 继承的属性:
┌─────────────────────────────────┐
│  B 的 PUBLIC + INTERFACE        │ ✅ 继承
│  A 的 PUBLIC + INTERFACE        │ ✅ 继承(因为 B PUBLIC A)
└─────────────────────────────────┘

2.5.5 target_link_libraries 的作用域

target_link_libraries 本身也有作用域,控制依赖关系的传播:

# CMakeLists.txt
# 库 A
add_library(libA a.cpp)

# 库 B 依赖 A(PRIVATE)
add_library(libB b.cpp)
target_link_libraries(libB PRIVATE libA)

# 库 C 依赖 A(PUBLIC)
add_library(libC c.cpp)
target_link_libraries(libC PUBLIC libA)

# 应用程序
add_executable(app main.cpp)
target_link_libraries(app PRIVATE libB libC)

结果分析

Target 链接的库 原因
libB libA 直接依赖
libC libA 直接依赖
app libB, libC, libA libC 的 PUBLIC 传递了 libA

关键点

  • libB PRIVATE libA:libA 是 libB 的内部实现,不传递给 app
  • libC PUBLIC libA:libA 是 libC 的公共接口,传递给 app
  • app 自动链接 libA,无需显式声明

2.5.6 如何选择作用域?

决策流程图

这个属性(头文件/宏/库)在公共头文件中使用吗?
├─ 是 → 用 PUBLIC
│   例:公共头文件 #include <boost/shared_ptr.hpp>
│
└─ 否 → 当前 Target 需要使用吗?
    ├─ 是 → 用 PRIVATE
    │   例:内部实现文件用的头文件
    │
    └─ 否 → 用 INTERFACE
        例:仅头文件库(header-only library)

常见场景速查表

场景 作用域 示例
公共 API 头文件路径 PUBLIC target_include_directories(lib PUBLIC include/)
内部实现头文件路径 PRIVATE target_include_directories(lib PRIVATE src/)
公共头文件中用到的第三方库 PUBLIC target_link_libraries(lib PUBLIC Boost::asio)
仅 .cpp 中用到的第三方库 PRIVATE target_link_libraries(lib PRIVATE fmt::fmt)
公共 API 的宏定义 PUBLIC target_compile_definitions(lib PUBLIC API_VERSION=2)
内部调试宏 PRIVATE target_compile_definitions(lib PRIVATE DEBUG_LOG)
纯头文件库 INTERFACE target_include_directories(header_lib INTERFACE include/)
编译选项(警告等) PRIVATE target_compile_options(lib PRIVATE -Wall)

2.5.7 实际应用场景

场景 1:第三方库依赖

# CMakeLists.txt
# 场景:你的库在公共头文件中使用了 Boost
add_library(mylib src/lib.cpp)

# ❌ 错误做法:PRIVATE
target_link_libraries(mylib PRIVATE Boost::filesystem)
# 问题:用户代码 #include "mylib.h" 时,找不到 Boost 头文件

# ✅ 正确做法:PUBLIC
target_link_libraries(mylib PUBLIC Boost::filesystem)
# 结果:用户自动获得 Boost 的头文件路径和链接库

判断标准

你的公共头文件中是否 #include <boost/filesystem.hpp>?
├─ 是 → PUBLIC(用户也需要 Boost)
└─ 否 → PRIVATE(仅内部实现用)

场景 2:纯头文件库(Header-Only Library)

# CMakeLists.txt
# 纯头文件库:没有 .cpp 文件
add_library(header_only_lib INTERFACE)

target_include_directories(header_only_lib INTERFACE include/)
target_compile_features(header_only_lib INTERFACE cxx_std_17)

# 用户代码
add_executable(app main.cpp)
target_link_libraries(app PRIVATE header_only_lib)

# app 自动获得:
# - include/ 路径
# - C++17 标准要求

为什么用 INTERFACE?

  • 头文件库自己没有编译过程(没有 .cpp)
  • 所有配置都是给依赖者用的

2.5.8 常见错误与调试

错误 1:头文件找不到

# CMakeLists.txt
# ❌ 错误:用了 PRIVATE,但用户需要这个头文件
add_library(mylib lib.cpp)
target_include_directories(mylib PRIVATE include/)

# 用户代码
add_executable(app main.cpp)
target_link_libraries(app PRIVATE mylib)
# 编译错误:fatal error: mylib.h: No such file or directory

解决方案:改为 PUBLIC

# CMakeLists.txt
target_include_directories(mylib PUBLIC include/)

错误 2:过度传播依赖

# CMakeLists.txt
# ❌ 错误:用了 PUBLIC,但用户不需要这个库
add_library(mylib lib.cpp)
target_link_libraries(mylib PUBLIC fmt::fmt)  # fmt 仅在 .cpp 中使用

# 问题:所有链接 mylib 的用户都被迫依赖 fmt

解决方案:改为 PRIVATE

# CMakeLists.txt
target_link_libraries(mylib PRIVATE fmt::fmt)

调试技巧

# 查看 Target 的属性
get_target_property(INCLUDES mylib INTERFACE_INCLUDE_DIRECTORIES)
message("mylib PUBLIC includes: ${INCLUDES}")

get_target_property(LINKS mylib INTERFACE_LINK_LIBRARIES)
message("mylib PUBLIC links: ${LINKS}")

2.6 旧方式 vs 现代方式

旧方式:全局命令(不推荐)

# 影响所有后续定义的 Target
include_directories(include/)
add_definitions(-DDEBUG)
add_compile_options(-Wall)

add_library(lib1 lib1.cpp)  # 继承上面所有设置
add_library(lib2 lib2.cpp)  # 也继承上面所有设置

问题

  • 全局污染,所有 Target 都受影响
  • 无法精确控制每个 Target 的配置
  • 依赖关系不清晰
  • 不支持作用域控制

现代方式:Target-based 命令(推荐)

add_library(lib1 lib1.cpp)
target_include_directories(lib1 PRIVATE include/)
target_compile_definitions(lib1 PRIVATE DEBUG)
target_compile_options(lib1 PRIVATE -Wall)

add_library(lib2 lib2.cpp)
# lib2 不受 lib1 配置的影响

优势对比

维度 旧方式 现代方式
作用范围 全局,影响所有后续 Target 只作用于指定 Target
依赖关系 看不出来 明确、清晰、可追踪
传播控制 无法精细控制 通过 PUBLIC/PRIVATE/INTERFACE 精确控制
底层存储 全局变量 每个 Target 自己的属性
适合大型项目 非常差 极好
封装性

3. 变量系统

📝 变量是什么:CMake 中的变量用于存储值,可以是字符串、列表、路径等。

3.1 为什么需要变量?

变量让你的 CMakeLists.txt 更灵活、更易维护:

  • 避免重复写相同的路径或值
  • 方便修改配置(改一处,全局生效)
  • 支持条件编译(根据平台或配置选择不同的值)

3.2 定义和使用变量

# 定义普通变量
set(MY_VAR "value")
set(MY_LIST item1 item2 item3)

# 使用变量
message("Variable value: ${MY_VAR}")

# 列表操作
list(APPEND MY_LIST item4)
list(LENGTH MY_LIST list_length)

3.3 变量的类型

普通变量

set(MY_VAR "hello")
set(MY_NUMBER 42)
set(MY_PATH /usr/local/include)

列表变量

set(SOURCES main.cpp utils.cpp helper.cpp)
# 等价于
set(SOURCES "main.cpp;utils.cpp;helper.cpp")

# 使用列表
add_executable(app ${SOURCES})

缓存变量(可在命令行修改)

set(MY_OPTION "default" CACHE STRING "Description")

# 命令行修改:cmake .. -DMY_OPTION=new_value

环境变量

# 读取环境变量
message("PATH: $ENV{PATH}")

# 设置环境变量
set(ENV{MY_VAR} "value")

3.4 常用内置变量

${CMAKE_SOURCE_DIR}          # 源代码根目录
${CMAKE_BINARY_DIR}          # 构建目录
${CMAKE_CURRENT_SOURCE_DIR}  # 当前 CMakeLists.txt 所在目录
${CMAKE_CURRENT_BINARY_DIR}  # 当前构建目录
${PROJECT_NAME}              # 项目名称
${PROJECT_VERSION}           # 项目版本
${CMAKE_CXX_COMPILER}        # C++ 编译器路径
${CMAKE_BUILD_TYPE}          # 构建类型(Debug/Release)

3.5 变量的作用域

# 全局作用域
set(GLOBAL_VAR "global")

function(my_function)
    # 函数作用域(独立)
    set(LOCAL_VAR "local")
    message("Inside function: ${GLOBAL_VAR}")  # 可以访问全局变量
endfunction()

my_function()
message("Outside function: ${LOCAL_VAR}")  # 空值,访问不到函数内的变量

4. 子目录与头文件管理

⚠️ 重要:多目录项目的核心。实际项目通常有多个子目录,每个子目录可能有自己的 CMakeLists.txt。理解如何组织和引入子目录是构建大型项目的关键。

4.1 add_subdirectory:引入子目录

4.1.1 基本用法

# 根目录 CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(MyProject)

# 引入子目录
add_subdirectory(src)
add_subdirectory(lib)
add_subdirectory(tests)

4.1.2 执行流程

项目结构:
MyProject/
├── CMakeLists.txt          # 根 CMakeLists.txt
├── src/
│   ├── CMakeLists.txt      # src 的 CMakeLists.txt
│   └── main.cpp
└── lib/
    ├── CMakeLists.txt      # lib 的 CMakeLists.txt
    ├── mylib.cpp
    └── mylib.h

执行流程:
1. cmake .. 开始配置
   ↓
2. 读取根目录 CMakeLists.txt
   ↓
3. 遇到 add_subdirectory(src)
   ↓
4. 进入 src/ 目录,读取 src/CMakeLists.txt
   ↓
5. 执行 src/CMakeLists.txt 中的所有指令
   ↓
6. 返回根目录,继续执行
   ↓
7. 遇到 add_subdirectory(lib)
   ↓
8. 进入 lib/ 目录,读取 lib/CMakeLists.txt
   ↓
9. 执行完毕,返回根目录
   ↓
10. 根目录 CMakeLists.txt 执行完毕
📝 关键点

  • add_subdirectory立即进入子目录并执行子目录的 CMakeLists.txt
  • 子目录中定义的 Target 在父目录中可见
  • 子目录继承父目录的变量,但修改不会影响父目录(除非用 PARENT_SCOPE

4.1.3 实际例子

# 根目录 CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(MyApp)

set(CMAKE_CXX_STANDARD 17)

# 引入库目录
add_subdirectory(lib)

# 引入源码目录
add_subdirectory(src)
# lib/CMakeLists.txt
add_library(mylib mylib.cpp)
target_include_directories(mylib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
# src/CMakeLists.txt
add_executable(app main.cpp)

# 可以直接使用 lib 目录中定义的 mylib 目标
target_link_libraries(app PRIVATE mylib)

4.1.4 变量作用域

# 根目录 CMakeLists.txt
set(MY_VAR "root value")
add_subdirectory(sub)
message("After subdirectory: ${MY_VAR}")  # 仍然是 "root value"

# sub/CMakeLists.txt
message("In subdirectory: ${MY_VAR}")     # "root value"(继承)
set(MY_VAR "sub value")                   # 修改局部副本
message("Modified: ${MY_VAR}")            # "sub value"

# 如果要修改父目录的变量:
set(MY_VAR "sub value" PARENT_SCOPE)     # 影响父目录

4.2 头文件路径管理

4.2.1 include_directories(旧式,不推荐)

# ❌ 旧式方法:全局影响所有目标
include_directories(include/)
include_directories(third_party/include/)

add_executable(app1 main1.cpp)  # 自动包含上面的路径
add_executable(app2 main2.cpp)  # 也自动包含上面的路径

问题:

  • 影响当前目录及所有子目录的所有目标
  • 无法控制作用域(PRIVATE/PUBLIC/INTERFACE)
  • 容易造成头文件污染
  • 不符合现代 CMake 的 Target-based 设计

4.2.2 target_include_directories(现代方法,推荐)

# ✅ 现代方法:针对特定目标
add_library(mylib src/lib.cpp)
target_include_directories(mylib
    PUBLIC include/          # 对外接口
    PRIVATE src/internal/    # 内部实现
)

add_executable(app main.cpp)
target_link_libraries(app PRIVATE mylib)
# app 自动获得 mylib 的 PUBLIC 头文件路径

优势:

  • 精确控制每个目标的头文件路径
  • 支持 PRIVATE/PUBLIC/INTERFACE 作用域
  • 依赖传递清晰
  • 符合现代 CMake 最佳实践

4.2.3 对比示例

# ❌ 旧式:全局污染
include_directories(boost/include)
include_directories(opencv/include)

add_library(lib1 lib1.cpp)  # 包含 boost 和 opencv
add_library(lib2 lib2.cpp)  # 也包含 boost 和 opencv(即使不需要)
add_executable(app main.cpp)  # 也包含所有路径
# ✅ 现代:精确控制
add_library(lib1 lib1.cpp)
target_include_directories(lib1 PRIVATE boost/include)  # 仅 lib1 用

add_library(lib2 lib2.cpp)
target_include_directories(lib2 PRIVATE opencv/include)  # 仅 lib2 用

add_executable(app main.cpp)
target_link_libraries(app PRIVATE lib1 lib2)
# app 不会获得 lib1 和 lib2 的 PRIVATE 头文件路径

4.3 完整的多目录项目示例

项目结构

MyProject/
├── CMakeLists.txt              # 根配置
├── include/                    # 公共头文件
│   └── myproject/
│       └── api.h
├── src/                        # 源码目录
│   ├── CMakeLists.txt
│   ├── main.cpp
│   └── internal.h
├── lib/                        # 库目录
│   ├── CMakeLists.txt
│   ├── mylib.cpp
│   └── mylib.h
└── third_party/                # 第三方库
    └── json/
        └── json.hpp

根目录 CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(MyProject VERSION 1.0.0)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 设置输出目录
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)

# 引入子目录(顺序很重要!)
add_subdirectory(lib)    # 先构建库
add_subdirectory(src)    # 再构建可执行文件(依赖库)

lib/CMakeLists.txt

# 创建库
add_library(mylib mylib.cpp)

# 设置头文件路径
target_include_directories(mylib
    PUBLIC
        ${CMAKE_SOURCE_DIR}/include      # 公共 API 头文件
        ${CMAKE_CURRENT_SOURCE_DIR}      # 库自己的头文件
    PRIVATE
        ${CMAKE_SOURCE_DIR}/third_party  # 第三方库(内部使用)
)

# 设置编译选项
target_compile_options(mylib PRIVATE -Wall -Wextra)

# 设置宏定义
target_compile_definitions(mylib
    PUBLIC MYLIB_VERSION="${PROJECT_VERSION}"
    PRIVATE MYLIB_INTERNAL_DEBUG
)

src/CMakeLists.txt

# 创建可执行文件
add_executable(app main.cpp)

# 链接库
target_link_libraries(app PRIVATE mylib)

# app 自动继承 mylib 的 PUBLIC 属性:
# - include/ 头文件路径
# - mylib.h 所在路径
# - MYLIB_VERSION 宏定义
# 但看不到 mylib 的 PRIVATE 属性:
# - third_party/ 路径
# - MYLIB_INTERNAL_DEBUG 宏

4.4 常见问题与最佳实践

问题 1:头文件找不到

# ❌ 错误:使用相对路径
target_include_directories(mylib PUBLIC include/)

# ✅ 正确:使用绝对路径
target_include_directories(mylib PUBLIC ${CMAKE_SOURCE_DIR}/include/)
# 或
target_include_directories(mylib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/../include/)

问题 2:子目录顺序错误

# ❌ 错误:src 依赖 lib,但 lib 还没定义
add_subdirectory(src)   # src 中使用 mylib 目标
add_subdirectory(lib)   # mylib 在这里才定义

# ✅ 正确:先定义依赖
add_subdirectory(lib)   # 先定义 mylib
add_subdirectory(src)   # 再使用 mylib

问题 3:混用旧式和现代方法

# ❌ 不推荐:混用
include_directories(include/)           # 旧式
target_include_directories(mylib ...)   # 现代

# ✅ 推荐:统一使用现代方法
target_include_directories(mylib PUBLIC include/)
target_include_directories(app PRIVATE src/)
💡 现代 CMake 最佳实践

  • 1. 优先使用 target_xxx 命令target_include_directories 而非 include_directoriestarget_compile_options 而非 add_compile_options
  • 2. 合理使用作用域:内部实现 → PRIVATE,公共接口 → PUBLIC,仅头文件库 → INTERFACE
  • 3. 路径使用变量${CMAKE_SOURCE_DIR}${CMAKE_CURRENT_SOURCE_DIR}${CMAKE_CURRENT_BINARY_DIR}
  • 4. 子目录顺序:先定义被依赖的目标,再定义依赖者
  • 5. 保持 CMakeLists.txt 简洁:每个子目录管理自己的目标,根目录只负责协调

5. 命令基础

5.1 条件判断

if(WIN32)
    message("Windows platform")
elseif(UNIX)
    message("Unix-like platform")
else()
    message("Other platform")
endif()

# 常用条件
if(DEFINED MY_VAR)          # 变量是否定义
if(EXISTS ${FILE_PATH})     # 文件是否存在
if(MY_VAR STREQUAL "value") # 字符串相等
if(MY_VAR MATCHES "regex")  # 正则匹配
if(MY_VAR GREATER 10)       # 数值比较

5.2 循环

# foreach 循环
set(MY_LIST item1 item2 item3)
foreach(item IN LISTS MY_LIST)
    message("Item: ${item}")
endforeach()

# 范围循环
foreach(i RANGE 5)
    message("Number: ${i}")  # 0, 1, 2, 3, 4, 5
endforeach()

# while 循环
set(i 0)
while(i LESS 5)
    message("i = ${i}")
    math(EXPR i "${i} + 1")
endwhile()

5.3 函数和宏

函数(有独立作用域)

function(my_function arg1 arg2)
    message("Args: ${arg1}, ${arg2}")
    set(LOCAL_VAR "local")  # 局部变量
endfunction()

my_function("hello" "world")
# LOCAL_VAR 在函数外访问不到

宏(无独立作用域)

macro(my_macro arg)
    set(result ${arg})  # 直接修改外部变量
endmacro()

my_macro("value")
message("result = ${result}")  # 可以访问
💡 函数 vs 宏

  • 函数:有独立作用域,不会污染外部变量(推荐)
  • :无作用域,直接文本替换(类似 C 的宏)

5.4 消息输出

message(STATUS "This is a status message")
message(WARNING "This is a warning")
message(FATAL_ERROR "This stops configuration")
message("Simple message")

# 调试输出
message("Variable value: ${MY_VAR}")

6. 总结

核心概念回顾

1. CMakeLists.txt

  • CMake 的配置文件,描述构建规则
  • 在配置阶段执行,生成构建系统文件

2. Target(目标)

  • CMake 的核心概念,代表构建产物
  • 每个 Target 有自己的属性(头文件路径、编译选项、依赖库等)
  • Target 之间可以建立依赖关系,自动传递属性
  • 作用域(PRIVATE/PUBLIC/INTERFACE):PRIVATE 仅自己用,PUBLIC 自己用依赖者也用,INTERFACE 仅依赖者用

3. 变量系统

  • 存储配置信息,提高灵活性
  • 分为普通变量、缓存变量、环境变量
  • 有作用域概念(全局、函数、目录)

4. 子目录与头文件管理

  • add_subdirectory:引入子目录的 CMakeLists.txt
  • target_include_directories:现代头文件管理(推荐)
  • include_directories:旧式全局方法(不推荐)
  • 子目录中的 Target 对父目录可见
  • 变量继承但修改不影响父目录

5. 命令基础

  • 条件判断、循环、函数/宏
  • 让 CMakeLists.txt 更灵活、更强大

学习路径:

✅ 引入篇:从 GCC 到 Makefile 再到 CMake → 理解为什么需要 CMake
✅ 篇一:快速入门 → 会用 CMake
✅ 篇二:核心概念 → 理解 CMake(当前)
→ 篇三:常用指令与实践 → 精通 CMake

系列文章:

下一步:

  • 学习篇三:掌握常用指令的详细用法
  • 实战练习:在真实项目中应用这些概念

最后更新:2026-05-28