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库时,记得要正确导出符号。否则别的程序根本调用不了你的函数。解决办法是用dllexportdllimport,或者用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.cpphelpers.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这种格式,可以避免名字冲突。我在一个大型项目里见过几十个库,如果没有命名空间,光是想不重名就够头疼的。

注意:别名只能用于同一个目录下的目标。你不能给其他目录的库创建别名。另外,IMPORTED库(从外部导入的库)也不能用ALIAS。

13.5 知识体系图

下面这张图总结了本章的核心内容,方便你快速回顾:

add_library() 库类型 STATIC SHARED MODULE OBJECT INTERFACE库 纯头文件库 传递依赖 库别名 命名空间 解耦 核心要点 • STATIC:编译时链接,产物独立 • SHARED:运行时加载,节省内存 • MODULE:手动加载,适合插件 • OBJECT:目标文件集合,避免重复编译 • INTERFACE:头文件库,传递依赖 • ALIAS:别名机制,解耦与命名空间
我的建议:在实际项目中,优先使用SHARED库(除非你有特殊理由)。STATIC库虽然部署简单,但会导致二进制体积膨胀。INTERFACE库是管理头文件依赖的利器,强烈推荐。别名机制虽然好用,但别滥用——只在需要解耦的地方使用,否则反而增加复杂度。

好了,关于add_library()的内容就讲到这里。每种库类型都有它的用武之地,关键是要理解它们的特性,然后在合适的场景选择合适的类型。我曾经在一个项目里把所有库都设成STATIC,结果最后链接出来的可执行文件有200多MB...嗯,从那以后我再也不敢无脑用STATIC了。

记住:工具是死的,人是活的。理解原理,灵活运用,这才是CMake的正确打开方式。


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