第11章:库的构建——静态库、动态库、对象库、接口库、导入库、库的链接

说到库,我估计很多刚接触CMake的朋友第一反应就是「哦,就是编译成.a或者.so文件嘛」。嗯,对,但不全对。CMake里对库的管理,其实比你想的要丰富得多。我在一个大型嵌入式项目中就吃过亏——当时只用了静态库,结果链接时符号冲突搞得我焦头烂额。后来才意识到,CMake提供了好几种库类型,各有各的适用场景。

这一章,我们就来把这些库类型挨个捋一遍。说白了,就是让你知道什么时候该用哪种库,以及怎么在CMake里把它们组织好。

11.1 静态库与动态库

这是最基础的两种库类型。静态库在链接时被直接复制到可执行文件中,动态库则在运行时加载。我个人习惯是:小工具用静态库,大型项目用动态库。为什么?你想想看,静态库虽然部署简单,但每个可执行文件都拷贝一份,磁盘和内存都浪费。

在CMake中,创建静态库和动态库非常简单:

add_library(my_static_lib STATIC src/lib.cpp)
add_library(my_shared_lib SHARED src/lib.cpp)

这里有个坑——STATIC和SHARED是关键字,必须大写。我曾经见过同事写成小写,结果CMake报错半天没反应过来。

小技巧:如果你不确定用哪种,可以先不指定类型,让CMake根据BUILD_SHARED_LIBS变量来决定。这样在调试时用静态库,发布时切到动态库,非常灵活。

11.2 对象库

对象库是个好东西。它不生成最终的库文件,而是只编译出目标文件(.o或.obj)。然后你可以把这些目标文件链接到多个目标中,避免重复编译。

我记得在一个游戏引擎项目中,核心算法模块被多个可执行文件和动态库同时使用。如果编译成静态库,每个目标都有一份拷贝,调试时符号表混乱。用对象库就完美解决了这个问题。

add_library(core_algorithms OBJECT src/algorithm1.cpp src/algorithm2.cpp)

add_executable(game_tool main.cpp $<TARGET_OBJECTS:core_algorithms>)
add_library(game_engine SHARED engine.cpp $<TARGET_OBJECTS:core_algorithms>)

注意那个$<TARGET_OBJECTS:core_algorithms>语法,这是生成器表达式。说白了,就是在构建时把对象库的目标文件展开到这里。

注意:对象库不能直接用于链接(不能出现在target_link_libraries中),只能通过$<TARGET_OBJECTS>引用。这是很多人容易搞混的地方。

11.3 接口库

接口库是个「虚」库——它不编译任何源文件,只传递编译选项、头文件路径和链接依赖。我最早接触接口库是在做跨平台UI库的时候,不同平台需要不同的编译宏和链接库,接口库帮我省了不少事。

add_library(platform_support INTERFACE)
target_include_directories(platform_support INTERFACE include/)
target_compile_definitions(platform_support INTERFACE PLATFORM_SUPPORT=1)
target_link_libraries(platform_support INTERFACE pthread dl)

然后其他目标只需要链接这个接口库,就能自动获得所有依赖:

add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE platform_support)

嗯,这里要注意:接口库的INTERFACE关键字表示这些属性只对链接者生效,对接口库本身无效(因为它根本没有编译过程)。

11.4 导入库

导入库用于引用已经构建好的、外部的库文件。比如你下载了一个第三方库的二进制包,里面有头文件和.a/.so文件,就可以用导入库来告诉CMake怎么用它们。

add_library(third_party_lib UNKNOWN IMPORTED)
set_target_properties(third_party_lib PROPERTIES
    IMPORTED_LOCATION /path/to/libthird_party.so
    INTERFACE_INCLUDE_DIRECTORIES /path/to/include
)

这里UNKNOWN表示让CMake自动检测库类型。你也可以明确指定STATICSHARED。我个人建议尽量指定,因为有些场景下自动检测会出错。

核心要点:导入库只是给CMake一个「描述」,告诉它这个库在哪里、头文件在哪里。它不会编译任何东西,也不会复制任何文件。

11.5 库的链接

链接库是CMake中最常见的操作之一。但很多人只用了target_link_libraries的基本用法,忽略了它的三个关键字:PUBLICPRIVATEINTERFACE

这三个关键字决定了依赖的传递方式:

关键字 对当前目标 对链接当前目标的目标
PRIVATE 链接 不传递
PUBLIC 链接 传递
INTERFACE 不链接 传递

举个例子:假设你有一个日志库log_lib,一个网络库net_lib依赖日志库,一个应用app依赖网络库。

add_library(log_lib STATIC log.cpp)
add_library(net_lib STATIC net.cpp)
target_link_libraries(net_lib PUBLIC log_lib)  # app也会链接log_lib

add_executable(app main.cpp)
target_link_libraries(app PRIVATE net_lib)     # app会链接net_lib和log_lib

如果net_liblog_lib使用PRIVATE,那么app就不会自动链接log_lib。这在大型项目中非常有用——你可以精确控制哪些依赖是内部实现细节,哪些是公开接口。

避坑指南:我曾经在一个项目中把所有链接都写成PUBLIC,结果依赖关系像蜘蛛网一样复杂。后来重构时花了整整两天才理清。建议你从一开始就养成习惯:能用PRIVATE就用PRIVATE,只有确实需要传递时才用PUBLIC。

11.6 知识体系总览

下面这张图总结了本章的核心内容,你可以把它当作一个快速参考:

CMake库类型与链接体系 库的构建 静态库 (STATIC) 动态库 (SHARED) 对象库 (OBJECT) 接口库 (INTERFACE) 导入库 (IMPORTED) 链接方式:target_link_libraries PRIVATE PUBLIC INTERFACE 选择原则:内部实现用PRIVATE,公开接口用PUBLIC,纯头文件用INTERFACE

这张图把五种库类型和三种链接方式放在了一起。你可以看到,静态库和动态库是最常用的,对象库适合避免重复编译,接口库用于传递依赖,导入库用于引用外部二进制。链接时,PRIVATEPUBLICINTERFACE三个关键字决定了依赖的传递范围。

好了,关于库的构建就讲到这里。记住一点:没有银弹。每种库类型都有它的适用场景,关键是根据项目需求来选择。我在实际项目中,经常是静态库、动态库、对象库混合使用——核心算法用对象库,公共模块用动态库,小工具用静态库。灵活搭配,才能发挥CMake的真正威力。


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