8. 链接库:target_link_libraries、静态库与动态库、链接顺序问题
链接库这件事,说白了就是让你的代码能调用别人写好的功能。不管是系统自带的库,还是你自己编译出来的库,CMake 都有一套清晰的管理方式。我个人习惯把链接库分为三类:静态库、动态库、以及接口库(header-only 库)。今天咱们重点聊前两种,以及那个让人头疼的链接顺序问题。
8.1 target_link_libraries 的基本用法
这个命令是 CMake 里最常用的命令之一。它的作用很简单:告诉链接器,这个 target 需要链接哪些库。
target_link_libraries(myapp
PRIVATE
mylib
pthread
PUBLIC
some_public_lib
INTERFACE
header_only_lib
)
这里的关键是 PRIVATE、PUBLIC、INTERFACE 这三个关键字。我刚开始用 CMake 时也搞混过,后来总结了一个简单的记忆方法:
- PRIVATE:自己用,不传给依赖你的人
- PUBLIC:自己用,也传给依赖你的人
- INTERFACE:自己不直接用,但依赖你的人需要
核心原则:头文件里引用了某个库,就用 PUBLIC;源文件里引用,用 PRIVATE;纯头文件库,用 INTERFACE。
8.2 静态库 vs 动态库
静态库和动态库的区别,其实就是一个「打包」和「引用」的关系。静态库在链接阶段直接复制到可执行文件里,动态库则在运行时加载。
| 特性 | 静态库 (.a / .lib) | 动态库 (.so / .dll) |
|---|---|---|
| 链接时机 | 编译时 | 运行时 |
| 文件大小 | 可执行文件变大 | 可执行文件较小 |
| 部署 | 单文件,方便 | 需要附带 .so/.dll |
| 更新 | 需要重新编译 | 替换库文件即可 |
| 内存占用 | 每个进程一份 | 多进程共享一份 |
我在项目中遇到过这样一个场景:一个嵌入式项目,目标系统空间只有 2MB。用静态库编译出来的可执行文件 1.8MB,差点放不下。后来改成动态库,可执行文件只有 400KB,库文件 1.2MB,但多个进程可以共享,反而更省空间。嗯,这里要注意,不是所有场景都适合动态库。
8.3 创建静态库和动态库
CMake 里创建库很简单,用 add_library 就行:
# 静态库
add_library(mylib STATIC
src/mylib.cpp
src/utils.cpp
)
# 动态库
add_library(mylib SHARED
src/mylib.cpp
src/utils.cpp
)
你可能会问:能不能同时生成静态库和动态库?当然可以,但要注意命名冲突:
add_library(mylib_static STATIC src/mylib.cpp)
add_library(mylib_shared SHARED src/mylib.cpp)
# 设置输出名称相同
set_target_properties(mylib_static PROPERTIES OUTPUT_NAME "mylib")
set_target_properties(mylib_shared PROPERTIES OUTPUT_NAME "mylib")
小技巧:用 OBJECT 库可以避免重复编译。先把源文件编译成目标文件,再分别链接到静态库和动态库。
8.4 链接顺序问题
这是 C/C++ 链接里最坑的地方,没有之一。我曾经花了一整天调试一个链接错误,最后发现只是库的顺序不对。
链接器处理库的顺序是从左到右。如果一个库 A 依赖库 B,那么 A 必须出现在 B 的左边:
# 正确:app 依赖 libA,libA 依赖 libB
target_link_libraries(app
libA
libB
)
# 错误:链接器会报 undefined reference
target_link_libraries(app
libB
libA
)
为什么会这样?因为链接器在处理 libA 时,发现它需要 libB 里的符号,但此时 libB 还没被处理。等处理到 libB 时,那些符号需求已经被丢弃了。
避坑指南:我曾经在大型项目里遇到循环依赖——libA 依赖 libB,libB 又依赖 libA。这种情况需要重新设计架构,或者用 --start-group 和 --end-group 来强制链接器多次扫描。
CMake 其实会自动处理大部分链接顺序问题。只要你用 target_link_libraries 正确声明了依赖关系,CMake 会帮你排好序。但如果你直接传递 -l 参数,那就得自己小心了。
8.5 知识体系图
下面这张图展示了链接库的核心概念和它们之间的关系:
8.6 实战建议
最后,分享几个我在实际项目中的经验:
- 优先用 PRIVATE:能少暴露就少暴露,这样改依赖时影响范围最小。
- 动态库的符号可见性:在 Linux 上用 -fvisibility=hidden 控制导出符号,Windows 上默认就是隐藏的。
- 链接顺序别硬编码:让 CMake 帮你管理,除非你很清楚自己在做什么。
- 用 --no-undefined 检查:在构建动态库时加上这个选项,可以提前发现未定义的符号。
个人习惯:我一般会在项目的根 CMakeLists.txt 里设置全局的链接选项,比如 set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--no-undefined"),这样所有动态库都能受益。
链接库这件事,说简单也简单,说复杂也复杂。关键是要理解背后的原理,而不是死记硬背命令。你想想看,一旦搞懂了链接器的工作方式,那些奇怪的链接错误就不再神秘了。
公众号:蓝海资料掘金营,微信deep3321