2. CMake基础回顾
好,咱们正式开始。上一章聊了CMake能解决什么问题,这一章咱们把地基夯实。说白了,CMake的一切操作,都围绕着一个核心文件——CMakeLists.txt。你想想看,整个构建系统的蓝图,全画在这个文件里。
我个人习惯,拿到一个新项目,第一件事就是扫一眼根目录的CMakeLists.txt。结构清不清晰,直接决定了后续维护的幸福感。嗯,这里要注意,别把它写成“意大利面条”。
2.1 CMakeLists.txt 的基本结构
一个典型的CMakeLists.txt,就像一份菜谱。它告诉你需要什么材料(依赖)、怎么处理(编译选项)、最后装盘(生成目标)。
最基本的骨架长这样:
cmake_minimum_required(VERSION 3.16)
project(MyProject VERSION 1.0.0 LANGUAGES CXX)
# 添加一个可执行文件
add_executable(my_app main.cpp utils.cpp)
# 添加一个库
add_library(my_lib STATIC lib.cpp lib.h)
# 链接库
target_link_libraries(my_app PRIVATE my_lib)
我在项目中遇到过,有人把cmake_minimum_required写得很低,比如2.8。结果新特性用不了,还一堆兼容性警告。我建议,除非你有特殊理由,否则直接设到3.16以上,省心。
核心原则:每个CMakeLists.txt文件,都应该以cmake_minimum_required和project开头。这是规矩,也是好习惯。
2.2 常用命令速览
CMake的命令很多,但常用的就那么十几个。我把它们分成三类,你记牢了,日常开发基本够用。
2.2.1 目标定义类
add_executable:生成可执行文件。比如你的main()函数所在的入口。add_library:生成库文件。可以是静态库(STATIC)、动态库(SHARED)或者模块库(MODULE)。add_custom_target:自定义目标。用来执行一些脚本、生成代码等“非编译”任务。
2.2.2 依赖管理类
target_link_libraries:链接库。注意那个PRIVATE、PUBLIC、INTERFACE关键字,它们控制传递依赖。我曾经因为全用了PUBLIC,导致下游项目链接了一堆不该链接的东西,编译慢得离谱。target_include_directories:指定头文件搜索路径。同样有PRIVATE/PUBLIC/INTERFACE。find_package:查找外部依赖包。比如找OpenCV、Boost、Qt等。
2.2.3 控制与查询类
set:设置变量。最常用的命令,没有之一。option:定义开关选项。比如option(BUILD_TESTS "Build unit tests" ON)。message:打印信息。调试时救命用的。if/else/endif:条件判断。用来处理不同平台、不同编译器的情况。
小技巧:我习惯把target_include_directories和target_link_libraries紧跟在add_executable或add_library后面写。这样阅读代码时,一眼就能看到这个目标依赖了哪些东西,逻辑非常清晰。
2.3 变量与缓存
CMake里的变量,说白了就是字符串。但它的作用域和缓存机制,坑了不少新手。
2.3.1 普通变量 vs 缓存变量
| 类型 | 定义方式 | 作用域 | 是否持久化 |
|---|---|---|---|
| 普通变量 | set(MY_VAR "hello") |
当前CMakeLists.txt及子目录 | 否,每次cmake重新运行都会重置 |
| 缓存变量 | set(MY_CACHE_VAR "world" CACHE STRING "描述") |
全局 | 是,存储在CMakeCache.txt中 |
为什么会这样?因为CMake的设计哲学是:普通变量用于内部逻辑传递,缓存变量用于用户配置。你想想看,如果你每次运行cmake都要重新设置编译器路径,那得多烦?缓存变量就是干这个的。
2.3.2 变量引用的坑
引用变量用${VAR_NAME}。但注意,如果变量没定义,它会变成空字符串,不会报错。我曾经因为这个,排查了一个小时的链接错误——原来是个路径变量拼写错了,结果链接器找不到库文件。
避坑指南:我曾经在项目里看到有人用set(CMAKE_CXX_FLAGS "-O2"),直接覆盖了编译器默认的flags。正确的做法是set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O2"),追加而不是覆盖。记住,CMAKE_开头的变量,大多是CMake内置的,操作它们要格外小心。
2.4 生成器与构建系统
CMake本身不编译代码。它是个“生成器”,负责生成其他构建系统的输入文件。比如Makefile、Ninja文件、Visual Studio的.sln文件等。
你可以通过-G参数指定生成器:
# 使用Ninja
cmake -G "Ninja" ..
# 使用Unix Makefiles(默认)
cmake -G "Unix Makefiles" ..
# 使用Visual Studio 2022
cmake -G "Visual Studio 17 2022" ..
2.4.1 生成器怎么选?
我个人经验:
- Linux/macOS开发:首选Ninja。它比Make快很多,尤其是增量编译。我在一个大型C++项目里,把Make换成Ninja后,全量编译时间从15分钟降到了11分钟,增量编译更是秒级。
- Windows开发:如果你用Visual Studio,直接用它的生成器。如果你用CLion或VSCode,Ninja也是好选择。
- 跨平台CI:统一用Ninja,可以避免不同平台Make行为的差异。
2.4.2 构建系统的层次
理解这个层次,对你调优很有帮助:
- 源文件:你的
.cpp、.h文件。 - CMake:读取
CMakeLists.txt,生成构建规则。 - 构建系统(Make/Ninja):执行构建规则,调用编译器。
- 编译器/链接器:真正干活的人,生成
.o、.a、.so、.exe。
你想想看,CMake的优化,本质上是让第2步和第3步更高效。比如减少不必要的重新配置、优化依赖图、减少生成的文件数量等。这些我们后面会详细展开。