一、目标(Target)——CMake 构建的核心单元
说实话,我刚接触 CMake 那会儿,最困惑的就是这个「目标」概念。你写个 add_executable(),它就生成了一个目标。但目标到底是什么?
我个人的理解是:目标就是你要构建的东西。它可能是一个可执行文件,也可能是一个库。CMake 里的一切操作,几乎都是围绕目标展开的。
举个例子。你写了一个 C++ 项目,里面有 main.cpp 和一些工具函数。你想把它们编译成一个叫 myapp 的程序。那你就写:
add_executable(myapp main.cpp utils.cpp)
这一行代码,就创建了一个名为 myapp> 的目标。CMake 知道:哦,我要把 main.cpp 和 utils.cpp 编译链接,最终生成一个可执行文件。
那库呢?也很简单。如果你想把工具函数单独打包,给别人用:
add_library(utils STATIC utils.cpp)
这就创建了一个静态库目标 utils。STATIC 表示静态库,SHARED 表示动态库,MODULE 是插件库。我在项目中常用的就是 STATIC 和 SHARED。
核心要点:add_executable() 和 add_library() 是 CMake 里最基础的两个命令。它们创建的目标,是后续所有依赖、属性、链接操作的基础。
二、目标的属性(Properties)——给目标加点配置
目标创建好了,但光秃秃的。你需要告诉 CMake:这个目标用什么编译选项?头文件在哪找?链接什么库?
这些信息,都通过「属性」来设置。属性就是目标的配置项。
我刚开始用的时候,总觉得属性很抽象。后来想通了:属性就是目标的「元数据」。就像一个人的身高、体重、年龄一样,目标也有自己的属性。
2.1 常用属性一览
| 属性名 | 作用 | 示例 |
|---|---|---|
| INCLUDE_DIRECTORIES | 指定头文件搜索路径 | target_include_directories(myapp PRIVATE src/include) |
| COMPILE_DEFINITIONS | 添加编译宏定义 | target_compile_definitions(myapp PRIVATE DEBUG=1) |
| COMPILE_OPTIONS | 添加编译选项 | target_compile_options(myapp PRIVATE -Wall -O2) |
| LINK_LIBRARIES | 指定链接的库 | target_link_libraries(myapp PRIVATE utils) |
| OUTPUT_NAME | 指定输出文件名 | set_target_properties(myapp PROPERTIES OUTPUT_NAME "my_cool_app") |
你看,这些属性覆盖了编译、链接、输出的方方面面。我习惯把属性设置写在目标创建之后,这样逻辑清晰,不容易漏。
2.2 设置属性的两种方式
第一种,用专门的命令。比如 target_include_directories()、target_link_libraries()。这些命令是 CMake 推荐的方式,语义明确。
第二种,用 set_target_properties()。这个更通用,可以设置任意属性。比如:
set_target_properties(myapp PROPERTIES
CXX_STANDARD 17
CXX_STANDARD_REQUIRED ON
POSITION_INDEPENDENT_CODE ON
)
我个人更推荐第一种方式,因为可读性更好。但有些属性没有专门的命令,那就只能用 set_target_properties() 了。
小技巧:用 get_target_property() 可以查看目标的属性值。调试时很有用。比如 get_target_property(out_name myapp OUTPUT_NAME)。
三、目标之间的依赖关系——构建的骨架
一个项目不可能只有一个目标。通常会有多个可执行文件、多个库。它们之间怎么组织?
答案就是:依赖关系。
你想想看,如果可执行文件 A 链接了库 B,那 A 就依赖于 B。CMake 必须保证:先编译 B,再编译 A。这就是依赖关系的核心作用——控制构建顺序。
3.1 链接依赖
最常用的依赖就是链接依赖。用 target_link_libraries() 来声明:
add_library(math_lib STATIC math.cpp)
add_executable(calculator main.cpp)
target_link_libraries(calculator PRIVATE math_lib)
这里 calculator 依赖于 math_lib。CMake 会自动处理构建顺序。你不需要手动指定先编译哪个。
PRIVATE、PUBLIC、INTERFACE 这三个关键字,我刚开始也搞混过。简单说:
- PRIVATE:依赖只对当前目标可见。比如 math_lib 用到了某个第三方库,但 calculator 不需要知道。
- PUBLIC:依赖对当前目标和链接它的目标都可见。比如 math_lib 的头文件路径,calculator 也需要。
- INTERFACE:依赖只对链接当前目标的目标可见,当前目标自己不用。常用于纯头文件库。
我在项目中遇到过一个问题:一个库的依赖设置成了 PRIVATE,结果链接它的可执行文件找不到头文件。排查了半天,才发现是作用域搞错了。嗯,这里要注意。
3.2 其他依赖关系
除了链接依赖,还有两种:
- add_dependencies():强制指定构建顺序。比如目标 A 需要在目标 B 之后构建,但两者没有链接关系。这种情况很少见,但确实存在。
- target_link_options():指定链接选项。比如你想链接一个静态库,但不想用
target_link_libraries(),可以用这个。
不过说实话,我用了这么多年 CMake,add_dependencies() 只用过两三次。大部分情况下,target_link_libraries() 就足够了。
避坑指南:我曾经在一个大型项目里,把依赖关系搞成了循环依赖。A 链接 B,B 链接 C,C 又链接 A。结果 CMake 直接报错,说检测到循环依赖。解决办法是重新设计库的层次结构,把公共部分抽出来。
四、知识体系总览
说了这么多,我画了一张图,帮你把本章的知识点串起来。你看一眼就明白了:
这张图把本章的内容分成了三大块:创建目标、设置属性、管理依赖。你顺着这个结构去理解,就不会乱了。
五、一个完整的例子
最后,我给你看一个完整的 CMakeLists.txt。它把今天讲的内容都用上了:
cmake_minimum_required(VERSION 3.16)
project(MyProject VERSION 1.0.0 LANGUAGES CXX)
# 创建一个库目标
add_library(math_utils STATIC
src/math/add.cpp
src/math/sub.cpp
src/math/mul.cpp
)
# 设置库的属性
target_include_directories(math_utils PUBLIC include)
target_compile_features(math_utils PUBLIC cxx_std_17)
# 创建一个可执行文件目标
add_executable(myapp
src/main.cpp
src/io.cpp
)
# 设置可执行文件的属性
target_include_directories(myapp PRIVATE include)
target_compile_definitions(myapp PRIVATE APP_VERSION="${PROJECT_VERSION}")
# 建立依赖关系
target_link_libraries(myapp PRIVATE math_utils)
你看,这个文件里:
- 先创建了两个目标:
math_utils和myapp - 然后给每个目标设置了属性:头文件路径、编译特性、宏定义
- 最后用
target_link_libraries()建立了依赖关系
整个构建过程,CMake 会自动处理。你只需要把目标之间的关系说清楚就行。
我的建议:刚开始写 CMakeLists.txt 时,别想着一步到位。先创建目标,再慢慢加属性。每加一个属性,就编译一次,看看效果。这样不容易出错。
好了,这一章的内容就到这里。目标的概念是 CMake 的基石,后面的所有内容都建立在这个基础上。你把它搞懂了,后面的路就好走了。