2026/10/18 9:41:04

Linux下CMake生成与链接库:静态库动态库实战指南

Linux下CMake生成与链接库:静态库动态库实战指南 1. 从一个真实场景说起为什么你需要搞懂CMake的库生成与链接如果你写过C或者C大概率经历过这样的阶段一开始所有代码都塞进一个main.c或者main.cpp里编译命令就是一行gcc main.c -o app简单粗暴能跑就行。但项目稍微大一点代码量上来了你就会开始琢磨是不是该把一些通用的功能抽出来单独编译成一个库这样既能复用又能让主程序的编译速度变快还能把接口和实现分离别人用你的库时只需要头文件和二进制文件不用关心内部实现细节。这个思路是对的但问题也随之而来库怎么生成生成之后怎么让主程序找到它、链接它在Linux环境下CMake是目前最主流的构建工具之一它跨平台、语法相对清晰、社区生态好。但很多人在第一次用CMake生成库并链接库的时候都会踩一堆坑undefined reference、cannot find -lxxx、运行时error while loading shared libraries……这些报错信息看着就头疼。我自己在带新人的时候发现大家最容易卡住的地方不是写代码而是构建系统。代码逻辑明明没问题但就是编译不过、链接不上。所以这篇文章我想把Linux下用CMake生成库、链接库这件事从头到尾讲清楚。不管你是刚接触CMake的新手还是已经用过但总是靠复制粘贴配置的老手我都尽量把每个参数、每个步骤背后的“为什么”说明白。文章会覆盖静态库和动态库的生成方式、链接时的路径处理、运行时动态库查找、常见报错排查以及一些我在实际项目中总结出来的经验技巧。你可以把它当成一份可以随时翻出来对照的操作手册。2. 先搞清楚基础概念静态库与动态库到底有什么区别在动手写CMake配置之前有必要先把静态库和动态库这两个概念理清楚。很多人配置写不对根本原因是对这两种库的链接机制理解模糊。2.1 静态库的工作方式与适用场景静态库在Linux下通常以.a结尾全称是archive本质上就是一堆.o目标文件的打包集合。当你把主程序和静态库链接时链接器会把静态库中用到的目标文件直接复制到最终的可执行文件里。这意味着最终的可执行文件体积会变大但好处是它不依赖外部库文件拿到哪里都能跑。静态库的生成过程分两步先把源文件编译成目标文件.o然后用ar命令打包。在CMake里你只需要用add_library命令并指定STATIC类型CMake会自动帮你完成这两步。静态库适合什么场景我个人的经验是小工具、嵌入式环境、需要单文件分发的场景以及你不想让用户操心动态库路径的情况。缺点是如果多个程序都链接了同一个静态库每个程序都会有一份库代码的副本内存和磁盘占用会重复。2.2 动态库的工作方式与适用场景动态库在Linux下以.so结尾全称是shared object。它和静态库最大的区别在于链接阶段只是记录了对动态库的引用真正的代码加载发生在程序启动时或者运行时通过dlopen手动加载。多个程序可以共享同一个动态库的内存映射节省系统资源。动态库的生成需要编译时加上-fPIC选项也就是位置无关代码。这是因为动态库被加载到内存的地址是不确定的代码必须能在任意地址执行。CMake里用add_library指定SHARED类型时会自动帮你加上-fPIC。动态库的优点是节省资源、方便更新替换.so文件即可不用重新编译主程序缺点是部署时需要考虑库的查找路径问题否则程序启动时会报找不到库。2.3 两种库的对比与选型建议对比维度静态库.a动态库.so链接时机编译链接时复制代码运行时加载可执行文件体积较大较小部署依赖无外部依赖需要库文件存在更新方式需重新编译主程序替换库文件即可内存占用每个程序独立副本多程序共享编译选项无特殊要求需要-fPIC适用场景小工具、嵌入式、单文件分发大型项目、多程序共享、插件架构选型的时候我一般遵循这样的原则如果是公司内部的基础组件多个项目都会用那就做成动态库方便统一升级如果是给外部交付的单文件工具那就静态链接省得用户配环境如果项目对启动速度有极致要求静态库会略快一点点因为省去了动态链接的过程。3. CMake生成库的核心命令与参数详解理解了静态库和动态库的区别之后我们来看CMake里具体怎么操作。这一部分我会把每个关键命令拆开讲包括参数的含义和我自己在使用中总结的注意事项。3.1 add_library命令的完整用法add_library是CMake生成库的核心命令基本语法是add_library(库名称 [STATIC | SHARED | MODULE] [源文件...])库名称不需要加lib前缀和.a/.so后缀CMake会自动处理。比如你写add_library(mymath STATIC math.cpp)最终生成的文件是libmymath.a。STATIC和SHARED分别对应静态库和动态库。如果不指定类型CMake会根据BUILD_SHARED_LIBS变量的值来决定默认是STATIC。我建议在项目里显式指定类型避免因为环境变量不同导致构建结果不一致。MODULE类型比较特殊它生成的是不会被自动链接的动态库通常用于插件场景需要配合dlopen手动加载。一般项目里用得少但如果你要做插件架构就会用到它。还有一个细节源文件列表可以写多个文件也可以用变量代替。比如set(MYMATH_SOURCES math.cpp advanced_math.cpp) add_library(mymath STATIC ${MYMATH_SOURCES})这样写的好处是源文件列表集中管理后续增删文件不容易漏改。3.2 头文件路径与安装规则的配置生成库的时候通常需要指定头文件路径让编译器能找到你的.h文件。用target_include_directories命令target_include_directories(mymath PUBLIC $BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include $INSTALL_INTERFACE:include )这里PUBLIC的意思是这个包含路径既用于编译库本身也会传递给链接这个库的目标。BUILD_INTERFACE和INSTALL_INTERFACE是生成器表达式分别指定构建时和安装后的路径。这样写的好处是当你的库被安装到系统目录后其他项目通过find_package找到它时能自动获得正确的头文件路径。安装规则用install命令install(TARGETS mymath ARCHIVE DESTINATION lib LIBRARY DESTINATION lib RUNTIME DESTINATION bin ) install(DIRECTORY include/ DESTINATION include)ARCHIVE对应静态库LIBRARY对应动态库RUNTIME对应可执行文件Windows下的DLL也会放到RUNTIME。这样配置之后执行make install就会把库文件和头文件分别放到lib和include目录下。3.3 版本号与SONAME的配置要点动态库有一个很重要的概念叫SONAMEShared Object Name它决定了程序运行时查找的是哪个版本的库。在CMake里通过set_target_properties配置set_target_properties(mymath PROPERTIES VERSION ${PROJECT_VERSION} SOVERSION ${PROJECT_VERSION_MAJOR} )假设版本号是1.2.3SOVERSION是1那么生成的文件会有三个libmymath.so.1.2.3实际文件、libmymath.so.1SONAME软链接、libmymath.so开发用软链接。程序链接时记录的是SONAME也就是libmymath.so.1运行时加载器会去找这个文件。这样做的好处是当你升级到1.3.0时SONAME还是libmymath.so.1旧程序不用重新编译就能用新库如果升级到2.0.0SOVERSION变成2旧程序仍然找libmymath.so.1不会因为接口不兼容而崩溃。注意如果你不设置SOVERSIONCMake默认不会生成SONAME软链接程序链接时记录的就是完整路径或者libmymath.so这会给后续升级带来麻烦。所以只要做动态库就一定要配VERSION和SOVERSION。4. 链接库的完整实操流程与参数计算库生成好了接下来就是怎么让主程序链接它。这一部分我按照从简单到复杂的顺序把链接方式分成几种情况来讲每种情况都给出完整的CMake配置和操作步骤。4.1 同一项目内链接target_link_libraries的基本用法如果库和主程序在同一个CMake项目里链接非常简单add_executable(myapp main.cpp) target_link_libraries(myapp PRIVATE mymath)PRIVATE表示这个链接关系只用于编译myapp本身不会传递给其他依赖myapp的目标。如果是库依赖库的情况比如mymath依赖mylog那mymath链接mylog时应该用PRIVATE还是PUBLIC这取决于mymath的头文件里是否暴露了mylog的类型。如果mymath.h里包含了mylog.h那就必须用PUBLIC否则链接mymath的程序会因为找不到mylog的符号而报错。如果mylog只在mymath.cpp里用那就用PRIVATE避免不必要的依赖传递。这个PRIVATE/PUBLIC/INTERFACE的区分是CMake现代用法里很重要的部分用好了能让依赖关系清晰编译速度也会更快。4.2 链接外部库find_library与link_directories的取舍当库不在同一个项目里而是已经安装在系统的某个位置就需要告诉CMake去哪里找。有两种方式第一种是用find_libraryfind_library(MYMATH_LIB NAMES mymath PATHS /opt/mymath/lib) if(NOT MYMATH_LIB) message(FATAL_ERROR mymath library not found) endif() target_link_libraries(myapp PRIVATE ${MYMATH_LIB})第二种是用link_directories加target_link_librarieslink_directories(/opt/mymath/lib) target_link_libraries(myapp PRIVATE mymath)我强烈建议用第一种方式。link_directories是全局生效的会影响之后所有目标的链接路径容易造成污染和意外链接到错误版本的库。find_library是精确查找找到之后把完整路径传给target_link_libraries依赖关系明确排查问题也方便。如果库有对应的CMake配置文件比如mymathConfig.cmake那更好的方式是用find_packagefind_package(mymath REQUIRED) target_link_libraries(myapp PRIVATE mymath::mymath)这种方式能自动处理头文件路径、库路径、依赖关系是最推荐的做法。但前提是库的开发者提供了配置文件这个我们后面会讲怎么生成。4.3 运行时动态库查找RPATH与LD_LIBRARY_PATH的处理动态库链接成功之后编译不会报错但运行的时候可能会遇到error while loading shared libraries: libmymath.so.1: cannot open shared object file这是因为动态链接器在运行时找不到库文件。默认情况下动态链接器会搜索/lib、/usr/lib这些系统目录以及LD_LIBRARY_PATH环境变量指定的目录。如果你的库安装在/opt/mymath/lib那就不在默认搜索路径里。解决办法有几种第一种是设置LD_LIBRARY_PATHexport LD_LIBRARY_PATH/opt/mymath/lib:$LD_LIBRARY_PATH ./myapp这种方式简单但每次运行都要设置而且如果多个项目依赖不同版本的库容易冲突。第二种是在CMake里设置RPATHset_target_properties(myapp PROPERTIES BUILD_RPATH /opt/mymath/lib INSTALL_RPATH /opt/mymath/lib )RPATH会被写入可执行文件运行时动态链接器会优先从RPATH指定的目录查找。BUILD_RPATH用于构建时INSTALL_RPATH用于安装后。这种方式比LD_LIBRARY_PATH更可靠因为它是写死在二进制文件里的不受环境变量影响。第三种是把库安装到系统默认搜索路径比如/usr/local/lib然后运行ldconfig更新缓存。这种方式适合系统级安装但需要root权限而且可能影响其他程序。我一般推荐用RPATH的方式特别是项目部署到多台机器时RPATH能保证程序在任何环境下都能找到正确的库。4.4 参数计算实例从编译命令反推CMake配置有时候我们需要从手写的编译命令反推CMake配置或者反过来验证CMake生成的命令是否正确。举个例子假设手写编译命令是这样的gcc main.c -o app -I/opt/mymath/include -L/opt/mymath/lib -lmymath -Wl,-rpath,/opt/mymath/lib对应的CMake配置就是find_library(MYMATH_LIB NAMES mymath PATHS /opt/mymath/lib) target_include_directories(myapp PRIVATE /opt/mymath/include) target_link_libraries(myapp PRIVATE ${MYMATH_LIB}) set_target_properties(myapp PROPERTIES INSTALL_RPATH /opt/mymath/lib)你可以用make VERBOSE1来查看CMake实际执行的编译和链接命令对比一下就能确认配置是否正确。这个技巧在排查链接问题时非常有用。5. 常见报错与排查技巧实录这一部分是我在实际项目里踩过的坑和总结出来的排查方法。链接问题往往报错信息很模糊但只要掌握套路定位起来并不难。5.1 undefined reference符号找不到的几种原因undefined reference to xxx是最常见的链接错误。原因通常有这几种第一种是库没有链接上。检查target_link_libraries里是否包含了对应的库库名拼写是否正确。注意CMake里写的是mymath不是libmymath.a。第二种是链接顺序问题。在Linux下链接器从左到右处理库如果库A依赖库B那A必须写在B前面。CMake的target_link_libraries会自动处理依赖顺序但如果你手动用-l参数就要注意顺序。第三种是C和C混合编译时的extern C问题。C编译器会对函数名进行name mangling如果C代码调用C库的函数或者反过来就需要用extern C包裹声明。这个坑在混合编程时非常常见。第四种是库的ABI不兼容。比如用不同版本的编译器编译的库或者C标准库版本不一致都可能导致符号找不到。这种情况比较隐蔽需要检查编译器和标准库版本。5.2 cannot find -lxxx库路径配置错误的排查cannot find -lmymath表示链接器在搜索路径里找不到libmymath.so或libmymath.a。排查步骤先确认库文件确实存在用ls看一下路径对不对。然后确认target_link_libraries里写的库名和实际文件名是否匹配。如果库文件是libmymath.a那CMake里写mymath就对了。如果库文件是libmymath_core.a那就要写mymath_core。再确认搜索路径是否包含库所在目录。用find_library的话检查PATHS参数是否正确用link_directories的话检查路径是否拼写错误。可以在CMake里加message(STATUS MYMATH_LIB${MYMATH_LIB})来打印查找结果。还有一个容易忽略的点32位和64位库混用。如果你的系统是64位但库是32位编译的链接时会报找不到。用file libmymath.a可以查看库的架构信息。5.3 运行时找不到动态库三种解决方案对比前面提到了error while loading shared libraries的问题这里把三种解决方案做个对比方案优点缺点适用场景LD_LIBRARY_PATH配置简单立即生效环境变量污染多版本冲突临时调试RPATH写入二进制不受环境影响路径写死迁移需重新编译固定部署路径ldconfig系统级生效所有程序可用需要root权限影响全局系统级安装我个人的习惯是开发阶段用LD_LIBRARY_PATH快速验证部署阶段用RPATH保证可靠性如果是给系统安装库那就用ldconfig。5.4 常见问题速查表报错信息可能原因排查方法解决方案undefined reference库未链接/顺序错误/extern C缺失检查target_link_libraries和头文件声明补链接、调整顺序、加extern Ccannot find -lxxx库路径不对/库名拼写错误用find_library打印结果修正路径或库名error while loading shared libraries运行时找不到.so用ldd查看依赖设置RPATH或LD_LIBRARY_PATHversion GLIBCXX_xxx not foundC标准库版本不匹配检查编译器和标准库版本统一编译环境或静态链接标准库cannot open shared object file库文件不存在或权限不足ls检查文件检查权限安装库文件或修改权限提示遇到链接问题时先用ldd查看可执行文件的依赖关系再用nm查看库里的符号最后用make VERBOSE1查看实际编译命令。这三板斧能解决大部分问题。6. 进阶技巧让库更专业、更好用基础操作掌握之后这一部分分享一些进阶技巧让你的库更容易被其他项目集成也更符合工程规范。6.1 生成CMake配置文件让find_package开箱即用如果你希望别人用find_package(mymath)就能找到你的库需要生成两个文件mymathConfig.cmake和mymathConfigVersion.cmake。CMake提供了configure_package_config_file和write_basic_package_version_file两个宏来简化这个过程。include(CMakePackageConfigHelpers) configure_package_config_file( ${CMAKE_CURRENT_SOURCE_DIR}/cmake/mymathConfig.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/mymathConfig.cmake INSTALL_DESTINATION lib/cmake/mymath ) write_basic_package_version_file( ${CMAKE_CURRENT_BINARY_DIR}/mymathConfigVersion.cmake VERSION ${PROJECT_VERSION} COMPATIBILITY SameMajorVersion ) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/mymathConfig.cmake ${CMAKE_CURRENT_BINARY_DIR}/mymathConfigVersion.cmake DESTINATION lib/cmake/mymath )mymathConfig.cmake.in的内容大概是PACKAGE_INIT include(${CMAKE_CURRENT_LIST_DIR}/mymathTargets.cmake)然后导出目标install(TARGETS mymath EXPORT mymathTargets ARCHIVE DESTINATION lib LIBRARY DESTINATION lib RUNTIME DESTINATION bin ) install(EXPORT mymathTargets FILE mymathTargets.cmake NAMESPACE mymath:: DESTINATION lib/cmake/mymath )这样配置之后其他项目用find_package(mymath REQUIRED)就能找到你的库然后用target_link_libraries(app PRIVATE mymath::mymath)链接头文件路径、库路径、依赖关系全部自动处理。这是最专业的做法也是开源库的标配。6.2 用Object Library减少重复编译如果你的库有多个目标共享同一批源文件可以用Object Library来避免重复编译add_library(mymath_obj OBJECT math.cpp advanced_math.cpp) target_include_directories(mymath_obj PUBLIC include) add_library(mymath_static STATIC $TARGET_OBJECTS:mymath_obj) add_library(mymath_shared SHARED $TARGET_OBJECTS:mymath_obj)这样math.cpp和advanced_math.cpp只编译一次生成的目标文件同时用于静态库和动态库编译时间能省不少。这个技巧在大型项目里特别有用。6.3 编译选项的精细控制与调试符号生成库的时候编译选项的控制也很重要。比如调试版本需要加-g发布版本需要加-O2这些可以通过target_compile_options来配置target_compile_options(mymath PRIVATE $$CONFIG:Debug:-g -O0 $$CONFIG:Release:-O2 -DNDEBUG )$CONFIG:Debug是生成器表达式根据构建类型选择不同的选项。这样一套配置就能同时支持Debug和Release构建不用手动改CMakeLists。另外动态库默认会导出所有符号如果你只想导出指定的接口可以用-fvisibilityhidden配合__attribute__((visibility(default)))来控制。这样能减小库的体积也能避免符号冲突。6.4 跨平台构建的注意事项虽然这篇文章主要讲Linux但CMake的优势就是跨平台。如果你希望库能在Windows和macOS上也能构建需要注意几点Windows下动态库需要显式导出符号通常用__declspec(dllexport)和__declspec(dllimport)可以定义一个宏来统一处理if(WIN32) target_compile_definitions(mymath PRIVATE MYMATH_EXPORTS) endif()然后在头文件里#ifdef _WIN32 #ifdef MYMATH_EXPORTS #define MYMATH_API __declspec(dllexport) #else #define MYMATH_API __declspec(dllimport) #endif #else #define MYMATH_API __attribute__((visibility(default))) #endifmacOS下动态库是.dylib安装名称install_name需要用rpath来配置CMake的INSTALL_RPATH在macOS下也适用但语法略有不同。这些细节在跨平台项目里都需要注意。7. 一个完整可复现的项目示例说了这么多理论最后给一个完整的项目示例你可以直接复制到本地运行验证整个流程。7.1 项目目录结构mymath_project/ ├── CMakeLists.txt ├── include/ │ └── mymath.h ├── src/ │ ├── math.cpp │ └── advanced_math.cpp └── app/ └── main.cpp7.2 库的CMakeLists.txt配置cmake_minimum_required(VERSION 3.16) project(mymath VERSION 1.0.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) add_library(mymath SHARED src/math.cpp src/advanced_math.cpp ) target_include_directories(mymath PUBLIC $BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include $INSTALL_INTERFACE:include ) set_target_properties(mymath PROPERTIES VERSION ${PROJECT_VERSION} SOVERSION ${PROJECT_VERSION_MAJOR} ) install(TARGETS mymath LIBRARY DESTINATION lib ARCHIVE DESTINATION lib RUNTIME DESTINATION bin ) install(DIRECTORY include/ DESTINATION include)7.3 主程序的链接配置add_executable(myapp app/main.cpp) target_link_libraries(myapp PRIVATE mymath)7.4 编译、安装与运行验证mkdir build cd build cmake .. -DCMAKE_INSTALL_PREFIX/opt/mymath make make install # 运行程序 /opt/mymath/bin/myapp如果运行时提示找不到动态库检查/opt/mymath/lib是否在RPATH里或者临时设置LD_LIBRARY_PATH验证。7.5 用ldd和nm验证链接结果# 查看可执行文件依赖的动态库 ldd /opt/mymath/bin/myapp # 查看库中导出的符号 nm -D /opt/mymath/lib/libmymath.so # 查看库的SONAME objdump -p /opt/mymath/lib/libmymath.so | grep SONAME这几个命令能帮你确认库是否正确生成、程序是否正确链接、运行时是否能找到库。8. 个人经验总结与后续扩展方向我在实际项目里用CMake管理库的生成和链接最大的体会是配置要显式依赖要清晰路径要可控。显式指定库类型、显式声明依赖关系、显式设置RPATH能避免很多“换个环境就编译不过”的问题。另外尽早引入find_package和配置文件导出机制虽然前期麻烦一点但项目变大之后会省很多事。后续如果你想继续深入可以研究这几个方向用CPack打包库的安装包用CTest做单元测试集成用Conan或vcpkg管理第三方依赖以及用CMake的FetchContent模块自动下载和构建依赖库。这些都是在实际工程中很实用的技能等基础打牢之后可以逐步尝试。