第100章:大型项目实战——综合运用所学知识,构建一个包含多个库、测试、安装的完整项目
终于到了最后一章。说实话,我写到这里还挺感慨的。从第一讲的hello world,到如今能驾驭多库、测试、安装的完整项目,这一路走来,你积累的东西已经足够应对绝大多数实际工程场景了。
这一讲,我们不学新语法。我们要做的,是把之前零零散散的知识点串起来,搭一个真正能用的项目骨架。我个人习惯叫它「工业级最小原型」——麻雀虽小,五脏俱全。
项目结构:先搭架子,再填肉
先看看我准备的项目长什么样。你想想看,一个正经的C++项目,至少得有:核心库、工具库、单元测试、安装脚本。下面这个结构,是我在多个项目中迭代出来的,比较顺手。
MyProject/
├── CMakeLists.txt # 顶层构建文件
├── cmake/
│ └── MyProjectConfig.cmake.in # 安装后的包配置文件
├── src/
│ ├── CMakeLists.txt
│ ├── core/ # 核心数学库
│ │ ├── CMakeLists.txt
│ │ ├── include/core/
│ │ │ ├── add.h
│ │ │ └── multiply.h
│ │ └── src/
│ │ ├── add.cpp
│ │ └── multiply.cpp
│ └── utils/ # 工具库(依赖core)
│ ├── CMakeLists.txt
│ ├── include/utils/
│ │ └── calculator.h
│ └── src/
│ └── calculator.cpp
├── tests/
│ ├── CMakeLists.txt
│ ├── test_add.cpp
│ ├── test_multiply.cpp
│ └── test_calculator.cpp
└── examples/
├── CMakeLists.txt
└── demo.cpp
嗯,这里要注意:include目录的路径设计是有讲究的。我见过太多人直接把头文件扔在src/里,安装时还得手动拷贝,特别容易漏。用include/<模块名>/这种结构,安装时直接整个目录搬走,干净利落。
顶层CMakeLists.txt:总控开关
顶层文件负责三件事:项目元信息、子目录引入、安装规则。我习惯把版本号、C++标准这些全局设置放在这里,子模块只管自己的事。
cmake_minimum_required(VERSION 3.16)
project(MyProject VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
# 开启测试
option(BUILD_TESTING "Build tests" ON)
option(BUILD_EXAMPLES "Build examples" ON)
add_subdirectory(src)
if(BUILD_TESTING)
enable_testing()
add_subdirectory(tests)
endif()
if(BUILD_EXAMPLES)
add_subdirectory(examples)
endif()
# 安装规则:导出包配置
install(
EXPORT MyProjectTargets
FILE MyProjectConfig.cmake
NAMESPACE MyProject::
DESTINATION lib/cmake/MyProject
)
include(CMakePackageConfigHelpers)
configure_package_config_file(
cmake/MyProjectConfig.cmake.in
"${CMAKE_CURRENT_BINARY_DIR}/MyProjectConfig.cmake"
INSTALL_DESTINATION lib/cmake/MyProject
)
install(
FILES "${CMAKE_CURRENT_BINARY_DIR}/MyProjectConfig.cmake"
DESTINATION lib/cmake/MyProject
)
这里有个坑我曾经踩过:CMAKE_POSITION_INDEPENDENT_CODE一定要开,否则编译动态库时链接静态库会报错。说白了,就是让所有目标都生成位置无关代码,省得后面出幺蛾子。
核心库core:最底层的积木
core库只做两件事:加法和乘法。它不依赖任何其他模块,是项目的最底层。
# src/core/CMakeLists.txt
add_library(core
src/add.cpp
src/multiply.cpp
)
target_include_directories(core
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
# 安装头文件和库
install(
TARGETS core
EXPORT MyProjectTargets
PUBLIC_HEADER DESTINATION include/core
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
)
# 手动安装头文件(因为PUBLIC_HEADER只认.h文件列表)
install(
DIRECTORY include/
DESTINATION include
)
你可能会问:为什么不用PUBLIC_HEADER属性?嗯,那个属性需要你手动列出所有头文件,维护起来太累。我更喜欢直接install(DIRECTORY),一劳永逸。
工具库utils:站在core的肩膀上
utils库封装了一个Calculator类,内部调用core的函数。它展示了库之间的依赖关系如何用CMake表达。
# src/utils/CMakeLists.txt
add_library(utils
src/calculator.cpp
)
target_include_directories(utils
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
target_link_libraries(utils PUBLIC core)
install(
TARGETS utils
EXPORT MyProjectTargets
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
)
install(
DIRECTORY include/
DESTINATION include
)
注意target_link_libraries(utils PUBLIC core)这一行。用PUBLIC而不是PRIVATE,是因为Calculator的头文件里引用了core的头文件。如果你用了PRIVATE,别人链接utils时会找不到core的符号——我曾经因为这个被坑了一整个下午。
测试:用CTest跑起来
测试目录的CMakeLists.txt用add_test注册每个测试用例。我习惯每个测试文件对应一个可执行文件,这样失败时能精确定位。
# tests/CMakeLists.txt
find_package(GTest REQUIRED)
# 测试core库
add_executable(test_add test_add.cpp)
target_link_libraries(test_add GTest::GTest GTest::Main core)
add_test(NAME test_add COMMAND test_add)
add_executable(test_multiply test_multiply.cpp)
target_link_libraries(test_multiply GTest::GTest GTest::Main core)
add_test(NAME test_multiply COMMAND test_multiply)
# 测试utils库
add_executable(test_calculator test_calculator.cpp)
target_link_libraries(test_calculator GTest::GTest GTest::Main utils)
add_test(NAME test_calculator COMMAND test_calculator)
测试代码本身很简单,比如test_add.cpp:
#include <gtest/gtest.h>
#include "core/add.h"
TEST(AddTest, PositiveNumbers) {
EXPECT_EQ(add(2, 3), 5);
}
TEST(AddTest, NegativeNumbers) {
EXPECT_EQ(add(-1, -1), -2);
}
运行ctest时,你会看到每个测试的通过/失败状态。如果某个测试挂了,直接看名字就知道是哪个模块的问题。
安装与打包:让别人能用你的库
安装的核心是生成MyProjectConfig.cmake,这样其他项目通过find_package(MyProject)就能找到你的库。配置模板文件cmake/MyProjectConfig.cmake.in长这样:
@PACKAGE_INIT@
include("${CMAKE_CURRENT_LIST_DIR}/MyProjectTargets.cmake")
check_required_components(MyProject)
然后用户在自己的项目里就可以这样用:
find_package(MyProject REQUIRED)
target_link_libraries(my_app PRIVATE MyProject::utils)
是不是很眼熟?没错,这就是你平时用find_package找第三方库的底层机制。自己写一遍,才能真正理解它。
知识体系总览
下面这张图,把整个项目的构建逻辑串起来了。你可以看到数据流从源码到测试、安装、再到外部使用的完整路径。
避坑指南:我踩过的几个雷
- 头文件路径混乱:我曾经把
include目录直接设成src/,结果安装时一堆内部头文件暴露出去。后来强制要求每个库有自己的include/<模块名>/,世界清净了。 - 忘记导出依赖:
utils链接core时用了PRIVATE,导致外部项目链接utils后编译报错。记住:头文件里引用的库,必须用PUBLIC。 - 测试没注册到CTest:只写了
add_executable忘了add_test,跑ctest时显示0个测试。嗯,这种低级错误我犯过不止一次。 - 安装路径硬编码:早期我直接写
install(TARGETS ... DESTINATION /usr/local/lib),换到Windows上就炸了。用DESTINATION lib这种相对路径,CMake会自动适配平台。
总结
这一章的内容,说白了就是把之前99讲的知识点揉成一个可运行的项目。你掌握了这个骨架,以后不管遇到多复杂的工程,都可以往里面套。核心就三条:模块化组织代码、用target传递依赖、用CTest和安装脚本收尾。
我个人觉得,CMake学到这个程度,已经可以应付95%的日常工作了。剩下的5%,遇到具体问题再查文档也不迟。毕竟,工具是为人服务的,别让工具把你给绕进去了。
核心要点回顾
- 项目结构:顶层CMakeLists.txt + 子模块独立CMakeLists.txt
- 库依赖:用
PUBLIC/PRIVATE/INTERFACE精确控制传递 - 测试:每个测试文件一个可执行文件,用
add_test注册 - 安装:生成
Config.cmake,让外部项目通过find_package使用 - 避坑:头文件路径、依赖可见性、测试注册、安装路径
小提示
如果你想把项目发布到GitHub上,建议在README.md里写清楚构建和安装步骤。我见过太多好项目因为文档不全,别人根本跑不起来。哪怕只有三行命令,也比没有强。
注意
跨平台编译时,DESTINATION路径要小心。Linux下lib对应lib,Windows下可能是bin(对于DLL)。CMake的GNUInstallDirs模块可以帮你自动处理,建议用起来。
公众号:蓝海资料掘金营,微信deep3321