13、库的构建:add_library()详解、STATIC/SHARED/MODULE/OBJECT库、INTERFACE库(头文件库)、库的别名
说到构建库,这大概是CMake里最核心的操作之一了。我刚开始用CMake那会儿,觉得add_library()不就是把源文件打包一下嘛,有啥好研究的?后来被坑了几次才明白——这里面的门道,比你想象的多得多。
今天咱们就把add_library()这个命令彻底掰开揉碎。从最基本的静态库、动态库,到你可能不太熟悉的MODULE库、OBJECT库,再到那个“只传头文件不编译”的INTERFACE库,最后聊聊库的别名机制。嗯,内容不少,但都很实用。
13.1 add_library()的基本用法
先看最基础的调用方式:
add_library(my_lib STATIC
src/utils.cpp
src/core.cpp
src/io.cpp
)
这行代码告诉CMake:我要创建一个叫my_lib的库,类型是STATIC,源文件是后面那三个cpp文件。就这么简单。
但这里有个细节——库的名字。CMake会帮你生成对应的文件名:Linux下是libmy_lib.a,Windows下是my_lib.lib。如果你想要自定义输出名,可以用OUTPUT_NAME属性:
set_target_properties(my_lib PROPERTIES
OUTPUT_NAME "custom_name"
)
我个人习惯把库名和项目名保持一致,这样在大型项目中找起来方便。当然,这只是个人偏好,没有硬性规定。
13.2 四种库类型:STATIC / SHARED / MODULE / OBJECT
add_library()的第一个参数除了库名,第二个参数就是库类型。CMake支持四种类型,每种都有自己的适用场景。
| 类型 | 生成产物 | 链接方式 | 典型用途 |
|---|---|---|---|
| STATIC | .a (Linux) / .lib (Windows) | 编译时静态链接 | 小型工具库、内部模块 |
| SHARED | .so (Linux) / .dll (Windows) | 运行时动态加载 | 公共接口库、插件系统 |
| MODULE | .so (Linux) / .dll (Windows) | 运行时手动加载 | 插件、动态模块 |
| OBJECT | .o / .obj 文件集合 | 不链接,直接合并 | 避免重复编译、大型项目拆分 |
STATIC库:说白了就是把所有目标文件打包成一个归档文件。链接时,链接器会把需要的代码直接复制到可执行文件中。优点是部署简单,一个可执行文件搞定;缺点是如果多个程序都用同一个静态库,内存里会有多份拷贝。
SHARED库:这是最常用的动态库。程序运行时才加载,多个程序可以共享同一份代码。我遇到过一个问题:在Windows上构建SHARED库时,记得要正确导出符号。否则别的程序根本调用不了你的函数。解决办法是用dllexport和dllimport,或者用CMake的GenerateExportHeader模块自动生成。
MODULE库:这个和SHARED有点像,但有个关键区别——MODULE库不会被链接器自动链接。你需要用dlopen()或LoadLibrary()手动加载。这玩意儿最适合做插件系统。我曾经在一个图像处理框架里用过,每个滤镜都是一个MODULE库,主程序运行时扫描目录加载所有插件。嗯,效果不错。
OBJECT库:这个比较特殊。它不生成最终的库文件,而是生成一堆目标文件(.o或.obj)。你可以把这些目标文件“喂”给其他库或可执行文件。好处是避免了重复编译。举个例子:
add_library(my_objects OBJECT
src/common.cpp
src/helpers.cpp
)
add_library(static_lib STATIC $<TARGET_OBJECTS:my_objects>)
add_library(shared_lib SHARED $<TARGET_OBJECTS:my_objects>)
这样common.cpp和helpers.cpp只编译一次,然后被两个库共用。我在一个需要同时提供静态库和动态库的项目里用过这招,编译时间直接砍半。
13.3 INTERFACE库:头文件库
INTERFACE库是个很有意思的存在。它不编译任何源文件,也不生成任何二进制产物。它存在的唯一目的——传递依赖。
你想想看,有时候你有一个纯头文件的库,比如一些模板库或者只包含内联函数的库。传统做法是让使用者自己加include_directories(),但这样很麻烦,也不够优雅。
INTERFACE库就是来解决这个问题的:
add_library(my_header_lib INTERFACE)
target_include_directories(my_header_lib INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
target_compile_features(my_header_lib INTERFACE cxx_std_17)
然后别的目标只需要:
target_link_libraries(my_app PRIVATE my_header_lib)
所有头文件路径、编译特性、甚至依赖的其他库,都会自动传递过去。干净利落。
我个人特别喜欢用INTERFACE库来封装第三方库。比如你项目里用了Boost,与其让每个子模块都去找Boost,不如创建一个INTERFACE库:
add_library(boost_wrapper INTERFACE)
target_link_libraries(boost_wrapper INTERFACE
Boost::filesystem
Boost::system
)
target_compile_definitions(boost_wrapper INTERFACE
BOOST_ALL_NO_LIB
)
这样整个项目只需要链接boost_wrapper就行了。哪天想换掉Boost,也只需要改这一个地方。
13.4 库的别名
别名机制是CMake 3.11引入的。说白了就是给库起个“外号”。为什么要这么做?
想象一下这个场景:你写了一个库叫my_awesome_library,然后在项目里到处用target_link_libraries(... my_awesome_library)。突然有一天,你想把这个库换成另一个实现,或者想把它拆成多个子库。这时候你就得满世界找引用,改起来很痛苦。
别名可以帮你解耦:
add_library(my_awesome_library STATIC src/core.cpp)
add_library(MyProject::core ALIAS my_awesome_library)
然后所有使用者都链接MyProject::core。将来你想换实现,只需要改别名指向的目标:
add_library(new_core STATIC src/new_core.cpp)
add_library(MyProject::core ALIAS new_core)
所有引用MyProject::core的地方自动生效。是不是很爽?
另外,别名还有一个好处——命名空间。用ProjectName::TargetName这种格式,可以避免名字冲突。我在一个大型项目里见过几十个库,如果没有命名空间,光是想不重名就够头疼的。
13.5 知识体系图
下面这张图总结了本章的核心内容,方便你快速回顾:
好了,关于add_library()的内容就讲到这里。每种库类型都有它的用武之地,关键是要理解它们的特性,然后在合适的场景选择合适的类型。我曾经在一个项目里把所有库都设成STATIC,结果最后链接出来的可执行文件有200多MB...嗯,从那以后我再也不敢无脑用STATIC了。
记住:工具是死的,人是活的。理解原理,灵活运用,这才是CMake的正确打开方式。
公众号:蓝海资料掘金营,微信deep3321