一、目标(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 直接报错,说检测到循环依赖。解决办法是重新设计库的层次结构,把公共部分抽出来。

四、知识体系总览

说了这么多,我画了一张图,帮你把本章的知识点串起来。你看一眼就明白了:

CMake 目标(Target)知识体系 目标(Target) 创建目标 add_executable() add_library() 属性设置 target_include_* target_compile_* set_target_properties() 依赖关系 target_link_libraries() add_dependencies() 目标 = 创建 + 配置 + 依赖 → 构建系统自动管理

这张图把本章的内容分成了三大块:创建目标、设置属性、管理依赖。你顺着这个结构去理解,就不会乱了。

五、一个完整的例子

最后,我给你看一个完整的 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_utilsmyapp
  • 然后给每个目标设置了属性:头文件路径、编译特性、宏定义
  • 最后用 target_link_libraries() 建立了依赖关系

整个构建过程,CMake 会自动处理。你只需要把目标之间的关系说清楚就行。

我的建议:刚开始写 CMakeLists.txt 时,别想着一步到位。先创建目标,再慢慢加属性。每加一个属性,就编译一次,看看效果。这样不容易出错。

好了,这一章的内容就到这里。目标的概念是 CMake 的基石,后面的所有内容都建立在这个基础上。你把它搞懂了,后面的路就好走了。

公众号:蓝海资料掘金营,微信deep3321