版权信息
warning
本文章为博主原创文章。遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
*AI生成内容
这是一份为有一定基础的 C/C++ 开发者准备的现代 CMake 实战指南。相比于早期 CMake 满天飞的全局变量(如 include_directories),现代 CMake(3.0+)的核心哲学是基于目标(Target-based)。
理解并坚持“基于目标”的原则,能极大提升软件架构的清晰度,尤其是在开发包含多个模块或硬件抽象层的复杂项目时。
1. 现代 CMake 的核心:Target 与属性
在现代 CMake 中,一切皆为 Target(可执行文件或库)。你不需要全局设置包含路径或编译选项,而是将它们绑定到特定的 Target 上。
核心命令只有这几个家族:
-
add_executable()/add_library():创建 Target。 -
target_include_directories():给 Target 添加头文件搜索路径。 -
target_link_libraries():给 Target 链接其他库。 -
target_compile_options()/target_compile_definitions():给 Target 添加编译选项或宏定义。
关键概念:PRIVATE, PUBLIC, INTERFACE。:
当你为一个 Target 指定包含路径、编译选项或链接库时,必须使用这三个关键字来声明作用域:
-
PRIVATE:该属性仅供本 Target 内部编译使用。使用者(依赖该 Target 的其他目标)不需要知道它。 -
INTERFACE:该属性本 Target 编译时不需要,但任何使用该 Target 的使用者都需要它(通常见于接口库( or Header-Only))。 -
PUBLIC:等于PRIVATE+INTERFACE。本 Target 编译时需要,使用者也需要。
示例:假设我们正在编写一个传感器硬件抽象层(sensor_hal)库,并且它内部调用了底层的内核级字符串处理函数(比如内核版的 string.h),但不对外暴露这些底层细节。
# 创建一个静态库 Target
add_library(sensor_hal STATIC hal_core.c hal_utils.c)
# PUBLIC: 使用 sensor_hal 的上层应用也需要找到这些头文件
target_include_directories(sensor_hal PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
# PRIVATE: 只有 sensor_hal 内部编译时需要这些特定的编译选项(如独立环境)
target_compile_options(sensor_hal PRIVATE -Wall -Wextra -ffreestanding)
2. 标准的单模块项目模板
适用于快速编写测试、脚本管理工具或小型服务端程序。
# 1. 指定最低版本要求 (建议 3.15 以上,支持更多现代特性)
cmake_minimum_required(VERSION 3.15)
# 2. 定义项目名称、版本和语言
project(SmartTerminal VERSION 1.0.0 LANGUAGES C CXX)
# 3. 设置全局 C/C++ 标准 (少数推荐的全局变量之一)
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 4. 收集源文件 (推荐明确列出,而不是用 file(GLOB) 避免缓存问题)
set(SOURCES
src/main.c
src/utils.c
)
# 5. 定义可执行文件 Target
add_executable(${PROJECT_NAME} ${SOURCES})
# 6. 指定头文件目录
# ${CMAKE_CURRENT_SOURCE_DIR} 指向当前 CMakeLists.txt 所在目录
target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
# 7. 添加特定宏定义 (如开启调试日志)
target_compile_definitions(${PROJECT_NAME} PRIVATE ENABLE_DEBUG_LOG=1)
3. 多模块架构与库的管理
对于工业级项目(如网关、复杂控制系统),通常会划分为核心业务、硬件抽象层(HAL)、第三方组件等。这就需要用到 add_subdirectory()。
拒绝 file GLOB。很多旧教程会教你使用
file(GLOB SRC_FILES "*.c")来批量添加源码。这是一个非常危险的反模式,因为当你新增或删除.c文件时,CMake 往往无法察觉,导致不会重新生成构建树。最佳实践是使用分治法、显式列出源码。
目录结构示例:
├── CMakeLists.txt # 顶层 CMake
├── app/
│ ├── CMakeLists.txt # 业务层 CMake
│ └── main.c
└── components/
└── hal/
├── CMakeLists.txt # 硬件驱动层 CMake
├── include/hal_gpio.h
└── src/hal_gpio.c
底层模块 (components/hal/CMakeLists.txt):
# 编译为一个静态库
add_library(hal STATIC src/hal_gpio.c)
# 任何链接了 'hal' 的 Target,都会自动获得 include 目录的搜索路径!
# 这就是 Modern CMake 的魅力,不需要在上层重复写 include_directories
target_include_directories(hal PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
顶层 (CMakeLists.txt):
cmake_minimum_required(VERSION 3.15)
project(IoTGateway LANGUAGES C)
# 引入子目录
add_subdirectory(components/hal)
add_subdirectory(app)
应用层 (app/CMakeLists.txt):
add_executable(gateway_app main.c)
# 链接底层库,自动获取其 PUBLIC 暴露的头文件路径和宏定义
target_link_libraries(gateway_app PRIVATE hal)
4. 引入外部依赖
日常开发中经常需要引入外部库(如多线程、或者像 LVGL 这样的 GUI 库)。
方式 A:系统已安装的库 (find_package)
适用于标准库或通过包管理器安装的库。
# 寻找系统的线程库
find_package(Threads REQUIRED)
add_executable(my_app main.c)
target_link_libraries(my_app PRIVATE Threads::Threads)
方式 B:源码级拉取 (FetchContent)
这是现代 CMake 管理第三方源码依赖的利器,非常适合嵌入式或没有包管理器的环境。
include(FetchContent)
# 自动从 Git 拉取依赖并加入构建树
FetchContent_Declare(
cJSON
GIT_REPOSITORY https://github.com/DaveGamble/cJSON.git
GIT_TAG v1.7.15
)
FetchContent_MakeAvailable(cJSON)
add_executable(my_app main.c)
# 直接链接拉取下来的 target
target_link_libraries(my_app PRIVATE cjson)
5. 交叉编译基础 (Cross-compiling)
如果在 WSL2 或普通 Linux 环境下为特定的 MCU 或嵌入式板子编译代码,不要在 CMakeLists.txt 里硬编码编译器路径,而是使用 Toolchain File (工具链文件)。
1. 创建一个 arm-gcc-toolchain.cmake:
set(CMAKE_SYSTEM_NAME Generic) # Generic 通常用于裸机/RTOS,如果是嵌入式 Linux 则写 Linux
set(CMAKE_SYSTEM_PROCESSOR arm)
# 指定编译器
set(CMAKE_C_COMPILER arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER arm-none-eabi-g++)
# 指定一些硬件相关的强制编译选项 (例如 Cortex-M4)
set(CMAKE_C_FLAGS "-mcpu=cortex-m4 -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16" CACHE INTERNAL "C Compiler options")
2. 在命令行使用该工具链配置项目:
# 在 build 目录下执行
cmake -DCMAKE_TOOLCHAIN_FILE=../arm-gcc-toolchain.cmake -DCMAKE_BUILD_TYPE=Release ..
make -j4
6. 日常高频命令汇总备忘
-
构建类型控制:通常通过命令行传入
-DCMAKE_BUILD_TYPE=Debug或Release。# 在 CMake 中针对 Debug 模式添加特殊选项 if(CMAKE_BUILD_TYPE STREQUAL "Debug") target_compile_options(${PROJECT_NAME} PRIVATE -g -O0 -Wall -Wextra) else() target_compile_options(${PROJECT_NAME} PRIVATE -O3) endif() -
生成 Compile Commands:对使用
clangd或 VSCode 插件进行代码跳转非常关键。# 执行 cmake 时加上,或在文件头部设置: set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # 这会在 build 目录下生成 compile_commands.json
7. 高频预定义变量 (Predefined Variables)
这部分相当于 CMake 给我们提供的系统级环境变量,直接决定了构建路径和编译参数。
7.1. 路径与目录相关(模块化开发必备)
在多目录项目中,极容易把这几个路径变量搞混:
-
CMAKE_SOURCE_DIR:顶层CMakeLists.txt所在的绝对路径。不管你在哪个子目录里调用,它永远指向最外层。 -
CMAKE_BINARY_DIR:顶层构建目录的绝对路径(通常就是你执行cmake ..所在的build文件夹)。 -
CMAKE_CURRENT_SOURCE_DIR:当前正在处理的CMakeLists.txt所在的目录。- 实战用法:在写子模块(如驱动层或工具库)时,包含当前目录下的头文件:
target_include_directories(my_target PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)。
- 实战用法:在写子模块(如驱动层或工具库)时,包含当前目录下的头文件:
-
PROJECT_SOURCE_DIR:最近一次调用project()命令的CMakeLists.txt所在的路径。如果你的工程嵌套了其他独立的 CMake 项目,这个变量会比CMAKE_SOURCE_DIR更安全。
7.2. 构建与编译控制
-
CMAKE_BUILD_TYPE:控制构建类型。常用值:Debug(带调试信息,-g)、Release(最高优化,-O3)、MinSizeRel(最小体积优化,-Os,这在 Flash 资源紧张的 MCU 裸机或 RTOS 开发中非常常用)。 -
CMAKE_C_FLAGS/CMAKE_CXX_FLAGS:全局的 C/C++ 编译选项。- 注:现代 CMake 更推荐使用
target_compile_options针对具体 Target 设置,但如果你要全局强加一些架构相关的硬性 flag(比如硬件浮点运算开启),仍会在这里设置。
- 注:现代 CMake 更推荐使用
-
BUILD_SHARED_LIBS:如果你使用add_library(lib_name src.c)没有指定STATIC或SHARED,这个变量为ON时默认编译动态库,为OFF时默认编译静态库。
7.3. 系统与平台判定(交叉编译必备)
当你的代码需要在不同平台(比如 Linux 服务器环境和 ARM 嵌入式设备)之间切换时:
-
CMAKE_SYSTEM_NAME:目标系统的名称。常用的有Linux、Windows、Darwin(macOS)。如果是编译裸机程序或 RTOS,通常会被工具链文件设置为Generic。 -
UNIX/WIN32/APPLE:快捷布尔变量。如果目标平台是 Unix-like 系统(包括 Linux 和 macOS),UNIX的值为 1。
# 实战示例:跨平台链接特定库
if(UNIX AND NOT APPLE)
target_link_libraries(my_app PRIVATE pthread)
elseif(WIN32)
target_link_libraries(my_app PRIVATE wsock32)
endif()