第六章 库的创建与链接: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_utilsnetwork_layer 这种。

小技巧: 如果你不确定用静态还是动态,先默认用 STATIC。静态库编译简单,部署也省心。等后面需要动态加载或者减小体积时,再改成 SHARED。

6.2 静态库 vs 动态库

咱们来对比一下这两者的区别。我整理了一个表格,方便你对照着看。

对比项 静态库 (.a / .lib) 动态库 (.so / .dll / .dylib)
链接时机 编译时直接嵌入可执行文件 运行时才加载
文件大小 可执行文件变大 可执行文件小,库单独存在
部署 一个文件搞定 需要带上 .so 或 .dll 文件
更新 需要重新编译整个程序 只替换库文件即可
内存占用 每个进程都有一份拷贝 多个进程可共享同一份

我在项目中遇到过这样一个坑:用静态库的时候,如果两个静态库都用了同一个第三方库,链接时会报符号冲突。那时候排查了好久才发现是重复定义了。后来改用动态库就没事了,因为动态库的符号是运行时解析的,不会冲突。

警告: 动态库虽然灵活,但有个著名的「DLL Hell」问题。你更新了动态库,但程序还是用旧版本的接口,就会崩溃。所以动态库的接口一定要做好版本管理。

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
)

这里有个关键点:PRIVATEPUBLICINTERFACE 这三个关键字。我刚开始学的时候也搞混过,后来总结了一句话:

  • PRIVATE:我自己用,不告诉别人
  • PUBLIC:我自己用,也告诉别人
  • INTERFACE:我自己不用,但告诉别人

举个例子。假设你写了一个数学库 math_lib,它内部用了 log_lib 来做日志。那么:

  • 如果 log_lib 只是 math_lib 内部实现细节,外部不需要知道 → 用 PRIVATE
  • 如果 math_lib 的头文件里引用了 log_lib 的类型,外部也必须知道 → 用 PUBLIC
  • 如果 math_lib 是纯头文件库,不需要编译,但外部需要它的依赖 → 用 INTERFACE
核心原则: 能用 PRIVATE 就用 PRIVATE。暴露越少,依赖越清晰,编译也越快。

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 知识体系图

下面这张图总结了本章的核心内容,你可以对照着梳理一下思路。

库的创建与链接 add_library() 静态库 vs 动态库 target_link_libraries() STATIC / SHARED / MODULE 源文件列表 库命名规范 编译时 vs 运行时 文件大小与部署 更新与内存占用 PRIVATE / PUBLIC / INTERFACE 依赖传递 可见性控制 核心原则:最小暴露原则 能用 PRIVATE 就用 PRIVATE,依赖越少越清晰

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