7、CMake目标管理:add_library()静态库与动态库、target_include_directories()、target_link_libraries()、PUBLIC/PRIVATE/INTERFACE

好,咱们今天聊CMake里最核心的一块——目标管理。说白了,就是怎么把源代码组织成库,怎么告诉编译器头文件在哪,怎么把库链接到一起。我刚开始用CMake时,觉得这不就是几个命令嘛,后来踩了不少坑才明白,这里面的PUBLIC、PRIVATE、INTERFACE这三个关键字,才是真正的精髓。

7.1 add_library():创建你的第一个库

先看最基础的。你想把一堆源文件打包成一个库,用add_library()就行。

# 创建一个静态库
add_library(my_lib STATIC
    src/utils.cpp
    src/core.cpp
)

# 创建一个动态库
add_library(my_shared SHARED
    src/utils.cpp
    src/core.cpp
)

STATIC生成的是.a.lib,SHARED生成的是.so.dll。我个人习惯是:小工具用静态库,方便分发;大型项目或者需要插件机制的,用动态库。

注意:Windows下动态库需要导出符号。你可以在源文件里用__declspec(dllexport),或者用CMake的GenerateExportHeader模块自动生成。我曾经在Windows上折腾了一下午,就是因为忘了导出符号,链接时一堆 unresolved external symbol。

7.2 target_include_directories():告诉编译器头文件在哪

库写好了,别人要用它,总得知道头文件在哪吧?target_include_directories()就是干这个的。

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

# 给 my_lib 自己用的头文件路径
target_include_directories(my_lib PRIVATE
    ${CMAKE_CURRENT_SOURCE_DIR}/src
)

# 给使用 my_lib 的目标提供的头文件路径
target_include_directories(my_lib PUBLIC
    ${CMAKE_CURRENT_SOURCE_DIR}/include
)

这里出现了PRIVATE和PUBLIC。什么意思呢?

  • PRIVATE:头文件只给这个库自己用。外部目标链接这个库时,不会自动添加这个路径。
  • PUBLIC:头文件既给库自己用,也给链接这个库的目标用。
  • INTERFACE:头文件只给外部目标用,库自己编译时不需要。

你想想看,如果你的库内部实现用了某个第三方头文件,但对外暴露的接口里没有引用它,那就用PRIVATE。如果对外接口里引用了,那就用PUBLIC。INTERFACE比较特殊,后面再说。

7.3 target_link_libraries():把库串起来

项目大了,库之间会有依赖。A库依赖B库,B库依赖C库。怎么管理?用target_link_libraries()

add_library(A STATIC a.cpp)
add_library(B STATIC b.cpp)
add_library(C STATIC c.cpp)

# B 依赖 C
target_link_libraries(B PUBLIC C)

# A 依赖 B
target_link_libraries(A PUBLIC B)

这里的关键还是PUBLIC、PRIVATE、INTERFACE。它们决定了依赖关系的传递方式。

7.4 PUBLIC / PRIVATE / INTERFACE:三个关键字的深度解析

这三个关键字,说白了就是控制「可见性」和「传递性」。我画个图帮你理解。

PUBLIC / PRIVATE / INTERFACE 依赖传递示意图 库 A (可执行文件) 库 B 库 C PUBLIC PUBLIC 依赖传递效果 🔹 A 链接 B (PUBLIC) → A 能看到 B 的 PUBLIC 头文件,也能链接 B 的依赖 🔹 B 链接 C (PUBLIC) → A 自动也能看到 C 的 PUBLIC 头文件,并链接 C 🔹 如果 B 链接 C 用 PRIVATE → A 看不到 C,也不会链接 C 🔹 如果 B 链接 C 用 INTERFACE → B 自己编译时不链接 C,但 A 会链接 C

嗯,这张图应该能帮你理清思路。我再说说实际项目里怎么用。

PUBLIC:最常用的传递方式

当库B的对外接口中直接使用了库C的类型,那么B链接C时应该用PUBLIC。这样,任何链接B的目标,都会自动获得C的头文件和库文件。省心。

PRIVATE:隐藏内部实现

如果B只是在内部实现里用了C,对外接口完全看不到C的影子,那就用PRIVATE。这样做的好处是:外部目标不会意外依赖C,编译更快,耦合更低。

我的经验:能用PRIVATE就用PRIVATE。我见过太多项目,因为滥用PUBLIC,导致一个小小的改动就要重新编译半个项目。PRIVATE就像封装,把实现细节藏起来,只暴露必要的接口。

INTERFACE:纯头文件库或接口库

这个比较特殊。INTERFACE表示:这个依赖关系只对使用方生效,库自己编译时不需要。典型场景是头文件库(header-only library)或者接口库。

# 纯头文件库,没有源文件
add_library(my_header_only INTERFACE)
target_include_directories(my_header_only INTERFACE
    ${CMAKE_CURRENT_SOURCE_DIR}/include
)

# 另一个库使用它
add_library(my_lib STATIC src/core.cpp)
target_link_libraries(my_lib PUBLIC my_header_only)

这里my_header_only没有源文件,它只是一个「接口」。链接它的目标会获得头文件路径,但不会链接任何二进制文件。

7.5 实战中的组合使用

来,看一个稍微完整的例子。假设我们有一个项目,结构如下:

project/
├── CMakeLists.txt
├── include/
│   └── math/
│       └── vector.h
├── src/
│   ├── vector.cpp
│   └── main.cpp
└── third_party/
    └── eigen/

CMakeLists.txt 可以这样写:

cmake_minimum_required(VERSION 3.16)
project(MathLib VERSION 1.0.0 LANGUAGES CXX)

# 创建数学库
add_library(math_lib STATIC
    src/vector.cpp
)

# 库内部实现用到了 Eigen,但对外接口不暴露
target_include_directories(math_lib PRIVATE
    ${CMAKE_CURRENT_SOURCE_DIR}/third_party/eigen
)

# 对外暴露的头文件
target_include_directories(math_lib PUBLIC
    ${CMAKE_CURRENT_SOURCE_DIR}/include
)

# 创建可执行文件
add_executable(math_app src/main.cpp)

# 链接库
target_link_libraries(math_app PRIVATE math_lib)

这里的关键点:

  • math_lib内部用Eigen,但对外接口不暴露,所以用PRIVATE。
  • math_libinclude/目录是对外接口,所以用PUBLIC。
  • math_app链接math_lib时用PRIVATE,因为math_app不需要把自己的依赖再往下传。

我曾经踩过的坑:有一次我把Eigen的头文件路径设成了PUBLIC,结果所有链接math_lib的目标都自动包含了Eigen。后来Eigen升级,接口变了,导致几十个目标重新编译,还报了一堆编译错误。从那以后,我对PUBLIC的使用就格外谨慎。

7.6 总结一下

CMake的目标管理,核心就是三个关键字:

关键字 对当前目标 对链接当前目标的目标 典型场景
PRIVATE 生效 不生效 内部实现依赖,对外隐藏
PUBLIC 生效 生效 对外接口依赖,需要传递
INTERFACE 不生效 生效 纯头文件库、接口库

记住一个原则:最小化PUBLIC,优先PRIVATE,必要时用INTERFACE。这样你的项目编译更快,依赖更清晰,维护起来也省心。

好,这一章就到这。下一章我们聊聊CMake的生成器表达式,那玩意儿在条件编译和跨平台构建里特别有用。