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%。

对象库的典型使用场景

我个人习惯在以下三种场景用对象库:

  1. 多个可执行文件共享同一组源文件——比如测试程序、主程序、工具程序都依赖同一套核心逻辑。
  2. 头文件驱动的模板库——模板实例化分散在多个翻译单元,用对象库可以避免重复实例化。
  3. 需要精细控制编译选项——对象库允许你给不同源文件设置不同的编译标志。

举个例子。我在做一个网络框架时,核心的协议解析模块被三个程序使用:服务端、客户端、压力测试工具。用静态库的话,每次改协议头文件,三个程序都得重新链接。换成对象库后,只有真正改了的源文件才重新编译,链接几乎是瞬时的。

对象库的完整用法

先看一个完整的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构建体系中的位置:

对象库在CMake构建体系中的位置 源文件 (.cpp) 头文件 (.h) 编译 (compile) -O2 -Wall -std=c++17 对象库 (OBJECT) 生成 .o 文件集合 无归档,无链接 可执行文件 A 可执行文件 B 可执行文件 C $<TARGET_OBJECTS:...> 注入 链接阶段:对象库直接注入 .o 文件,跳过归档/解包

从图里能看出来,对象库处在「编译完成」和「链接之前」的中间地带。它不参与归档,也不参与链接,纯粹是中间产物的集合。

什么时候不该用对象库?

嗯,这里要注意。对象库不是万能的。

  • 你要发布库给别人用——对象库没法分发,别人拿不到你的.o文件。
  • 只有一个可执行文件——用对象库没意义,静态库就够了。
  • 需要运行时动态加载——必须用动态库。
  • 跨平台分发——对象库的.o文件格式跟平台绑定,没法跨平台。

我记得有一次,团队里有人把核心算法写成对象库,然后想打包给客户。折腾了半天发现根本行不通,最后还是改回了静态库。所以,对象库是构建时的优化手段,不是分发手段

总结

对象库的核心价值就一句话:省掉打包解包,让多个目标共享编译产物

它不是什么黑科技,但用好了,编译速度的提升是实打实的。尤其是大型项目,几十个可执行文件共享同一组源文件时,效果特别明显。

你想想看,每次改代码只需要重新编译改动的源文件,然后直接注入到各个可执行文件里。没有ar打包,没有链接器解包,干净利落。

一句话总结:对象库 = 编译后的 .o 文件集合,直接注入可执行文件,跳过归档环节。适合多目标共享同一组源文件的场景。


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