18、对象库(Object Library):使用OBJECT类型库提高编译效率。
说实话,很多C++项目编译慢,不是代码量大,而是重复编译太多。
我见过一个团队,项目里十几个可执行文件,每个都链接同一套核心算法库。每次改一行公共头文件,所有目标都得重新编译一遍。那滋味,啧,等编译的时间够喝三杯咖啡了。
今天聊的对象库(OBJECT库),就是专门解决这个痛点的。
什么是对象库?
先看个直观对比。
传统静态库(STATIC库)长这样:
add_library(my_lib STATIC a.cpp b.cpp c.cpp)
add_executable(app1 main1.cpp)
target_link_libraries(app1 my_lib)
每次编译app1,链接器会把my_lib.a里的目标文件打包进可执行文件。如果同时编译app2,同样的目标文件又得重新打包一次。
对象库就不一样了:
add_library(my_objs OBJECT a.cpp b.cpp c.cpp)
add_executable(app1 main1.cpp $<TARGET_OBJECTS:my_objs>)
add_executable(app2 main2.cpp $<TARGET_OBJECTS:my_objs>)
关键区别在哪?对象库只编译,不归档。它生成的是一堆.o文件,直接喂给可执行文件。没有ar打包这一步,也没有解包这一步。
核心结论:对象库省去了「打包-解包」的中间环节。对于大型项目,编译时间能缩短20%-40%。
对象库的典型使用场景
我个人习惯在以下三种场景用对象库:
- 多个可执行文件共享同一组源文件——比如测试程序、主程序、工具程序都依赖同一套核心逻辑。
- 头文件驱动的模板库——模板实例化分散在多个翻译单元,用对象库可以避免重复实例化。
- 需要精细控制编译选项——对象库允许你给不同源文件设置不同的编译标志。
举个例子。我在做一个网络框架时,核心的协议解析模块被三个程序使用:服务端、客户端、压力测试工具。用静态库的话,每次改协议头文件,三个程序都得重新链接。换成对象库后,只有真正改了的源文件才重新编译,链接几乎是瞬时的。
对象库的完整用法
先看一个完整的CMakeLists.txt:
cmake_minimum_required(VERSION 3.12)
project(ObjectLibDemo)
# 创建对象库
add_library(core_objects OBJECT
parser.cpp
serializer.cpp
validator.cpp
)
# 给对象库设置编译选项
target_include_directories(core_objects PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
target_compile_definitions(core_objects PRIVATE CORE_BUILD=1)
# 第一个可执行文件
add_executable(server
server_main.cpp
$<TARGET_OBJECTS:core_objects>
)
target_link_libraries(server pthread)
# 第二个可执行文件
add_executable(client
client_main.cpp
$<TARGET_OBJECTS:core_objects>
)
target_link_libraries(client pthread)
注意那个$<TARGET_OBJECTS:core_objects>语法。这是CMake的生成器表达式,它会在构建时展开成对象库生成的所有.o文件路径。
小技巧:如果你用的是CMake 3.12以上版本,还可以用target_sources给对象库追加源文件,不用一次性列完。
对象库 vs 静态库 vs 动态库
我整理了一张对比表,方便你决策:
| 特性 | 对象库 | 静态库 | 动态库 |
|---|---|---|---|
| 编译产物 | .o文件集合 | .a/.lib | .so/.dll |
| 链接方式 | 直接注入 | 打包后解包 | 运行时加载 |
| 编译速度 | 快(无归档) | 中等 | 慢(需处理符号) |
| 内存占用 | 低 | 低 | 高(运行时) |
| 适用场景 | 多目标共享 | 通用库分发 | 插件/模块化 |
说白了,对象库就是「轻量级共享」方案。它不生成库文件,只生成中间产物。
避坑指南
我曾经在项目里踩过一个坑,分享给你。
对象库的源文件如果包含全局状态(比如单例、静态变量),多个可执行文件会各自持有一份副本。这跟静态库的行为一样,但跟动态库不同。如果你期望的是共享同一份全局状态,那就得用动态库。
注意:对象库不能直接用于target_link_libraries。你必须用$<TARGET_OBJECTS:...>语法显式引用。这是新手最容易犯的错。
还有一个细节:对象库的编译选项是独立的。你可以在对象库上设置target_compile_options,这些选项只影响对象库自己的源文件,不影响最终的可执行文件。反过来,可执行文件的编译选项也不会渗透到对象库。
对象库的进阶用法
对象库还能跟add_custom_command配合,做代码生成。比如:
add_library(generated_objects OBJECT)
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generated.cpp
COMMAND python3 ${CMAKE_CURRENT_SOURCE_DIR}/gen.py
DEPENDS gen.py
)
target_sources(generated_objects PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/generated.cpp)
这样生成的代码直接作为对象库的一部分,不用单独处理。
知识体系图
下面这张图帮你理清对象库在整个CMake构建体系中的位置:
从图里能看出来,对象库处在「编译完成」和「链接之前」的中间地带。它不参与归档,也不参与链接,纯粹是中间产物的集合。
什么时候不该用对象库?
嗯,这里要注意。对象库不是万能的。
- 你要发布库给别人用——对象库没法分发,别人拿不到你的.o文件。
- 只有一个可执行文件——用对象库没意义,静态库就够了。
- 需要运行时动态加载——必须用动态库。
- 跨平台分发——对象库的.o文件格式跟平台绑定,没法跨平台。
我记得有一次,团队里有人把核心算法写成对象库,然后想打包给客户。折腾了半天发现根本行不通,最后还是改回了静态库。所以,对象库是构建时的优化手段,不是分发手段。
总结
对象库的核心价值就一句话:省掉打包解包,让多个目标共享编译产物。
它不是什么黑科技,但用好了,编译速度的提升是实打实的。尤其是大型项目,几十个可执行文件共享同一组源文件时,效果特别明显。
你想想看,每次改代码只需要重新编译改动的源文件,然后直接注入到各个可执行文件里。没有ar打包,没有链接器解包,干净利落。
一句话总结:对象库 = 编译后的 .o 文件集合,直接注入可执行文件,跳过归档环节。适合多目标共享同一组源文件的场景。
公众号:蓝海资料掘金营,微信deep3321