4、CMake目标与属性:add_library、target_include_directories、target_link_libraries、属性设置

好,咱们今天聊聊CMake里最核心的几个概念——目标(Target)和属性(Property)。

说实话,我刚开始用CMake那会儿,也是从变量和目录命令一路写过来的。后来项目越来越大,发现那种全局式的写法简直是个噩梦。直到我真正理解了“目标”这个概念,才觉得CMake的世界一下子清晰了。

说白了,CMake里的“目标”就是一个构建单元。它可以是库,也可以是可执行文件。而“属性”就是挂在这个目标上的各种配置信息。你想想看,一个库需要知道它的头文件在哪、它依赖谁、它编译时要加什么宏——这些,都是属性。

4.1 add_library:创建你的第一个目标

add_library 是创建库目标的入口。它的基本用法很简单:

add_library(my_lib STATIC src/lib.cpp src/utils.cpp)

这里我创建了一个叫 my_lib 的静态库。STATIC 表示静态库,SHARED 是动态库,MODULE 是插件模块。

我在项目中遇到过一种情况:团队里有人把源文件路径写错了,结果编译时死活找不到符号。后来我养成了一个习惯——源文件列表尽量用变量管理,比如:

set(MY_LIB_SOURCES
    src/lib.cpp
    src/utils.cpp
    src/helper.cpp
)

add_library(my_lib STATIC ${MY_LIB_SOURCES})

这样改起来方便,也容易排查。

小提示:如果你不确定用 STATIC 还是 SHARED,我的建议是——内部模块用静态库,对外发布的用动态库。静态库链接简单,但体积大;动态库节省空间,但要注意导出符号。

4.2 target_include_directories:告诉编译器头文件在哪

库写好了,别人怎么用?首先得让人家能找到你的头文件。这就是 target_include_directories 的活。

target_include_directories(my_lib
    PUBLIC
        include
    PRIVATE
        src
)

这里有两个关键字:PUBLICPRIVATE。它们的区别很关键:

  • PUBLIC:当前目标能看到,链接当前目标的其他目标也能看到。
  • PRIVATE:只有当前目标能看到,别人看不到。
  • INTERFACE:当前目标看不到,但链接它的目标能看到(常用于纯头文件库)。

嗯,这里要注意:PUBLIC 和 INTERFACE 用错了,很容易导致头文件泄露或者找不到头文件。我曾经接手过一个项目,所有头文件路径都设成了 PUBLIC,结果下游项目莫名其妙地包含了不该包含的内部头文件,编译报错一堆。后来我花了一下午才理清楚。

避坑指南:我曾经因为把 PRIVATE 写成了 PUBLIC,导致内部实现头文件暴露给了外部用户。用户不小心包含了它,结果我们改内部结构时,他们的编译就炸了。所以——能用 PRIVATE 就别用 PUBLIC

4.3 target_link_libraries:把目标粘在一起

一个库很少是孤立的。它通常会依赖其他库。这时候就需要 target_link_libraries 了。

target_link_libraries(my_app
    PUBLIC
        my_lib
        fmt::fmt
    PRIVATE
        zlib
)

这个命令的作用是:告诉链接器,my_app 需要链接 my_libfmtzlib

你可能会问:为什么还要分 PUBLIC 和 PRIVATE?

原因很简单——传递性。如果 my_app 是一个库,它 PUBLIC 链接了 my_lib,那么任何链接 my_app 的目标也会自动链接 my_lib。这叫做“依赖传递”。

我个人习惯是:只在最顶层可执行文件里用 PRIVATE,库之间尽量用 PUBLIC 或 INTERFACE,这样依赖关系更清晰。

核心要点:target_link_libraries 不只是链接库,它还会传递头文件路径、编译定义、编译选项等属性。这就是 CMake 的“属性传递”机制。

4.4 属性设置:set_target_properties 与 get_target_property

除了上面三个命令,CMake 还提供了更通用的属性设置方式——set_target_properties

set_target_properties(my_lib
    PROPERTIES
        CXX_STANDARD 17
        CXX_STANDARD_REQUIRED ON
        POSITION_INDEPENDENT_CODE ON
)

这里我设置了三个属性:

  • CXX_STANDARD:指定 C++ 标准版本。
  • CXX_STANDARD_REQUIRED:强制要求编译器支持该标准。
  • POSITION_INDEPENDENT_CODE:生成位置无关代码,动态库必须开启。

如果你想读取某个属性,可以用 get_target_property

get_target_property(my_std my_lib CXX_STANDARD)
message(STATUS "my_lib 的 C++ 标准是: ${my_std}")

我在项目中遇到过一个问题:有人用 set(CMAKE_CXX_STANDARD 17) 全局设置,结果不同目标需要不同标准时,就乱套了。后来我改成每个目标单独设置属性,问题就解决了。

小技巧:常用的属性还有 OUTPUT_NAME(修改输出文件名)、VERSIONSOVERSION(动态库版本号)、COMPILE_DEFINITIONS(编译宏)。你可以用 cmake --help-property-list 查看所有属性。

4.5 知识体系图:目标与属性的关系

下面这张图展示了本章的核心逻辑——目标如何通过属性与外部交互:

my_lib (静态库目标) 源文件列表 src/lib.cpp PUBLIC 头文件 include/ 链接依赖 fmt::fmt 属性设置 CXX_STANDARD=17 源文件输入 头文件输出 依赖输入 属性配置

从这张图可以看得很清楚:一个目标接收源文件和依赖,输出头文件和库文件,同时受属性控制。这就是 CMake 构建系统的核心模型。

4.6 实战建议:如何组织你的 CMakeLists.txt

最后,我分享一个我在实际项目中用的组织方式:

# 1. 定义源文件
set(MYLIB_SOURCES src/core.cpp src/io.cpp)

# 2. 创建目标
add_library(mylib STATIC ${MYLIB_SOURCES})

# 3. 设置头文件路径
target_include_directories(mylib
    PUBLIC  include
    PRIVATE src
)

# 4. 设置编译属性
set_target_properties(mylib PROPERTIES
    CXX_STANDARD 20
    CXX_STANDARD_REQUIRED ON
)

# 5. 链接依赖
target_link_libraries(mylib
    PUBLIC  fmt::fmt
    PRIVATE zlib
)

这个顺序是我个人习惯——先有目标,再配属性,最后链接依赖。逻辑清晰,不容易漏掉东西。

总结一下:add_library 创建目标,target_include_directories 管理头文件可见性,target_link_libraries 处理依赖传递,set_target_properties 做精细化控制。这四个命令,基本覆盖了日常 90% 的 CMake 目标操作。

嗯,今天就聊到这儿。记住一句话:用目标思维写 CMake,而不是用目录思维写 CMake。你会发现,项目越大,这种写法越省心。