项目目录结构、第三方库集成与 CMake 实战 / Project Structure, Third-Party Dependencies, and CMake
📅 创建时间:2026-07-13 🏷️ 标签:#C++ #CMake #项目结构 #第三方库 #vcpkg #Conan 📚 前置知识:cpp project fundamentals, windows build outputs, linux build outputs
📋 本章目标
- 掌握 C++ 项目的标准目录结构布局
- 理解第三方库集成的四种场景:预编译二进制 vs 源码 vs 包管理器 vs 系统包
- 彻底搞懂
find_package的 Module 模式和 Config 模式 - 掌握 CMake 的核心三件套:
add_library/add_executable→target_link_libraries - 理解
PUBLIC/PRIVATE/INTERFACE依赖传递的语义 - 学会使用
FetchContent和ExternalProject处理源码级第三方库 - 掌握 RPATH 配置的最佳实践
第1部分:标准 C++ 项目目录结构
1.1 两种经典布局
方案 A:include 和 src 分离(库项目推荐)
my_library/ # 适合:你要发布一个库给别人用
├── CMakeLists.txt
├── include/ # 公开API —— 给使用者看的
│ └── my_library/
│ ├── api.h
│ ├── types.h
│ └── version.h
├── src/ # 内部实现 —— 使用者不需要关心
│ ├── api.cpp
│ ├── internal_impl.cpp
│ └── CMakeLists.txt
├── test/
├── examples/
└── external/ # 第三方依赖源码方案 B:.h 和 .cpp 放在一起(应用程序项目推荐)
my_application/
├── CMakeLists.txt
├── src/
│ ├── main.cpp
│ ├── core/
│ │ ├── engine.h # .h 和 .cpp 在同一个目录
│ │ └── engine.cpp
│ ├── ui/
│ │ ├── window.h
│ │ └── window.cpp
│ └── utils/
│ ├── logger.h
│ └── logger.cpp
├── test/
└── external/┌─────────────────────────────────────────────────────────────────────────────┐
│ 方案选择建议 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 方案A(分离式): │
│ ✅ 适合:你在开发一个库,会被别人 #include │
│ ✅ 优点:公开接口和内部实现物理隔离,使用者一眼看到 API │
│ ✅ include/my_library/ 的嵌套结构利于 install 后组织 │
│ │
│ 方案B(同目录): │
│ ✅ 适合:应用程序、内部工具 │
│ ✅ 优点:改 .h 时 .cpp 就在旁边,不需要跳目录 │
│ ✅ 结构扁平,小型项目更直观 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘1.2 install 的概念——你的项目被别人使用时的样子
当你 cmake --install 或 make install 后,文件会被拷贝到系统标准位置:
安装前(你的开发目录) 安装后(系统目录)
───────────────────────── ────────────────────────
project/ /usr/local/
├── include/ ├── include/
│ └── mylib/ │ └── mylib/
│ └── api.h ───→ │ └── api.h
├── src/ │
│ └── impl.cpp ├── lib/
└── build/ │ ├── libmylib.so -> libmylib.so.1
└── libmylib.so ───→ │ └── libmylib.so.1 -> libmylib.so.1.0.0
│ └── libmylib.so.1.0.0
└── share/
└── mylib/
└── mylibConfig.cmake ← CMake 包配置这就是为什么 include/ 里要用 mylib/api.h 的嵌套结构——防止安装后头文件名冲突。
第2部分:第三方库集成——四种场景
这是日常开发中最常遇到的工程问题。
场景总览
┌─────────────────────────────────────────────────────────────────────────────┐
│ 第三方库集成的四种场景 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 场景1:甲方给了预编译二进制 │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 交付物:.h + .lib + .dll(Windows) │ │
│ │ .h + .so(Linux,直接给二进制) │ │
│ │ .h + .a(Linux,静态库) │ │
│ │ │ │
│ │ 你需要: │ │
│ │ 1. 把头文件路径加到 include 搜索路径 │ │
│ │ 2. 把库文件路径加到链接器搜索路径 │ │
│ │ 3. 告诉 CMake 链接这个库 │ │
│ │ │ │
│ │ 优点:不需要编译,直接链接即可 │ │
│ │ 痛点:编译器版本不匹配(第05篇深入) │ │
│ │ 缺少某个平台的版本 │ │
│ │ 库的 Debug 版本和 Release 版本要分开 │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ 场景2:甲方只给了源码 │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 交付物:一堆 .cpp 和 .h │ │
│ │ │ │
│ │ 你需要: │ │
│ │ 1. 看它有没有 CMakeLists.txt │ │
│ │ • 有 → FetchContent 或 add_subdirectory │ │
│ │ • 没有 → 自己写 CMakeLists.txt 或 ExternalProject │ │
│ │ 2. 看它有没有第三方依赖 │ │
│ │ • 先解决它的依赖,再解决你自己的 │ │
│ │ │ │
│ │ 优点:可以 Debug 进去看源码、可以修改、编译器选项可控 │ │
│ │ 痛点:编译慢(每次 CI 都要重新编) │ │
│ │ 依赖传递(它的依赖你也得处理) │ │
│ │ 源码里可能有硬编码路径、平台特定代码等坑 │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ 场景3:用包管理器 │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ • vcpkg(Microsoft 出品,Windows 上最方便) │ │
│ │ • Conan(跨平台,C/C++ 专用,企业用户多) │ │
│ │ • 系统包管理器(apt install libxxx-dev) │ │
│ │ │ │
│ │ 优点:自动解决依赖、版本管理、一键安装 │ │
│ │ 痛点:不是所有库都在包管理器中 │ │
│ │ 版本可能不是最新的 │ │
│ │ vcpkg 在 Linux 上的体验不如 Conan │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ 场景4:系统自带(Linux apt/pacman) │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ sudo apt install libboost-dev libeigen3-dev libopencv-dev │ │
│ │ │ │
│ │ 优点:快、方便、系统维护 │ │
│ │ 痛点:版本固定(Ubuntu 22.04 的 boost 很旧) │ │
│ │ 不同发行版版本不同 │ │
│ │ Windows 上没这套 │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘2.1 场景1实战:对接预编译二进制
# 假设甲方给的文件放在 external/acme_sdk/ 下:
# external/acme_sdk/
# ├── include/acme/api.h
# ├── lib/win64/acme.lib ← Windows 导入库
# ├── bin/win64/acme.dll ← Windows DLL
# └── lib/linux64/libacme.so ← Linux 动态库
cmake_minimum_required(VERSION 3.20)
project(MyApp)
# 方法1:手动指定路径(适合简单场景)
add_executable(my_app main.cpp)
target_include_directories(my_app PRIVATE
${CMAKE_SOURCE_DIR}/external/acme_sdk/include
)
if(WIN32)
target_link_libraries(my_app PRIVATE
${CMAKE_SOURCE_DIR}/external/acme_sdk/lib/win64/acme.lib
)
# 运行时需要 acme.dll,拷贝到输出目录
add_custom_command(TARGET my_app POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_SOURCE_DIR}/external/acme_sdk/bin/win64/acme.dll
$<TARGET_FILE_DIR:my_app>
)
else()
target_link_libraries(my_app PRIVATE
${CMAKE_SOURCE_DIR}/external/acme_sdk/lib/linux64/libacme.so
)
endif()# 方法2:创建 IMPORTED 目标(推荐,适合复杂项目)
add_library(acme SHARED IMPORTED)
set_target_properties(acme PROPERTIES
IMPORTED_LOCATION "${CMAKE_SOURCE_DIR}/external/acme_sdk/bin/win64/acme.dll"
IMPORTED_IMPLIB "${CMAKE_SOURCE_DIR}/external/acme_sdk/lib/win64/acme.lib"
INTERFACE_INCLUDE_DIRECTORIES "${CMAKE_SOURCE_DIR}/external/acme_sdk/include"
)
# 之后直接 target_link_libraries(my_app PRIVATE acme) 即可2.2 场景2实战:对接源码——FetchContent
# FetchContent = CMake 3.11+ 内置,自动下载 + 编译
include(FetchContent)
FetchContent_Declare(
spdlog
GIT_REPOSITORY https://github.com/gabime/spdlog.git
GIT_TAG v1.13.0
)
FetchContent_MakeAvailable(spdlog)
# 现在 spdlog 已经编译好了,可以直接用
target_link_libraries(my_app PRIVATE spdlog::spdlog)┌─────────────────────────────────────────────────────────────────────────────┐
│ FetchContent vs ExternalProject vs add_subdirectory │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ FetchContent: │
│ • 在 configure 阶段下载源码 │
│ • 和你的项目在同一个 build 系统中 │
│ • 适合:CMake 项目,依赖会在 CMake 生成时一起处理 │
│ • 缺点:第一次 configure 慢(要下载) │
│ │
│ ExternalProject: │
│ • 在 build 阶段下载和编译(独立进程) │
│ • 适合:非 CMake 项目(autotools、Make、自定义脚本) │
│ • 缺点:依赖关系复杂(它是独立构建,你的项目在 configure 时看不到它) │
│ │
│ add_subdirectory: │
│ • 源码已经在你的项目目录里(vendor/ 或 external/ 或 git submodule) │
│ • 直接将第三方库的 CMakeLists.txt 纳入你的构建 │
│ • 适合:三方库已经是 CMake 项目 │
│ • 缺点:需要手动管理源码更新 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘2.3 场景3实战:包管理器
# vcpkg —— 适合 Windows,也支持 Linux/macOS
git clone https://github.com/Microsoft/vcpkg.git
cd vcpkg && ./bootstrap-vcpkg.sh
./vcpkg install spdlog:x64-windows # Windows
./vcpkg install spdlog:x64-linux # Linux
# 然后在 CMake 中使用(需要 -DCMAKE_TOOLCHAIN_FILE=...)
cmake -B build -DCMAKE_TOOLCHAIN_FILE=/path/to/vcpkg/scripts/buildsystems/vcpkg.cmake
# 之后直接用 find_package(spdlog CONFIG REQUIRED) 即可# Conan —— 跨平台,中大型项目首选
# conanfile.txt:
[requires]
spdlog/1.13.0
[generators]
CMakeDeps
CMakeToolchain
# 安装
conan install . --build=missing2.4 场景4实战:系统包管理器 (Linux)
# 先 apt install libspdlog-dev
# 然后用 find_package 找到它
find_package(spdlog REQUIRED)
target_link_libraries(my_app PRIVATE spdlog::spdlog)第3部分:find_package 深度解析
3.1 两种模式
find_package 是 CMake 中最强大也最容易迷惑的命令。它有两种完全不同的工作模式:
┌─────────────────────────────────────────────────────────────────────────────┐
│ find_package 的两种模式 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Module 模式(传统方式): │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ find_package(Foo REQUIRED) │ │
│ │ │ │
│ │ CMake 在 CMAKE_MODULE_PATH 中找 FindFoo.cmake │ │
│ │ FindFoo.cmake 里手写了: │ │
│ │ • 在哪些目录下找 foo.h │ │
│ │ • 在哪些目录下找 libfoo.so │ │
│ │ • 版本检测、组件检测 │ │
│ │ │ │
│ │ CMake 自带了大量 FindXXX.cmake(FindBoost, FindOpenGL, ...) │ │
│ │ 你也可以自己写 FindXXX.cmake 放在 cmake/ 目录下 │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ Config 模式(现代方式,推荐): │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ find_package(Foo CONFIG REQUIRED) │ │
│ │ # 或 find_package(Foo REQUIRED) —— 优先 Config,fallback Module│ │
│ │ │ │
│ │ CMake 找 FooConfig.cmake 或 foo-config.cmake │ │
│ │ 这个文件由库的作者提供,记录了一切: │ │
│ │ • 头文件在哪里 │ │
│ │ • 库文件在哪里 │ │
│ │ • 依赖了哪些其他库 │ │
│ │ • 编译选项(C++标准、宏定义等) │ │
│ │ │ │
│ │ 搜索路径: │ │
│ │ <prefix>/lib/cmake/Foo/ (Linux 标准) │ │
│ │ <prefix>/share/Foo/ (Linux 备用) │ │
│ │ <prefix>/CMake/ (Windows) │ │
│ │ CMAKE_PREFIX_PATH 环境变量或 CMake 变量 │ │
│ │ Foo_DIR CMake 变量或环境变量 │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ 实战对比: │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ # 告诉 CMake 去哪找 │ │
│ │ cmake -B build -DCMAKE_PREFIX_PATH=/opt/custom_libs │ │
│ │ # 或针对特定库: │ │
│ │ cmake -B build -DFoo_DIR=/opt/foo/lib/cmake/Foo │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘3.2 一个库的作者如何提供 Config 文件?
# 在你的 CMakeLists.txt 中(编写库的一方)
include(CMakePackageConfigHelpers)
# 安装时生成 MyLibConfig.cmake
install(TARGETS mylib EXPORT MyLibTargets
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
RUNTIME DESTINATION bin
INCLUDES DESTINATION include
)
install(EXPORT MyLibTargets
FILE MyLibTargets.cmake
NAMESPACE mylib::
DESTINATION lib/cmake/MyLib
)
# 这样别人用 find_package(MyLib CONFIG) 就能找到
# 然后 target_link_libraries(xxx PRIVATE mylib::mylib)第4部分:CMake 核心三件套与依赖传递
4.1 PUBLIC / PRIVATE / INTERFACE
这是 CMake 中最核心的概念,也是最容易用错的地方。
┌─────────────────────────────────────────────────────────────────────────────┐
│ PUBLIC / PRIVATE / INTERFACE 的语义 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 假设 B 依赖 A,C 依赖 B: │
│ │
│ A ───PRIVATE──→ B ───???──→ C │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ PRIVATE(私有依赖): │ │
│ │ • B 在 .cpp 里用了 A 的头文件,但 B 的 .h 里没暴露 A 的类型 │ │
│ │ • C 不需要知道 A 的存在 │ │
│ │ • A 的 include 路径只传给 B,不传给 C │ │
│ │ • A 的链接只传给 B,不传给 C │ │
│ │ │ │
│ │ PUBLIC(公开依赖): │ │
│ │ • B 的 .h 里 #include 了 A 的头文件 │ │
│ │ • B 的接口返回或接受 A 中定义的类型 │ │
│ │ • C 需要能 include 到 A │ │
│ │ • A 的 include 路径和链接都传给 B 和 C │ │
│ │ │ │
│ │ INTERFACE(仅接口依赖): │ │
│ │ • B 自己不需要 A(.cpp 不引用 A),但 B 的使用者需要 A │ │
│ │ • 典型场景:header-only 库、接口库 │ │
│ │ • A 的 include 路径传给 C,但 B 自己不用 │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ 代码示例: │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ // B.h - 公开头文件 │ │
│ │ #include <A/api.h> // B 的接口暴露了 A 的类型 │ │
│ │ │ │
│ │ class B { │ │
│ │ public: │ │
│ │ A::Result doSomething(); // 返回值是 A 的类型 │ │
│ │ }; │ │
│ │ │ │
│ │ // 这时候 B 对 A 的依赖必须是 PUBLIC │ │
│ │ target_link_libraries(B PUBLIC A) │ │
│ │ // 这样 C → B 时,C 自动能 include A 和链接 A │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ 记忆口诀: │
│ PRIVATE = "我只告诉你我知道,但你别告诉别人" │
│ PUBLIC = "我告诉你的,你可以告诉所有人" │
│ INTERFACE = "我不需要,但我的使用者需要" │
│ │
└─────────────────────────────────────────────────────────────────────────────┘4.2 CMake 实战:一个完整的示例
cmake_minimum_required(VERSION 3.20)
project(MyProject VERSION 1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# ====== 你自己的库 ======
add_library(mycore
src/core/engine.cpp
src/core/parser.cpp
)
target_include_directories(mycore
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src # 内部头文件只有自己用
)
# mycore 依赖 spdlog(只在自己的 .cpp 里用 → PRIVATE)
# find_package 或者 FetchContent 获取 spdlog
target_link_libraries(mycore PRIVATE spdlog::spdlog)
# ====== 你的可执行文件 ======
add_executable(my_app src/main.cpp)
target_link_libraries(my_app PRIVATE mycore) # my_app 只需要知道 mycore第5部分:RPATH 配置最佳实践
5.1 问题场景
你编译了一个程序,它依赖 libmycore.so。默认情况下,程序只在系统路径(/usr/lib 等)找 .so。你的 .so 在 build 目录里,所以运行 ./my_app 会报:
./my_app: error while loading shared libraries: libmycore.so: cannot open shared object file你不想每次都用 LD_LIBRARY_PATH。这就是 RPATH 解决的问题。
# CMake 中设置 RPATH 的最佳实践
set(CMAKE_INSTALL_RPATH "$ORIGIN/../lib") # 安装后,.so 在 ../lib 相对位置
set(CMAKE_BUILD_RPATH "$ORIGIN") # 构建时,.so 和可执行文件在一起
# 或者使用 CMake 3.14+ 的便捷属性:
set_target_properties(my_app PROPERTIES
BUILD_RPATH "$ORIGIN"
INSTALL_RPATH "$ORIGIN/../lib"
)
# $ORIGIN = 可执行文件所在的目录(Linux)
# 等价于 Windows 上 .exe 所在目录(Windows 默认就在这里搜 DLL,不需要 $ORIGIN)核心总结
┌─────────────────────────────────────────────────────────────────────────────┐
│ 第三方库集成决策树 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 甲方给你的形式 → 你的处理方式 │
│ ───────────────────── ────────────── │
│ .h + .lib + .dll IMPORTED 目标 + 拷贝 DLL 到输出目录 │
│ .h + .so IMPORTED 目标 + 设置 RPATH/$ORIGIN │
│ 只有源码 (有CMake) FetchContent 或 add_subdirectory │
│ 只有源码 (无CMake) ExternalProject 或自己写 CMakeLists.txt │
│ 包管理器安装 find_package + toolchain file │
│ 系统 apt 安装 find_package 直接找 │
│ │
│ PUBLIC = 接口暴露了依赖类型(.h 里 #include 了) │
│ PRIVATE = 只在 .cpp 里用,对外不可见 │
│ INTERFACE = header-only 库或纯接口目标 │
│ │
│ $ORIGIN RPATH = 让你的程序从自己所在目录的 ../lib 加载 .so │
│ │
└─────────────────────────────────────────────────────────────────────────────┘章节测试
测试1:目录结构
你正在开发一个给别人用的 C++ 网络库。你应该用 include/src 分离式还是同目录式布局?为什么?
测试2:imported target vs 手动路径
为什么推荐用 IMPORTED target 而不是在 target_link_libraries 中直接写库的绝对路径?
测试3:依赖传递
库 B 的公开头文件里 #include <A/api.h> 并且返回了 A::Result 类型。B 对 A 的依赖应该用 PUBLIC 还是 PRIVATE?为什么?
测试4:FetchContent vs add_subdirectory
你拿到一个第三方库的源码,它有 CMakeLists.txt。用 FetchContent 还是 add_subdirectory(把源码复制到 external/ 下)?各有什么优劣?
测试5:find_package
find_package(Boost REQUIRED COMPONENTS filesystem) 这条命令做了什么?是 Module 模式还是 Config 模式?
测试6:RPATH
你的可执行文件在 bin/my_app,你的 .so 在 lib/libmycore.so。为了让 my_app 运行时能找到 libmycore.so(无需 LD_LIBRARY_PATH),RPATH 应该设为什么?
参考答案
测试1答案
答案:include/src 分离式。因为你要发布给别人用:
include/里的头文件是你的公开 API,使用者 #includesrc/里的实现不需要暴露- 安装时
include/→/usr/local/include,清晰分离 - 如果是内部应用程序,选同目录式更简单
测试2答案
答案:因为 IMPORTED target 是一个"接口对象",可以携带完整的信息:
- 自动处理 include 路径(INTERFACE_INCLUDE_DIRECTORIES)
- 自动处理链接依赖(INTERFACE_LINK_LIBRARIES)
- 可附加编译选项(INTERFACE_COMPILE_DEFINITIONS)
- 方便 Debug/Release 不同配置切换
- 如果库有子依赖,手动路径方式需要逐一手动添加
测试3答案
答案:PUBLIC。因为 B 的 .h 文件里包含了 A 的头文件,并且 B 公开接口的返回值使用了 A 中的类型。这意味着任何使用 B 的人(比如 C)也需要能看到 A 的类型定义才能编译通过。用 PUBLIC 可以让 A 的 include 路径和链接自动传递给 C。
测试4答案
答案:
- FetchContent:自动下载 + 编译,适合 CI 自动化,不需要手动管理源码。缺点:每次 configure 要下载。
- add_subdirectory:源码已经在本地(复制到 external/ 或用 git submodule),不需要网络。缺点:需要手动维护源码版本更新。
- 一般推荐 FetchContent(自动化),除非内网环境不能访问外网。
测试5答案
答案:优先尝试 Config 模式(找 BoostConfig.cmake),找不到则 fallback 到 Module 模式(用 CMake 自带的 FindBoost.cmake)。COMPONENTS filesystem 指定了要 Boost 的 filesystem 组件。如果是 find_package(Boost CONFIG ...) 则强制只用 Config 模式。
测试6答案
答案:$ORIGIN/../lib。$ORIGIN = 可执行文件所在目录(即 bin/),../lib 向上找到项目根目录再进 lib/。用 patchelf --set-rpath '$ORIGIN/../lib' bin/my_app 或通过 CMake 的 INSTALL_RPATH 设置。
相关笔记
- windows build outputs - Windows 编译产物详解
- linux build outputs - Linux 编译产物详解
- static vs dynamic linking - 静态链接 vs 动态链接深度对比
- CMake 构建插件项目 - CMake 构建动态库与插件项目(进阶)
下一步学习
- [ ] 阅读 04 - 静态链接 vs 动态链接
学习状态:🟡 开始学习