动态库

静态库的代码会进入最终程序。动态库改为在程序运行时加载,多个程序也可以共享同一个库文件。本章把上一章的 greeting 改成动态库,先介绍动态库的查找机制,再依次约定产物目录、查找路径,最后构建与运行。

认识动态库

动态库在 Linux 上通常使用 .so 扩展名。构建 app 时,链接器记录它对库的依赖,不把库代码复制进程序。

内容 静态库 动态库
文件 libgreeting.a libgreeting.so
链接结果 库代码进入 app app 记录库依赖
运行条件 不再需要库文件 必须找到匹配的 .so

约定产物目录

运行时能找到库,前提是产物位置固定。顶层规则用两行 set 约定产物目录:

CMakeLists.txt
# 构建 SharedLibrary 工程:根 main.cpp 使用 greeting/ 提供的动态库。
cmake_minimum_required(VERSION 3.31)
project(SharedLibrary LANGUAGES CXX)

# 使用 C++20 作为语言标准。
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 可执行文件输出到 build/bin。
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
# 动态库输出到 build/lib。
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)

# 编译数据库,记录每个源文件的实际编译参数。
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

# 加载 greeting/CMakeLists.txt 中的动态库目标。
add_subdirectory(greeting)

# 创建 app 可执行目标。
add_executable(app main.cpp)

# 链接 greeting 动态库。
target_link_libraries(app PRIVATE greeting)

# 编译时启用 libc++ 与常用警告。
target_compile_options(app PRIVATE -stdlib=libc++ -Wall -Wextra -Werror)
# 链接时也使用 libc++。
target_link_options(app PRIVATE -stdlib=libc++)

# 运行 app 时从 build/bin/../lib 查找动态库。
set_target_properties(app PROPERTIES BUILD_RPATH "$ORIGIN/../lib")

其中两行 set 决定产物位置:CMAKE_RUNTIME_OUTPUT_DIRECTORY 收纳可执行文件,CMAKE_LIBRARY_OUTPUT_DIRECTORY 收纳动态库。build/lib 是构建产物目录,源码树里没有 lib/ 目录。

在库目录中把 STATIC 改成 SHARED,其余与静态库工程相同。

greeting/CMakeLists.txt
# 构建 greeting 动态库。

# 声明 greeting 共享库目标:由 src/greeting.cpp 编译生成。
add_library(greeting SHARED src/greeting.cpp)

# include/ 加入头文件搜索路径,供使用方 include。
target_include_directories(greeting PUBLIC include)

# 编译时启用 libc++ 与常用警告。
target_compile_options(greeting PRIVATE -stdlib=libc++ -Wall -Wextra -Werror)
# 链接时也使用 libc++。
target_link_options(greeting PRIVATE -stdlib=libc++)

SHARED 让 add_library 生成动态库 libgreeting.so,构建后出现在 build/lib/ 下。顶层规则里的 set_target_properties 行负责运行时查找,查找路径一节单独解释。

查找路径

产物分离后,运行中的 app 需要自己找到 build/lib/ 里的库。主路径是下面这行,它把查找位置写进可执行文件:

CMakeLists.txt
# 运行 app 时从 build/bin/../lib 查找动态库。
set_target_properties(app PROPERTIES BUILD_RPATH "$ORIGIN/../lib")

BUILD_RPATH 设置 RPATH(程序内部记录的库查找位置)。$ORIGIN 表示可执行文件所在目录,$ORIGIN/../lib 就是 build/bin 旁边的 build/lib。程序无论从哪里启动,都按自身位置找到库,不依赖当前目录,也不依赖全局配置。

临时查找路径

RPATH 已经写进 app,直接运行即可。排查库问题时,可以给单条命令临时追加查找路径:

Ubuntu Bash(工程根目录)
# 给这一条命令临时追加 build/lib,命令结束后设置即失效
env LD_LIBRARY_PATH="$PWD/build/lib" ./build/bin/app

LD_LIBRARY_PATH 是动态加载器在标准位置之外追加查找路径的环境变量。env 只在命令运行期间设置它,不改动任何持久配置。

教学工程以 RPATH 为主路径,这种一次性变量用于排查。不要把 LD_LIBRARY_PATH 写进 ~/.bashrc 或系统配置,也不要用 ldconfig 注册工程产物。全局改动会影响所有程序,出错时也更难定位。

构建与运行

Ubuntu Bash(工程根目录)
# 配置工程,并生成 Ninja 使用的构建规则
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=clang++
# 编译动态库和 app,并完成链接
cmake --build build
# 运行程序
./build/bin/app

输出如下:

输出
Hello, World!

构建产物是 build/lib/libgreeting.so 和 build/bin/app。用 ldd(列出程序运行时依赖的动态库)确认 app 找到的库位置:

Ubuntu Bash(工程根目录)
ldd build/bin/app | grep libgreeting

输出显示 libgreeting.so 已解析到完整路径:

输出
libgreeting.so => /…/cpp-notes/code/practice/shared-library/build/bin/../lib/libgreeting.so (0x0000799516b60000)

前半段是工程在你机器上的路径,关键在结尾:build/bin/../lib/ 正是 $ORIGIN/../lib 的解析结果。移动或删除这个 .so 后,程序将无法启动。

常见错误

运行时找不到 libgreeting.so

直接运行 ./build/bin/app 报 “error while loading shared libraries: libgreeting.so”,按下面的顺序排查:

Ubuntu Bash(工程根目录)
# 重新配置,让 RPATH 写入新构建的 app
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=clang++
# 重新编译并链接
cmake --build build
# 查看 app 记录的库查找结果
ldd build/bin/app | grep libgreeting

先确认 build/lib/libgreeting.so 已生成,再检查顶层规则是否包含 set_target_properties(app PROPERTIES BUILD_RPATH "$ORIGIN/../lib"),补回后重新配置和构建。

缺临时变量不运行

加 env LD_LIBRARY_PATH=... 能运行、去掉就失败,说明查找路径没有写进 app,处理与上一条相同:

  • 把 BUILD_RPATH 写回顶层规则;
  • 重新配置和构建;
  • 不加临时变量直接运行验证。

路径写进全局配置

换一个终端就找不到库,或卸载工程后其他程序也受影响,按下面顺序处理:

  • 原因通常是之前把 LD_LIBRARY_PATH 写进了 ~/.bashrc,或用 ldconfig 注册过工程产物;
  • 打开 ~/.bashrc 删除自己追加的行;
  • 重新打开终端后按本章方案验证:只依赖 RPATH,app 应当直接运行。

自测问题

先不要展开答案,试着自己把回答完整说出来,再展开对照。说不出来的部分,回到对应小节对照正文。

  1. 为什么动态库工程可能构建成功,但运行 app 时仍然失败?

    查看答案

    构建阶段只证明链接器记录了动态库依赖。运行阶段还需要动态加载器找到匹配的 .so。如果库文件不在搜索路径中,或 RPATH 没有指向 build/lib/,程序就无法启动。

  2. RPATH 与临时 LD_LIBRARY_PATH 各解决什么问题?

    查看答案

    RPATH 查找路径,是程序的长期主路径。临时 LD_LIBRARY_PATH 只包住单条命令,用于排查和对照验证。两者都不改动全局配置。

本章回顾

你已经能构建动态库,并让程序在运行时找到它。完成本页后,入门路线中的构建页面就全部完成了。