CMake 入门

不论你是写 C/C++、CUDA,还是要同时照顾 Linux/Windows/macOS,当项目走出“玩具”阶段,构建就会成为最大的摩擦源:不同编译器、不同 IDE、不同参数、不同依赖的发现与链接。如果还靠手写 Makefile 或分别维护 VS/Xcode 工程,成本会直线上升。

这正是 CMake 出现的原因:用一份跨平台的构建脚本,生成各平台各 IDE 需要的工程与命令。

CMake 解决了哪些痛点(精简版)

  • 跨平台:一份 CMakeLists.txt,到处生成工程(Ninja/Makefiles、Visual Studio、Xcode)。

  • 少折腾:自动管理头文件目录、编译选项、链接依赖,不用手拼长命令。

  • 更清爽:源代码与构建产物分离,目录不再被临时文件污染。

一图速懂:它到底做了什么



graph TD;
  A["写 CMakeLists.txt"] --> B["cmake -S 源码 -B 构建"];
  B --> C["构建:cmake --build 构建目录"];
  C --> D["运行:./build/hello(或 IDE 里运行)"];
  B --> E["按平台生成合适的工程/构建文件"];

为什么在 B 分叉

  • B 是“配置/生成”阶段,产生两种后续选择:
  • 走 C:继续用 cmake --build 统一构建(CMake 调用本机构建器编译/链接)。
  • 走 E:直接使用已生成的本机构建文件(Makefile、Ninja、Visual Studio、Xcode 工程)在命令行或 IDE 中构建。
  • 二者本质等价,通常二选一即可;团队混用也没问题。

配置阶段(B)的产物

  • 本机构建文件:Makefilebuild.ninja、VS .sln/.vcxproj、Xcode .xcodeproj(由生成器决定)。
  • CMake 缓存与元数据:CMakeCache.txtCMakeFiles/、平台/编译器/特性探测结果。
  • 可选辅助:compile_commands.json-DCMAKE_EXPORT_COMPILE_COMMANDS=ON)、通过 configure_file() 生成的配置头/源文件。
  • 位置:全部写入 -B 指定的构建目录。

构建阶段(C)的产物

  • 可执行文件:如 build/hello(Windows 为 build/hello.exe)。
  • 库文件:静态库(.a/.lib)、共享库(.so/.dll/.dylib)。
  • 中间文件:目标文件(*.o/*.obj)、依赖/缓存等,位于构建目录。
  • 自定义输出:可用 CMAKE_RUNTIME_OUTPUT_DIRECTORYCMAKE_LIBRARY_OUTPUT_DIRECTORYCMAKE_ARCHIVE_OUTPUT_DIRECTORY 调整位置。

5 分钟上手

目录结构:

1
2
3
your_project/
CMakeLists.txt
src/main.cpp

CMakeLists.txt

1
2
3
4
cmake_minimum_required(VERSION 3.20)
project(hello LANGUAGES CXX)
add_executable(hello src/main.cpp)
target_compile_features(hello PRIVATE cxx_std_17)

src/main.cpp

1
2
3
4
5
#include <iostream>
int main() {
std::cout << "Hello, CMake!" << std::endl;
return 0;
}

构建与运行:

1
2
3
cmake -S . -B build
cmake --build build
./build/hello

CMake 改变了什么:一个对比例子

场景:两个源文件 src/main.cppsrc/util.cpp,需要 C++17,并在 Linux 与 Windows 上都能构建。

  • 没有 CMake(每个平台都要记不同命令)

  • Linux(GCC/Clang):

1
g++ -std=c++17 -O2 src/main.cpp src/util.cpp -Iinclude -o app
  • Windows(MSVC):
1
cl /std:c++17 /O2 src\main.cpp src\util.cpp /I include /Fe:app.exe
  • 维护代价:命令不通用,IDE 工程(VS/Xcode)还要各自维护一份。

  • 有 CMake(一份脚本到处用)

CMakeLists.txt

1
2
3
4
5
cmake_minimum_required(VERSION 3.20)
project(app LANGUAGES CXX)
add_executable(app src/main.cpp src/util.cpp)
target_compile_features(app PRIVATE cxx_std_17)
target_include_directories(app PRIVATE include)

构建命令(所有平台一致):

1
2
cmake -S . -B build
cmake --build build

效果:不用关心底层是 Make 还是 Ninja、GCC 还是 MSVC,也不用为 VS/Xcode 手工维护工程;需要 IDE 时让 CMake 直接生成即可。诸如 -lm、异常/运行时等平台差异,交给生成器与工具链处理。

g++ 命令解析(在没有 CMake 时)

以 Linux 的命令为例:

1
g++ -std=c++17 -O2 src/main.cpp src/util.cpp -Iinclude -o app
  • g++:调用 GNU C++ 编译器。

  • -std=c++17:启用 C++17 标准(在 MSVC 下等价为 /std:c++17)。

  • -O2:开启二级优化(体积与速度的折中)。

  • src/main.cpp src/util.cpp:要编译并参与链接的源文件。

  • -Iinclude:增加头文件搜索目录 include

  • -o app:输出可执行文件名为 app

在实际项目中,这条命令会迅速膨胀:再加多目录、宏定义、库搜索路径(-L)、链接库(-lxxx)、平台差异参数,就会变得又长又难复用。这正是 CMake 希望你“声明语义、自动映射细节”的原因。

工作原理



graph TD;
  A["CMakeLists.txt:声明要什么"] --> B["配置阶段:检测平台/编译器/特性"];
  B --> C["选择生成器:Ninja/Makefiles/VS/Xcode"];
  C --> D["生成本机构建文件(规则/工程)"];
  D --> E["cmake --build:调用本机构建器编译/链接"];

一句话:你只写“意图”,CMake 按“环境”翻译成正确做法。

这几行背后发生了什么

1
2
3
4
5
cmake_minimum_required(VERSION 3.20)
project(app LANGUAGES CXX)
add_executable(app src/main.cpp src/util.cpp)
target_compile_features(app PRIVATE cxx_std_17)
target_include_directories(app PRIVATE include)
  • cmake_minimum_required(VERSION 3.20):锁定最低版本,启用对应语法与行为,避免旧版差异。

  • project(app LANGUAGES CXX):确定工程名与语言;CMake 会探测可用的 C++ 编译器与平台特性。

  • add_executable(app …):声明目标 app;CMake 为它建立依赖图,并在生成阶段把它翻译成本机的规则/工程项。

  • target_compile_features(app PRIVATE cxx_std_17):声明“需要 C++17”,CMake 按编译器映射成正确开关:

    • GCC/Clang:-std=c++17

    • MSVC:/std:c++17

  • target_include_directories(app PRIVATE include):声明头文件目录,按平台生成参数:

    • GCC/Clang:-I include

    • MSVC:/I include

这就是“一份脚本到处用”的核心:写语义,不写平台细节;由生成器与工具链适配差异。

CMake 支持哪些语言?

首要用户群是 C/C++,但并不止于此。常见支持包括:

  • C、C++:最成熟、生态最广。

  • CUDA、HIP:GPU 相关项目常用。

  • Fortran:在科学计算/数值领域仍然重要。

  • Objective-C/Swift(通过外部集成):在 Apple 平台工程生成方面配合 Xcode。

  • ASM、ISPC 等:低层或专用场景。

此外,CMake 也常被当作“元构建工具”,去驱动其他语言/工具链(如生成外部工具命令、打包脚本)。原则是:若目标/命令能被描述成“配置+构建”的过程,CMake 往往能充当胶水把它们组织起来,但其强项仍然是 C/C++ 及其周边。

就这么记住

  • 写清“要什么”:目标、源文件、需要的标准或库。

  • 总在源码外构建-S 指源码,-B 指构建目录。

  • 统一的构建命令cmake --build,不用关心底层是 Make 还是 Ninja。

结语

先用这套最小流程把程序跑起来,再根据需要逐步增加依赖与选项;当你不想再重复为不同平台维护多套工程时,CMake 就是省心的那一个。