第1章:子目录与项目组织——add_subdirectory(),构建树与源树分离
大家好,我是你们这门课的老朋友。今天咱们聊聊CMake里一个特别基础、但又特别重要的东西——怎么组织你的项目目录。
说实话,我见过太多人把CMake当成一个“点一下就能编译”的黑盒子。所有源文件堆在一个目录里,CMakeLists.txt写得跟流水账似的。嗯,小项目还行,项目一大了,那叫一个乱。我自己早期带团队的时候,就吃过这个亏。
1.1 为什么需要子目录?
你想想看,一个大型C++项目,可能有几十个模块。网络模块、日志模块、UI模块、算法模块……如果全塞在一个目录里,你找文件都得翻半天。更别提编译了,改一行代码,整个项目重新编译,那滋味……
所以,我们需要子目录。每个模块一个文件夹,每个文件夹里放一个CMakeLists.txt。这样,结构清晰,编译也快。
CMake里,用add_subdirectory()这个命令,就能把子目录“拉”进构建系统。
1.2 add_subdirectory() 的基本用法
语法很简单:
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
- source_dir:子目录的路径(相对于当前CMakeLists.txt所在目录)。
- binary_dir(可选):指定子目录的构建输出目录。如果不指定,默认在构建目录下生成一个同名文件夹。
- EXCLUDE_FROM_ALL(可选):如果加上,这个子目录里的目标不会默认被构建,除非你显式指定。
举个例子。假设你的项目结构是这样的:
my_project/
├── CMakeLists.txt # 根目录
├── src/
│ ├── CMakeLists.txt # 源码子目录
│ ├── main.cpp
│ └── utils.cpp
└── lib/
├── CMakeLists.txt # 库子目录
└── mylib.cpp
根目录的CMakeLists.txt里,你只需要写:
cmake_minimum_required(VERSION 3.10)
project(MyProject)
add_subdirectory(src)
add_subdirectory(lib)
然后,在src/CMakeLists.txt里,你可以定义可执行文件:
add_executable(my_app main.cpp utils.cpp)
target_link_libraries(my_app PRIVATE mylib) # 链接lib目录下的库
在lib/CMakeLists.txt里,定义库:
add_library(mylib STATIC mylib.cpp)
你看,这样一组织,每个模块的职责就非常清楚了。
1.3 构建树与源树分离
这是CMake一个非常强大的特性。说白了,就是你的源代码目录(源树)和编译产生的中间文件、最终产物(构建树)是分开的。
为什么要这么做?
- 干净:源目录里不会出现一堆
.o、.exe文件。 - 多配置:你可以为同一个源树创建多个构建树。比如一个Debug版,一个Release版。
- 版本控制:构建目录可以随时删除,不影响源代码。
具体做法很简单。在项目根目录下,创建一个build文件夹,然后进去执行cmake:
mkdir build
cd build
cmake ..
这样,所有构建产物都会在build目录下,源目录纹丝不动。
结合add_subdirectory(),构建树的结构会镜像源树的结构。比如上面的例子,构建树里会有:
build/
├── src/
│ └── ... (src目录的构建产物)
└── lib/
└── ... (lib目录的构建产物)
每个子目录的binary_dir默认就是构建树里对应的路径。你也可以手动指定,但一般不需要。
1.4 大型项目的目录结构设计
好了,基础讲完了。咱们来点实战经验。一个大型项目,目录结构该怎么设计?
我个人比较推崇一种“分层+模块化”的结构。举个例子:
my_large_project/
├── CMakeLists.txt # 根目录
├── cmake/ # 存放自定义的CMake模块
│ └── FindMyLib.cmake
├── third_party/ # 第三方库
│ ├── CMakeLists.txt
│ ├── json/
│ └── fmt/
├── modules/ # 业务模块
│ ├── CMakeLists.txt
│ ├── network/
│ │ ├── CMakeLists.txt
│ │ ├── include/network/
│ │ │ ├── tcp_client.h
│ │ │ └── tcp_server.h
│ │ └── src/
│ │ ├── tcp_client.cpp
│ │ └── tcp_server.cpp
│ ├── logger/
│ │ ├── CMakeLists.txt
│ │ ├── include/logger/
│ │ │ └── logger.h
│ │ └── src/
│ │ └── logger.cpp
│ └── ui/
│ ├── CMakeLists.txt
│ ├── include/ui/
│ │ └── window.h
│ └── src/
│ └── window.cpp
├── apps/ # 可执行程序
│ ├── CMakeLists.txt
│ ├── server/
│ │ ├── CMakeLists.txt
│ │ └── main.cpp
│ └── client/
│ ├── CMakeLists.txt
│ └── main.cpp
└── tests/ # 测试
├── CMakeLists.txt
├── test_network.cpp
└── test_logger.cpp
这个结构有几个好处:
- 清晰的分层:第三方库、业务模块、应用程序、测试,各归其位。
- 模块内部自包含:每个模块有自己的
include和src目录,头文件和实现文件分开。 - 易于扩展:加一个新模块,只需要新建一个目录,写一个CMakeLists.txt,然后在父目录的CMakeLists.txt里加一行
add_subdirectory()。
根目录的CMakeLists.txt大概长这样:
cmake_minimum_required(VERSION 3.15)
project(MyLargeProject VERSION 1.0.0 LANGUAGES CXX)
# 设置C++标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 添加第三方库
add_subdirectory(third_party)
# 添加业务模块
add_subdirectory(modules)
# 添加应用程序
add_subdirectory(apps)
# 可选:添加测试
option(BUILD_TESTS "Build tests" OFF)
if(BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
每个子模块的CMakeLists.txt只定义自己的目标,然后通过target_link_libraries和target_include_directories来建立依赖关系。这样,整个项目的依赖图就非常清晰了。
1.5 知识体系图
下面这张图,帮你把本章的核心逻辑串起来:
这张图展示了从根目录CMakeLists.txt出发,通过add_subdirectory()引入各个子模块,每个子模块定义自己的目标,最终所有构建产物都输出到独立的构建树中。这就是CMake项目组织的核心思想。
1.6 总结
好了,这一章的内容就这些。总结一下:
- add_subdirectory() 是组织多目录项目的核心命令。
- 构建树与源树分离 是CMake的最佳实践,一定要养成习惯。
- 大型项目的目录结构 要分层、模块化,每个模块的CMakeLists.txt只负责自己的文件。
下一章,我们会深入聊聊target_include_directories和target_link_libraries,看看如何管理模块之间的依赖关系。嗯,到时候见。
公众号:蓝海资料掘金营,微信deep3321