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:最常用的传递方式
当库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_lib的include/目录是对外接口,所以用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的生成器表达式,那玩意儿在条件编译和跨平台构建里特别有用。