第六章 库的创建与链接:add_library(),静态库与动态库,target_link_libraries()
说到库,我估计很多刚接触CMake的朋友都会觉得有点抽象。说白了,库就是一堆编译好的代码,等着别人来调用。你写了一个排序算法,封装成库,别人拿来就能用,不用关心你里面怎么实现的。这就是库的价值。
我个人习惯把库分成两类:静态库和动态库。这两者在CMake里的创建方式几乎一样,但行为差别很大。咱们今天就把它们彻底讲清楚。
6.1 用 add_library() 创建库
先看最基本的用法。你想创建一个库,只需要告诉CMake三件事:库的名字、库的类型、以及哪些源文件属于这个库。
add_library(my_library STATIC
src/utils.cpp
src/algorithm.cpp
src/io.cpp
)
这里 STATIC 表示静态库。如果你改成 SHARED,那就是动态库。还有一种 MODULE,用得少,咱们先不提它。
嗯,这里要注意:库的名字不要乱起。我见过有人把库名叫成 lib 或者 test,结果后面链接的时候各种冲突。建议用项目名或者模块名做前缀,比如 core_utils、network_layer 这种。
6.2 静态库 vs 动态库
咱们来对比一下这两者的区别。我整理了一个表格,方便你对照着看。
| 对比项 | 静态库 (.a / .lib) | 动态库 (.so / .dll / .dylib) |
|---|---|---|
| 链接时机 | 编译时直接嵌入可执行文件 | 运行时才加载 |
| 文件大小 | 可执行文件变大 | 可执行文件小,库单独存在 |
| 部署 | 一个文件搞定 | 需要带上 .so 或 .dll 文件 |
| 更新 | 需要重新编译整个程序 | 只替换库文件即可 |
| 内存占用 | 每个进程都有一份拷贝 | 多个进程可共享同一份 |
我在项目中遇到过这样一个坑:用静态库的时候,如果两个静态库都用了同一个第三方库,链接时会报符号冲突。那时候排查了好久才发现是重复定义了。后来改用动态库就没事了,因为动态库的符号是运行时解析的,不会冲突。
6.3 用 target_link_libraries() 链接库
库创建好了,怎么让别人用?用 target_link_libraries()。这个命令的作用是告诉CMake:目标 A 需要链接目标 B。
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE
my_library
)
这里有个关键点:PRIVATE、PUBLIC、INTERFACE 这三个关键字。我刚开始学的时候也搞混过,后来总结了一句话:
- PRIVATE:我自己用,不告诉别人
- PUBLIC:我自己用,也告诉别人
- INTERFACE:我自己不用,但告诉别人
举个例子。假设你写了一个数学库 math_lib,它内部用了 log_lib 来做日志。那么:
- 如果
log_lib只是math_lib内部实现细节,外部不需要知道 → 用PRIVATE - 如果
math_lib的头文件里引用了log_lib的类型,外部也必须知道 → 用PUBLIC - 如果
math_lib是纯头文件库,不需要编译,但外部需要它的依赖 → 用INTERFACE
6.4 实战:一个完整的库项目
咱们来写一个完整的例子。假设你要做一个字符串工具库,包含一个静态库和一个测试程序。
# CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(StringUtils VERSION 1.0.0 LANGUAGES CXX)
# 创建静态库
add_library(string_utils STATIC
src/trim.cpp
src/split.cpp
src/join.cpp
)
# 指定头文件路径
target_include_directories(string_utils PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
# 创建测试程序
add_executable(test_string_utils
test/test_main.cpp
)
# 链接库
target_link_libraries(test_string_utils PRIVATE
string_utils
)
你看,整个流程很清晰:先创建库,再创建可执行文件,最后把库链接过去。这里我用了 target_include_directories 来指定头文件路径,这样外部使用者只需要 #include <string_utils/trim.h> 就能用了。
include/项目名/ 目录下,这样引用的时候路径清晰,不会和其他库冲突。比如 include/string_utils/trim.h,而不是直接 include/trim.h。
6.5 库的可见性与传递
库的链接关系是可以传递的。什么意思?就是 A 链接了 B,B 链接了 C,那么 A 可能间接用到了 C。这时候 PRIVATE/PUBLIC 就发挥作用了。
add_library(c STATIC c.cpp)
add_library(b STATIC b.cpp)
add_library(a STATIC a.cpp)
target_link_libraries(b PUBLIC c) # b 用 c,并且告诉别人
target_link_libraries(a PRIVATE b) # a 用 b,但不告诉别人
在这个例子里,a 能用到 c 吗?答案是:能。因为 b 把 c 声明为 PUBLIC,所以 a 链接 b 的时候,c 也被带进来了。但如果 b 把 c 声明为 PRIVATE,a 就链接不到 c。
我曾经在一个大项目里吃过这个亏。有个底层库改了依赖关系,从 PUBLIC 改成了 PRIVATE,结果上层十几个模块全部编译失败。从那以后,我每次改链接属性都会全局搜索一下,看看有没有被其他地方依赖。
6.6 知识体系图
下面这张图总结了本章的核心内容,你可以对照着梳理一下思路。
6.7 避坑指南
最后,我把自己踩过的坑总结一下,希望能帮你少走弯路。
- 静态库的链接顺序:在Linux上,静态库的链接顺序很重要。如果 A 依赖 B,那么 A 必须出现在 B 前面。我曾经因为顺序写反了,折腾了一下午才发现。
- 动态库的 RPATH:编译动态库时,记得设置
CMAKE_INSTALL_RPATH,否则运行时找不到 .so 文件。我习惯用$ORIGIN来指定相对路径。 - 头文件依赖:如果库的头文件变了,但库本身没变,CMake 可能不会重新编译。这时候可以手动
touch一下源文件,或者用add_dependencies强制重新生成。 - 跨平台路径:Windows 上动态库是 .dll,Linux 上是 .so,macOS 上是 .dylib。CMake 会自动处理这些差异,但你在写路径判断时要小心。
嗯,关于库的创建与链接,今天就讲到这里。记住一句话:库是代码的封装,链接是依赖的管理。把这两件事做好了,你的项目结构就会非常清晰。
公众号:蓝海资料掘金营,微信deep3321