第1章:子目录与项目组织——add_subdirectory(),构建树与源树分离

大家好,我是你们这门课的老朋友。今天咱们聊聊CMake里一个特别基础、但又特别重要的东西——怎么组织你的项目目录。

说实话,我见过太多人把CMake当成一个“点一下就能编译”的黑盒子。所有源文件堆在一个目录里,CMakeLists.txt写得跟流水账似的。嗯,小项目还行,项目一大了,那叫一个乱。我自己早期带团队的时候,就吃过这个亏。

1.1 为什么需要子目录?

你想想看,一个大型C++项目,可能有几十个模块。网络模块、日志模块、UI模块、算法模块……如果全塞在一个目录里,你找文件都得翻半天。更别提编译了,改一行代码,整个项目重新编译,那滋味……

所以,我们需要子目录。每个模块一个文件夹,每个文件夹里放一个CMakeLists.txt。这样,结构清晰,编译也快。

CMake里,用add_subdirectory()这个命令,就能把子目录“拉”进构建系统。

核心思想: 每个子目录是一个独立的“编译单元”,有自己的CMakeLists.txt,负责定义自己的目标(库或可执行文件)。

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)

你看,这样一组织,每个模块的职责就非常清楚了。

我的习惯: 每个子目录的CMakeLists.txt只关心自己目录里的文件。不要跨目录引用源文件路径,那样会破坏模块化。

1.3 构建树与源树分离

这是CMake一个非常强大的特性。说白了,就是你的源代码目录(源树)和编译产生的中间文件、最终产物(构建树)是分开的。

为什么要这么做?

  • 干净:源目录里不会出现一堆.o.exe文件。
  • 多配置:你可以为同一个源树创建多个构建树。比如一个Debug版,一个Release版。
  • 版本控制:构建目录可以随时删除,不影响源代码。

具体做法很简单。在项目根目录下,创建一个build文件夹,然后进去执行cmake:

mkdir build
cd build
cmake ..

这样,所有构建产物都会在build目录下,源目录纹丝不动。

注意: 千万不要在源目录里直接运行cmake!我曾经见过有人直接在项目根目录下执行cmake,结果源目录被各种临时文件污染了,最后不得不重新clone代码。

结合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

这个结构有几个好处:

  • 清晰的分层:第三方库、业务模块、应用程序、测试,各归其位。
  • 模块内部自包含:每个模块有自己的includesrc目录,头文件和实现文件分开。
  • 易于扩展:加一个新模块,只需要新建一个目录,写一个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_librariestarget_include_directories来建立依赖关系。这样,整个项目的依赖图就非常清晰了。

避坑指南: 我曾经在一个项目里,看到有人把所有模块的源文件路径都写死在根目录的CMakeLists.txt里。结果后来要拆分模块,改得那叫一个痛苦。记住:每个模块的CMakeLists.txt只负责自己目录里的文件,这是铁律。

1.5 知识体系图

下面这张图,帮你把本章的核心逻辑串起来:

CMake项目组织核心逻辑 根目录 CMakeLists.txt add_subdirectory(src) add_subdirectory(lib) add_subdirectory(tests) add_executable(my_app ...) add_library(mylib ...) add_test(...) 构建树与源树分离 mkdir build && cd build && cmake ..

这张图展示了从根目录CMakeLists.txt出发,通过add_subdirectory()引入各个子模块,每个子模块定义自己的目标,最终所有构建产物都输出到独立的构建树中。这就是CMake项目组织的核心思想。

1.6 总结

好了,这一章的内容就这些。总结一下:

  • add_subdirectory() 是组织多目录项目的核心命令。
  • 构建树与源树分离 是CMake的最佳实践,一定要养成习惯。
  • 大型项目的目录结构 要分层、模块化,每个模块的CMakeLists.txt只负责自己的文件。

下一章,我们会深入聊聊target_include_directoriestarget_link_libraries,看看如何管理模块之间的依赖关系。嗯,到时候见。


公众号:蓝海资料掘金营,微信deep3321