6、库的构建与管理:静态库与动态库的创建、BUILD_SHARED_LIBS变量、库的版本号与SOVERSION

说到库,我估计你脑子里马上会蹦出两个词:静态库和动态库。嗯,这俩兄弟在C++世界里几乎无处不在。今天我们就来聊聊,在CMake里怎么优雅地创建它们,怎么控制版本,以及那些我踩过的坑。

6.1 静态库 vs 动态库:先搞清楚你要什么

静态库(.a.lib)说白了就是把一堆目标文件打包成一个档案。链接时,链接器会把需要的代码直接拷贝到最终的可执行文件里。好处是部署简单,一个二进制文件拿走就能跑。坏处也很明显:每个可执行文件都有一份库的副本,磁盘和内存都浪费。

动态库(.so.dylib.dll)则是运行时加载的。多个程序可以共享同一份库代码,更新库时只要替换文件就行,不用重新链接所有程序。但你也得小心——动态库版本不匹配导致的“找不到符号”错误,我当年可是被折磨过好几次。

我个人习惯是:工具类、基础算法库做成静态库,方便集成;插件、公共运行时组件做成动态库,方便独立升级。当然,这没有绝对标准,看你的项目场景。

6.2 用CMake创建静态库和动态库

CMake创建库非常简单,核心就是 add_library 命令。看个例子:

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

# 创建一个动态库
add_library(my_shared_lib SHARED
    src/plugin.cpp
    src/interface.cpp
)

这里的关键字 STATICSHARED 直接告诉CMake你要生成什么类型的库。如果你不写这两个关键字,CMake会默认生成静态库——除非你设置了 BUILD_SHARED_LIBS 变量。

重要提醒: 如果你在Windows上构建动态库,记得在导出类或函数前加上 __declspec(dllexport) / __declspec(dllimport)。CMake的 GenerateExportHeader 模块可以帮你自动生成这些宏,省去手动维护的麻烦。

6.3 BUILD_SHARED_LIBS:一个变量切换库类型

有时候你希望同一个项目既能生成静态库,也能生成动态库,但不想改CMakeLists.txt里的 add_library 调用。这时候 BUILD_SHARED_LIBS 就派上用场了。

它的用法很简单:

# 在CMake命令行中设置
cmake -DBUILD_SHARED_LIBS=ON ..

# 或者在CMakeLists.txt中设置(不推荐,会覆盖用户选择)
set(BUILD_SHARED_LIBS ON)

当你把 BUILD_SHARED_LIBS 设为 ON 时,所有没有显式指定 STATICSHAREDadd_library 调用,都会生成动态库。设为 OFF 则生成静态库。

我在项目中遇到过这样的情况:一个底层数学库,开发阶段用动态库方便调试,发布时切回静态库减少部署依赖。用 BUILD_SHARED_LIBS 配合 if 判断,一行代码都不用改,非常清爽。

小技巧: 如果你的库有多个,但只想让其中一部分受 BUILD_SHARED_LIBS 控制,可以显式指定 STATICSHARED 来覆盖全局设置。CMake会优先使用你显式指定的类型。

6.4 库的版本号与 SOVERSION:别让用户一脸懵

动态库的版本管理是个大学问。你想想看,用户升级了你的库,结果旧程序崩溃了,为什么?多半是因为ABI(应用程序二进制接口)不兼容。CMake提供了两个属性来帮你管理:VERSIONSOVERSION

  • VERSION:库的完整版本号,比如 1.2.3。它会被写入库文件的元数据中,通常用于 .so 文件的命名,比如 libfoo.so.1.2.3
  • SOVERSION:库的API/ABI兼容版本号,通常只包含主版本号,比如 1。它决定了运行时链接器查找哪个版本的库。如果 SOVERSION 变了,旧程序就无法加载新库。

看个实际例子:

add_library(my_shared_lib SHARED src/foo.cpp)

# 设置版本号
set_target_properties(my_shared_lib PROPERTIES
    VERSION 2.3.1
    SOVERSION 2
)

构建后,你会看到类似这样的文件结构:

libmy_shared_lib.so          # 软链接 -> libmy_shared_lib.so.2
libmy_shared_lib.so.2        # 软链接 -> libmy_shared_lib.so.2.3.1
libmy_shared_lib.so.2.3.1    # 实际库文件

运行时链接器会查找 libmy_shared_lib.so.2,而 libmy_shared_lib.so.2.3.1 是真正的实现。这样,如果你修复了一个bug,把版本升到 2.3.2,只要 SOVERSION 还是 2,旧程序就能无缝使用新库。

我曾经踩过的坑: 有一次我改了库的某个函数签名,但忘了更新 SOVERSION。结果用户升级后,程序在运行时直接崩溃,报“undefined symbol”。从那以后,我养成了一个习惯:只要改了公开头文件或ABI,必须递增 SOVERSION。哪怕只是加了一个新函数,也要考虑是否会影响现有调用。

6.5 版本号与 SOVERSION 的最佳实践

关于版本号怎么定,业界有个通用的规则:

版本号变化 含义 SOVERSION 是否递增
主版本号(如 1.x.x → 2.x.x) ABI不兼容,旧代码无法链接新库
次版本号(如 1.2.x → 1.3.x) ABI兼容,但新增了功能或接口 否(除非你破坏了向后兼容)
补丁版本号(如 1.2.3 → 1.2.4) 仅修复bug,接口不变

我个人建议:SOVERSION 设为主版本号。这样最清晰,也最安全。如果你不确定某个改动是否影响ABI,那就递增 SOVERSION。宁可让用户重新编译,也别让他们在运行时崩溃。

6.6 知识体系一览

下面这张图帮你梳理了本章的核心逻辑:从库的类型选择,到CMake构建,再到版本管理,一条线串起来。

库的构建与管理 静态库 (STATIC) .a / .lib 动态库 (SHARED) .so / .dylib / .dll BUILD_SHARED_LIBS VERSION 完整版本号 (1.2.3) SOVERSION ABI兼容版本 (1) ABI兼容性检查 选择策略

从图中你可以看到,整个体系其实就三个决策点:选类型(静态/动态)、控制全局开关(BUILD_SHARED_LIBS)、管理版本(VERSION / SOVERSION)。把这三点搞明白,库的构建管理你就掌握了八成。

6.7 避坑指南:我踩过的那些雷

  • Windows上的DLL导出:记得用 __declspec(dllexport),否则链接器会报“无法解析的外部符号”。CMake的 GenerateExportHeader 可以帮你自动生成,别手写。
  • 静态库不要设置 SOVERSION:静态库没有运行时版本概念,设了也没用,反而会让构建系统困惑。
  • 动态库的链接顺序:在Linux上,链接器从左到右解析符号。如果你把动态库放在依赖它的目标文件前面,可能会报“未定义引用”。我习惯把库放在最后面。
  • macOS上的 .dylib.so:macOS虽然也支持 .so,但官方推荐用 .dylib。CMake默认生成 .dylib,别强行改成 .so,否则 install_name_tool 可能不认。

最后一个小建议: 每次发布新版本前,运行一下 ldd(Linux)或 otool -L(macOS)检查你的动态库链接了哪些依赖。我曾经因为忘记更新 SOVERSION,导致用户升级后程序崩溃,排查了半天才发现是ABI不兼容。嗯,从那以后,我把版本检查写进了CI脚本里。

公众号:蓝海资料掘金营,微信deep3321