3、源文件管理:如何优雅地组织源文件?

说实话,这个问题我几乎在每个项目里都会被问到。不管是刚入行的新人,还是写了几年 C++ 的老手,面对 CMake 里那几种源文件组织方式,总会纠结一阵子。

我个人习惯是:先搞清楚你的项目规模,再选方案。别一上来就想着用最炫酷的写法,稳扎稳打才是正道。

今天咱们就把 aux_source_directoryfile(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。
  • 多人协作时,容易漏掉别人的新文件,导致编译失败。
我的建议: 如果你的项目源文件少于 20 个,或者你对构建的确定性要求极高(比如嵌入式、安全关键系统),就用显式列出。别嫌麻烦,稳定第一。

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 可能静默地跳过,导致链接错误。
避坑指南: 如果你非要用 GLOB,记得在 CMakeLists.txt 里加上这么一段,强制每次构建都重新 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 官方态度 推荐 谨慎使用 不推荐

我的实战建议

说了这么多,到底该怎么选?我给出三个场景,你对照着来:

  1. 项目很小(少于 10 个源文件):直接显式列出。别折腾,简单就是最好的。
  2. 项目中等(10-50 个源文件):用 file(GLOB ...) 配合 CONFIGURE_DEPENDS。但记得,每次新增文件后,手动 cmake --build . 之前先看一眼配置有没有更新。
  3. 项目很大(50+ 源文件):我建议用显式列出,但按模块拆分。每个模块一个 CMakeLists.txt,各自显式列出自己的源文件。这样既清晰,又不会让单个文件列表太长。
核心原则: 源文件管理,本质上是「便利性」和「确定性」之间的权衡。你越追求便利,就越要承担构建行为不透明的风险。我个人更倾向于确定性,所以大部分项目我都用显式列出。

一张图看懂源文件管理策略

下面这张 SVG 图,帮你梳理了不同场景下的选择逻辑:

源文件管理策略选择流程 开始选择 项目规模? 小(<10文件) 中(10-50文件) 大(>50文件) 显式列出 简单直接 file(GLOB ...) 配合 CONFIGURE_DEPENDS 显式列出 按模块拆分 确定性优先,便利性次之 根据项目规模灵活选择

最后说两句

源文件管理这件事,说大不大,说小不小。但我在项目里见过太多因为源文件组织混乱导致的构建问题。有的是漏文件,有的是多文件,还有的是 GLOB 没刷新导致编译了旧代码。

我的建议很简单:别偷懒,也别过度设计。小项目就老老实实显式列出,大项目就按模块拆分。至于 aux_source_directory,忘掉它吧。

记住,CMake 的目标是让构建变得可预测、可维护。你的源文件管理方式,也应该服务于这个目标。


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