1. 项目初始化:CMakeLists.txt 从零编写,构建系统选型与目录结构设计
说实话,我见过太多 C++ 项目,一上来就开干。代码写得飞起,结果三个月后连编译都成问题。嗯,这其实是可以避免的。项目初始化就像盖楼打地基,地基歪了,后面再漂亮的代码也白搭。
今天我们就聊聊这个「地基」怎么打。我会从三个角度切入:构建系统选型、目录结构设计、以及 CMakeLists.txt 的从零编写。这三件事做好了,后面几十号人协作都不会乱。
构建系统选型:Make 还是 Ninja?
先问个问题:你平时用什么构建?我猜大部分人会回答 Make。但说实话,在大型项目里,我越来越倾向于 Ninja。
为什么会这样?我们来对比一下。
| 特性 | Make | Ninja |
|---|---|---|
| 构建速度 | 中等,依赖解析较慢 | 极快,专为增量编译优化 |
| 并行支持 | 支持(-j 参数) | 默认并行,自动利用多核 |
| 语法复杂度 | 较复杂,规则多 | 极简,但需生成器配合 |
| 跨平台 | Linux 原生,Windows 需额外工具 | 全平台支持 |
| 与 CMake 配合 | 生成 Makefile | 生成 build.ninja |
我个人习惯是:开发阶段用 Ninja,发布阶段用 Make。Ninja 快,适合频繁编译调试;Make 生态成熟,适合 CI/CD 流水线。
cmake -G "Ninja" .. 或 cmake -G "Unix Makefiles" ..
我在项目中遇到过一个问题:团队用 Make,每次改一行代码要等 40 秒重新链接。换成 Ninja 后,降到 8 秒。你想想看,一天下来省多少时间?
项目目录结构设计
目录结构这事,看似简单,其实坑很多。我见过把全部代码塞进 src 的,也见过目录深到 7 层的。都不好。
我推荐一个经过多个项目验证的结构:
my_project/
├── CMakeLists.txt # 顶层构建文件
├── src/ # 源代码
│ ├── CMakeLists.txt
│ ├── main.cpp
│ ├── module_a/
│ │ ├── CMakeLists.txt
│ │ ├── a.cpp
│ │ └── a.h
│ └── module_b/
│ ├── CMakeLists.txt
│ ├── b.cpp
│ └── b.h
├── include/ # 公共头文件
│ └── my_project/
│ ├── api.h
│ └── config.h
├── lib/ # 第三方库(编译后)
│ ├── libfoo.a
│ └── libbar.so
├── test/ # 单元测试
│ ├── CMakeLists.txt
│ ├── test_a.cpp
│ └── test_b.cpp
└── third_party/ # 第三方源码(如 git submodule)
├── googletest/
└── spdlog/
这个结构有几个好处:
- src 和 include 分离:内部实现不暴露给外部
- 模块化:每个模块有自己的 CMakeLists.txt,方便独立编译
- test 独立:测试代码不污染主工程
- third_party 统一管理:避免到处拷贝第三方库
另外,include 目录下再加一层项目名(如 include/my_project/),这是为了防止头文件冲突。你想想看,如果两个库都有 config.h,那编译时就会出问题。
CMakeLists.txt 从零编写
好,结构定好了,接下来写 CMakeLists.txt。我习惯从顶层开始,一层层往下写。
顶层 CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(MyProject VERSION 1.0.0 LANGUAGES CXX)
# C++ 标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# 构建类型
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Build type" FORCE)
endif()
# 输出目录
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
# 添加子目录
add_subdirectory(src)
add_subdirectory(test)
这里有几个关键点:
cmake_minimum_required指定最低版本,我一般用 3.16,因为支持很多新特性CMAKE_CXX_STANDARD 17是现代 C++ 的起点,别再用 C++11 了- 输出目录统一到
bin/和lib/,方便后续打包
src/CMakeLists.txt
# 添加模块
add_subdirectory(module_a)
add_subdirectory(module_b)
# 主程序
add_executable(my_app main.cpp)
target_link_libraries(my_app
PRIVATE
module_a
module_b
)
src/module_a/CMakeLists.txt
add_library(module_a
STATIC
a.cpp
a.h
)
target_include_directories(module_a
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
)
target_include_directories(module_a
PRIVATE
${CMAKE_SOURCE_DIR}/include
)
这里我用了 PUBLIC 和 PRIVATE 来控制头文件可见性。简单说:
- PUBLIC:模块自己的头文件,其他模块也能用
- PRIVATE:只有模块内部能用,外部看不到
test/CMakeLists.txt
# 添加 Google Test
add_subdirectory(${CMAKE_SOURCE_DIR}/third_party/googletest
${CMAKE_BINARY_DIR}/googletest)
# 测试程序
add_executable(test_a test_a.cpp)
target_link_libraries(test_a
PRIVATE
module_a
gtest_main
)
add_test(NAME test_a COMMAND test_a)
测试这块,我习惯用 Google Test。它成熟、文档全,而且 CMake 集成很方便。
知识体系总览
下面这张图,把整个初始化的核心逻辑串起来了。你可以把它当作一个检查清单:
一些经验之谈
最后,分享几个我踩过的坑:
- 不要手动管理第三方库:用 git submodule 或 FetchContent。我曾经手动拷贝过 spdlog,结果版本混乱,编译报错查了半天。
- CMake 版本要统一:团队里有人用 3.10,有人用 3.20,写出来的 CMakeLists.txt 互相不兼容。统一版本,省心。
- 测试要早加:别等代码写完了再补测试。项目初期就搭好测试框架,后面每加一个模块就写几个测试,养成习惯。
好了,项目初始化这部分就聊到这。记住:好的开始是成功的一半。把 CMakeLists.txt 写清楚、目录结构设计好,后面的事情就顺了。
公众号:蓝海资料掘金营,微信 deep3321