cmake之旅(4)
cmake之旅4静态库与动态库1 什么是库2 静态库与动态库的区别2.1 静态库2.2 动态库2.3 对比总结3 在 CMake 中构建静态库3.1 显式指定 STATIC3.2 构建并观察4 在 CMake 中构建动态库5 让用户选择库类型6 同时构建静态库和动态库7 控制库的输出路径8 设置库的版本号9 OBJECT 库 —— 第三种选择10 完整示例11 什么时候用静态库什么时候用动态库12 本篇命令速查表13 总结与下一篇预告同系列文章cmake之旅(1):构建的过程cmake之旅(2):CMakeLists.txt 核心语法cmake之旅(3):多目录项目管理cmake之旅(4):静态库与动态库cmake之旅5):函数、宏与 .cmake 模块cmake之旅6查找和使用第三方库静态库与动态库上一篇我们学会了用add_subdirectory管理多目录项目并且在子目录中用add_library把源文件编译成了库。但当时我们只是简单地用了一下并没有深入思考编译出来的到底是什么类型的库静态库和动态库有什么区别什么时候该用哪一种这一篇我们就来彻底搞清楚这些问题。1 什么是库在正式开始之前我们先理解一下库这个概念。库Library就是一组已经编译好的代码打包在一起供别人使用。你可以把它想象成一个工具箱——别人不需要知道工具箱里的工具是怎么造出来的只需要知道怎么用就行。回忆一下我们之前的 add 模块// add.h —— 告诉别人我有什么工具intadd(inta,intb);// add.cpp —— 工具的具体实现intadd(inta,intb){returnab;}如果把add.cpp编译成库别人只需要拿到add.h知道怎么用和编译好的库文件工具本身就能使用 add 功能了完全不需要拿到add.cpp的源码。库分为两种静态库Static Library和动态库Dynamic Library / Shared Library。2 静态库与动态库的区别在讲 CMake 的用法之前我们先搞清楚这两种库的本质区别。2.1 静态库静态库在链接阶段会被完整地复制到可执行文件中。打个比方你要写一篇论文引用了一本参考书中的一个章节。静态库的做法是——你把那个章节的内容完整地抄写到你的论文里面。这样你的论文自包含了所有内容不依赖外部任何东西但论文的篇幅也变大了。在不同的操作系统上静态库的文件格式不同系统静态库文件示例Linux / macOS.alibadd.aWindows.libadd.lib2.2 动态库动态库在链接阶段只是做了一个标记真正的库代码在程序运行时才被加载。还是论文的比方这次你不抄写内容了只在论文中写了一个注释——“请参见《XXX》第 3 章”。读者操作系统在阅读你的论文时会自己去找那本书、翻到第 3 章来看。这样你的论文变短了但前提是读者手边必须有那本参考书。系统动态库文件示例Linux.solibadd.somacOS.dyliblibadd.dylibWindows.dll.libadd.dlladd.lib2.3 对比总结对比项静态库动态库链接方式编译时复制进可执行文件运行时动态加载可执行文件大小较大包含了库的代码较小只包含引用信息运行时依赖无独立运行需要库文件存在于系统中更新库需要重新编译可执行文件替换库文件即可无需重新编译内存占用每个程序各自一份副本多个程序可共享同一份部署难度简单只需要一个可执行文件需要一起分发库文件一句话总结静态库打包带走动态库现场借用。3 在 CMake 中构建静态库我们继续使用上一篇的项目结构├── CMakeLists.txt ├── include │ └── calc │ ├── add.h │ └── de.h └── src ├── CMakeLists.txt ├──add│ ├── CMakeLists.txt │ └── add.cpp ├── de │ ├── CMakeLists.txt │ └── de.cpp └── main.cpp源文件和上一篇完全一样这里不再重复列出。3.1 显式指定 STATIC在上一篇中我们的子模块 CMakeLists.txt 是这样写的add_library(add_lib add.cpp)这里有一个细节我们没有指定库的类型。当不指定类型时CMake 会根据BUILD_SHARED_LIBS变量来决定——如果这个变量为 True就构建动态库否则构建静态库。默认情况下BUILD_SHARED_LIBS未定义所以默认构建的是静态库。但是依赖默认值不是好习惯。推荐显式指定库的类型src/add/CMakeLists.txt# 显式指定为静态库STATIC add_library(add_lib STATIC add.cpp) # 设置头文件路径 target_include_directories(add_lib PUBLIC ${PROJECT_SOURCE_DIR}/include)src/de/CMakeLists.txt# 显式指定为静态库STATIC add_library(de_lib STATIC de.cpp) # 设置头文件路径 target_include_directories(de_lib PUBLIC ${PROJECT_SOURCE_DIR}/include)3.2 构建并观察mkdirbuildcdbuild cmake..make构建完成后在 build 目录下你可以找到生成的静态库文件find.-name*.a你会看到类似这样的输出./src/add/libadd_lib.a ./src/de/libde_lib.a注意CMake 自动给库名加上了lib前缀和.a后缀。也就是说你在 CMakeLists.txt 中写的目标名是add_lib生成的实际文件名是libadd_lib.a。这是 Linux 下的命名约定CMake 会自动处理。4 在 CMake 中构建动态库把STATIC换成SHARED就行了。src/add/CMakeLists.txt# 指定为动态库SHARED add_library(add_lib SHARED add.cpp) # 设置头文件路径 target_include_directories(add_lib PUBLIC ${PROJECT_SOURCE_DIR}/include)src/de/CMakeLists.txt# 指定为动态库SHARED add_library(de_lib SHARED de.cpp) # 设置头文件路径 target_include_directories(de_lib PUBLIC ${PROJECT_SOURCE_DIR}/include)重新构建后查看find.-name*.so输出./src/add/libadd_lib.so ./src/de/libde_lib.so可执行文件也能正常运行。但这里有一个关键区别——可执行文件现在依赖于这些 .so 文件。如果你把可执行文件拷贝到另一个目录单独运行可能会报错error while loading shared libraries: libadd_lib.so: cannot open shared object file这就是动态库的特点运行时必须能找到库文件。5 让用户选择库类型有时候你希望把选择权交给使用者——让他自己决定构建静态库还是动态库。CMake 提供了一个内置变量BUILD_SHARED_LIBS来实现这个需求。src/add/CMakeLists.txt# 不指定 STATIC 或 SHARED由 BUILD_SHARED_LIBS 决定 add_library(add_lib add.cpp) # 设置头文件路径 target_include_directories(add_lib PUBLIC ${PROJECT_SOURCE_DIR}/include)使用者可以在构建时通过命令行参数来控制# 构建静态库默认cmake..# 构建动态库cmake-DBUILD_SHARED_LIBSON..这种方式在开源项目中很常见让使用者根据自己的需求灵活选择。6 同时构建静态库和动态库有些项目希望同时提供静态库和动态库让使用者按需取用。做法是定义两个不同的目标src/add/CMakeLists.txt# 静态库 add_library(add_static STATIC add.cpp) target_include_directories(add_static PUBLIC ${PROJECT_SOURCE_DIR}/include) # 动态库 add_library(add_shared SHARED add.cpp) target_include_directories(add_shared PUBLIC ${PROJECT_SOURCE_DIR}/include)这样构建后会同时生成libadd_static.a和libadd_shared.so。但这样写有个问题动态库和静态库的目标名不同add_staticvsadd_shared生成的文件名也不同。如果我们希望它们生成的文件名相同都叫libadd可以用set_target_properties来修改输出名称# 静态库 add_library(add_static STATIC add.cpp) target_include_directories(add_static PUBLIC ${PROJECT_SOURCE_DIR}/include) set_target_properties(add_static PROPERTIES OUTPUT_NAME add) # 动态库 add_library(add_shared SHARED add.cpp) target_include_directories(add_shared PUBLIC ${PROJECT_SOURCE_DIR}/include) set_target_properties(add_shared PROPERTIES OUTPUT_NAME add)这样静态库会生成libadd.a动态库会生成libadd.so文件名统一不会冲突因为后缀不同。7 控制库的输出路径默认情况下CMake 会把生成的库文件放在与源文件对应的构建子目录中。比如src/add/CMakeLists.txt生成的库会出现在build/src/add/下。但在实际项目中我们通常希望把所有的库和可执行文件集中输出到统一的目录中比如build/lib/和build/bin/。在顶层 CMakeLists.txt 中添加# 设定可执行文件的输出目录 set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) # 设定静态库的输出目录 set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) # 设定动态库的输出目录 set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)构建后的目录结构就会变成build/ ├── bin │ └── Calculator# 可执行文件└── lib ├── libadd_lib.a# 静态库或 .so└── libde_lib.a# 静态库或 .so这三个变量分别控制不同类型文件的输出位置变量控制的文件类型示例CMAKE_RUNTIME_OUTPUT_DIRECTORY可执行文件.exebin/CalculatorCMAKE_ARCHIVE_OUTPUT_DIRECTORY静态库.a / .liblib/libadd.aCMAKE_LIBRARY_OUTPUT_DIRECTORY动态库.so / .dyliblib/libadd.so注意在 Windows 上.dll文件被视为 RUNTIME而不是 LIBRARY所以 Windows 的动态库会输出到CMAKE_RUNTIME_OUTPUT_DIRECTORY指定的目录中。这是一个容易踩的坑。8 设置库的版本号对于动态库设置版本号是一个好习惯。版本号可以帮助使用者区分不同版本的库也有利于系统的库管理。add_library(add_lib SHARED add.cpp) # 设置版本号 set_target_properties(add_lib PROPERTIES VERSION 1.2.3 # 库的完整版本号主版本.次版本.修订号 SOVERSION 1 # SO 版本号通常等于主版本号用于 ABI 兼容 )构建后Linux 下会生成以下文件libadd_lib.so -libadd_lib.so.1# 符号链接指向 SO 版本libadd_lib.so.1 -libadd_lib.so.1.2.3# 符号链接指向完整版本libadd_lib.so.1.2.3# 真实的库文件为什么要这样设计程序链接时使用libadd_lib.so不带版本号所以升级库时程序不需要重新编译运行时加载libadd_lib.so.1SO 版本只要主版本号不变程序就能兼容真实文件libadd_lib.so.1.2.3包含完整版本信息方便管理多个版本共存这套机制是 Linux 动态库版本管理的标准做法CMake 通过VERSION和SOVERSION两个属性就能自动帮你生成。9 OBJECT 库 —— 第三种选择除了 STATIC 和 SHAREDCMake 还提供了一种特殊的库类型——OBJECT 库。add_library(add_obj OBJECT add.cpp)OBJECT 库不会生成 .a 或 .so 文件它只是把源文件编译成目标文件.o然后让其他目标直接引用这些目标文件。什么时候用 OBJECT 库最典型的场景就是同时构建静态库和动态库。回想一下第 6 节我们为了同时生成两种库不得不写两次add_library源文件也被编译了两次。用 OBJECT 库可以避免这个问题# 源文件只编译一次生成目标文件 add_library(add_obj OBJECT add.cpp) target_include_directories(add_obj PUBLIC ${PROJECT_SOURCE_DIR}/include) # 静态库和动态库都复用同一份目标文件 add_library(add_static STATIC $TARGET_OBJECTS:add_obj) add_library(add_shared SHARED $TARGET_OBJECTS:add_obj)$TARGET_OBJECTS:add_obj是一个生成器表达式Generator Expression它的值是 add_obj 编译产生的所有 .o 文件。我们会在后续的文章中详细讲解生成器表达式这里了解用法即可。不过要注意动态库编译时通常需要-fPIC选项Position Independent Code位置无关代码。如果你的 OBJECT 库要同时被静态库和动态库使用需要加上这个属性add_library(add_obj OBJECT add.cpp) # 启用位置无关代码这样生成的目标文件既能用于静态库也能用于动态库 set_target_properties(add_obj PROPERTIES POSITION_INDEPENDENT_CODE ON)10 完整示例我们把这一篇的知识汇总写一个完整的示例项目结构如下├── CMakeLists.txt ├── include │ └── calc │ ├── add.h │ └── de.h └── src ├── CMakeLists.txt ├──add│ ├── CMakeLists.txt │ └── add.cpp ├── de │ ├── CMakeLists.txt │ └── de.cpp └── main.cpp源文件与上一篇相同这里只列出 CMakeLists.txt。顶层 CMakeLists.txt# # 项目Calculator # 描述cmake之旅4完整示例 —— 静态库与动态库 # # 设定 CMake 最低版本 cmake_minimum_required(VERSION 3.10) # 定义项目信息 project(Calculator VERSION 1.0.0 LANGUAGES CXX) # 设定 C 标准 set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED True) # 统一输出目录 set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) # 提供选项让使用者选择构建静态库还是动态库 option(CALC_BUILD_SHARED 构建动态库 OFF) # 打印构建信息 if(CALC_BUILD_SHARED) message(STATUS 库类型: 动态库 (SHARED)) else() message(STATUS 库类型: 静态库 (STATIC)) endif() # 添加 src 子目录 add_subdirectory(src)这里用了option命令定义了一个开关CALC_BUILD_SHARED使用者可以通过-DCALC_BUILD_SHAREDON来选择构建动态库。option和set(... CACHE BOOL ...)类似但语法更简洁专门用于布尔类型的选项。src/add/CMakeLists.txt# 根据选项决定库类型 if(CALC_BUILD_SHARED) add_library(add_lib SHARED add.cpp) else() add_library(add_lib STATIC add.cpp) endif() # 设置头文件路径 target_include_directories(add_lib PUBLIC ${PROJECT_SOURCE_DIR}/include) # 如果是动态库设置版本号 if(CALC_BUILD_SHARED) set_target_properties(add_lib PROPERTIES VERSION ${PROJECT_VERSION} SOVERSION 1 ) endif()src/de/CMakeLists.txt# 根据选项决定库类型 if(CALC_BUILD_SHARED) add_library(de_lib SHARED de.cpp) else() add_library(de_lib STATIC de.cpp) endif() # 设置头文件路径 target_include_directories(de_lib PUBLIC ${PROJECT_SOURCE_DIR}/include) # 如果是动态库设置版本号 if(CALC_BUILD_SHARED) set_target_properties(de_lib PROPERTIES VERSION ${PROJECT_VERSION} SOVERSION 1 ) endif()src/CMakeLists.txt# 添加各模块子目录 add_subdirectory(add) add_subdirectory(de) # 生成可执行文件 add_executable(${PROJECT_NAME} main.cpp) # 链接所有模块 target_link_libraries(${PROJECT_NAME} PRIVATE add_lib de_lib)构建和运行mkdirbuildcdbuild# 构建静态库版本默认cmake..make./bin/Calculator# 或者构建动态库版本cmake-DCALC_BUILD_SHAREDON..make./bin/Calculator注意看因为我们设置了CMAKE_RUNTIME_OUTPUT_DIRECTORY可执行文件现在在bin/目录下库文件在lib/目录下整齐多了。11 什么时候用静态库什么时候用动态库这个问题没有标准答案取决于你的具体场景。以下是一些通用的建议优先选择静态库的场景部署环境不可控时静态库更可靠因为所有代码都打包在可执行文件中不怕目标机器上缺少库文件。嵌入式开发和跨平台发布工具通常优先使用静态库。对性能要求极高的场景也倾向静态库因为它避免了运行时的动态链接开销虽然这个开销通常很小。优先选择动态库的场景大型项目中多个程序共用同一个库时动态库可以节省磁盘空间和内存。需要支持插件机制的程序必须使用动态库因为插件需要在运行时加载。库本身更新频繁、但 API 不变的情况下动态库可以单独替换而不需要重新编译所有依赖它的程序。初学者建议如果没有明确的需求就用静态库。它更简单、更不容易出问题。等到项目规模增大、有了明确的需求时再考虑切换到动态库。12 本篇命令速查表命令 / 属性作用示例add_library(name STATIC src)构建静态库add_library(mylib STATIC a.cpp)add_library(name SHARED src)构建动态库add_library(mylib SHARED a.cpp)add_library(name OBJECT src)构建 OBJECT 库add_library(myobj OBJECT a.cpp)option(name desc val)定义布尔选项option(BUILD_TESTS 构建测试 OFF)set_target_properties设置目标属性见下方VERSION库的完整版本号VERSION 1.2.3SOVERSIONSO 版本号ABI 兼容版本SOVERSION 1OUTPUT_NAME自定义输出文件名OUTPUT_NAME addPOSITION_INDEPENDENT_CODE启用 -fPICPOSITION_INDEPENDENT_CODE ON输出路径相关变量变量控制的文件CMAKE_RUNTIME_OUTPUT_DIRECTORY可执行文件、Windows .dllCMAKE_ARCHIVE_OUTPUT_DIRECTORY静态库 .a / .libCMAKE_LIBRARY_OUTPUT_DIRECTORY动态库 .so / .dylib13 总结与下一篇预告这一篇我们系统学习了静态库和动态库的区别、CMake 中如何构建它们、如何控制输出路径和版本号还了解了 OBJECT 库这种特殊类型。回头看一下我们目前写过的 CMakeLists.txt你可能已经注意到一个问题很多代码在重复。比如 add 和 de 的 CMakeLists.txt 几乎一模一样只是库名和源文件不同。如果有十个模块就要写十份几乎相同的 CMakeLists.txt改一个地方还要改十次。有没有办法把这些重复的逻辑封装起来复用就像 C 中的函数一样定义一次到处调用下一篇——cmake之旅5函数、宏与 .cmake 模块我们来学习 CMake 的代码复用机制。