3、源文件管理:如何优雅地组织源文件?
说实话,这个问题我几乎在每个项目里都会被问到。不管是刚入行的新人,还是写了几年 C++ 的老手,面对 CMake 里那几种源文件组织方式,总会纠结一阵子。
我个人习惯是:先搞清楚你的项目规模,再选方案。别一上来就想着用最炫酷的写法,稳扎稳打才是正道。
今天咱们就把 aux_source_directory、file(GLOB ...) 和显式列出源文件这三种方式,掰开揉碎讲清楚。我会结合自己踩过的坑,给你一些实在的建议。
三种方式,各有什么门道?
1. 显式列出源文件:最笨,但也最稳
这是最原始的方式。你在 CMakeLists.txt 里,把每个 .cpp 文件的名字都写出来:
add_executable(my_app
src/main.cpp
src/utils.cpp
src/network.cpp
src/parser.cpp
)
嗯,看起来有点啰嗦。但我在项目中遇到过好几次,正是这种「啰嗦」救了我一命。
优点:
- 构建系统完全可控。你加了什么文件,CMake 一清二楚。
- 修改源文件列表后,CMake 会自动重新配置。不会出现「我加了文件,但编译没反应」的诡异情况。
- 代码审查时,别人能一眼看出你新增或删除了哪些源文件。
缺点:
- 文件多了,维护起来确实烦。每次新建一个 .cpp,都得回来改 CMakeLists.txt。
- 多人协作时,容易漏掉别人的新文件,导致编译失败。
2. file(GLOB ...):方便,但有坑
这个命令可以让你用通配符自动收集源文件:
file(GLOB MY_SOURCES "src/*.cpp")
add_executable(my_app ${MY_SOURCES})
看起来是不是清爽多了?加文件不用改 CMakeLists.txt 了。
但这里有个大坑——CMake 不会自动检测新文件。
什么意思呢?你想想看:你在 src 目录下新建了一个 new_feature.cpp,然后直接 make。结果呢?CMake 根本不知道有这个文件,因为它只在 cmake .. 配置阶段执行了一次 GLOB。你新增的文件,它没看到。
我曾经因为这个坑,浪费了整整一个下午。当时我在一个大型项目里加了一堆新模块,结果编译死活过不去。排查了半天,才发现是 GLOB 没重新执行。从那以后,我对 GLOB 就多留了个心眼。
优点:
- 写起来快,省事。适合快速原型开发。
- 文件结构变化时,不需要手动修改 CMakeLists.txt。
缺点:
- 新增文件后,必须手动重新运行
cmake配置,否则新文件不会被编译。 - 构建行为不够透明。你很难一眼看出到底编译了哪些文件。
- 如果误删了源文件,GLOB 可能静默地跳过,导致链接错误。
file(GLOB MY_SOURCES CONFIGURE_DEPENDS "src/*.cpp")
但注意,CONFIGURE_DEPENDS 在旧版 CMake 里不支持,而且性能上也有损耗。
3. aux_source_directory:我基本不用
这个命令是 CMake 提供的一个「自动收集」方式:
aux_source_directory(src MY_SOURCES)
add_executable(my_app ${MY_SOURCES})
它会把指定目录下所有源文件都加进来。听起来很智能?但实际用起来,问题不少。
缺点很明显:
- 和 GLOB 一样,不会自动检测新文件。
- 它会把目录下所有 .cpp 文件都加进来,你没法做筛选。比如你有个测试文件
test_main.cpp放在 src 下,它也会被编译进主程序。 - 官方文档其实已经不太推荐用了。说白了,它就是个历史遗留产物。
我个人习惯是:从来不用 aux_source_directory。它的功能 GLOB 都能覆盖,而且 GLOB 还更灵活。没必要为了一个过时的命令增加学习成本。
三种方式对比,一目了然
我整理了一张表,方便你对比:
| 特性 | 显式列出 | file(GLOB ...) | aux_source_directory |
|---|---|---|---|
| 维护工作量 | 高(每次加文件都要改) | 低(自动收集) | 低(自动收集) |
| 构建确定性 | 高(CMake 完全知道文件列表) | 低(需手动重新配置) | 低(需手动重新配置) |
| 文件筛选能力 | 完全可控 | 支持通配符,可筛选 | 不支持筛选,全量加入 |
| 推荐场景 | 小项目、安全关键系统 | 中等规模、快速迭代 | 不推荐使用 |
| CMake 官方态度 | 推荐 | 谨慎使用 | 不推荐 |
我的实战建议
说了这么多,到底该怎么选?我给出三个场景,你对照着来:
- 项目很小(少于 10 个源文件):直接显式列出。别折腾,简单就是最好的。
- 项目中等(10-50 个源文件):用
file(GLOB ...)配合CONFIGURE_DEPENDS。但记得,每次新增文件后,手动cmake --build .之前先看一眼配置有没有更新。 - 项目很大(50+ 源文件):我建议用显式列出,但按模块拆分。每个模块一个
CMakeLists.txt,各自显式列出自己的源文件。这样既清晰,又不会让单个文件列表太长。
一张图看懂源文件管理策略
下面这张 SVG 图,帮你梳理了不同场景下的选择逻辑:
最后说两句
源文件管理这件事,说大不大,说小不小。但我在项目里见过太多因为源文件组织混乱导致的构建问题。有的是漏文件,有的是多文件,还有的是 GLOB 没刷新导致编译了旧代码。
我的建议很简单:别偷懒,也别过度设计。小项目就老老实实显式列出,大项目就按模块拆分。至于 aux_source_directory,忘掉它吧。
记住,CMake 的目标是让构建变得可预测、可维护。你的源文件管理方式,也应该服务于这个目标。