第4章 CMake入门:从零开始构建你的C++项目

说实话,我刚入行那会儿,项目构建全靠手写Makefile。那叫一个痛苦——稍微改个文件路径,整个构建就崩了。后来接触到CMake,我才发现原来构建可以这么清爽。今天我就带你从零开始,把CMake玩明白。

4.1 为什么是CMake?

你可能会问:Makefile不是挺好的吗?嗯,对于只有一个源文件的小项目,确实够用。但一旦项目膨胀到几十个源文件、依赖第三方库、需要跨平台编译……手写Makefile就成了噩梦。

CMake解决的核心问题就一个:让你用更少的代码,管理更复杂的构建。它不直接编译代码,而是生成Makefile(或Ninja、Visual Studio工程文件),然后让真正的编译器干活。

核心思想:CMakeLists.txt 描述“要做什么”,CMake 负责“怎么做”。

4.2 第一个CMakeLists.txt

我们先从一个最简单的例子开始。假设你有一个 main.cpp:

// main.cpp
#include <iostream>

int main() {
    std::cout << "Hello, CMake!" << std::endl;
    return 0;
}

在同一个目录下,创建 CMakeLists.txt:

cmake_minimum_required(VERSION 3.10)
project(HelloCMake)
add_executable(hello main.cpp)

就这么三行。我来解释一下:

  • cmake_minimum_required:指定CMake的最低版本。我习惯写3.10以上,因为很多新特性从3.10开始支持。
  • project:定义项目名称。这个名字会影响到后续的一些变量,比如 PROJECT_NAME。
  • add_executable:告诉CMake,我们要生成一个可执行文件,名字叫 hello,源文件是 main.cpp。

小技巧:add_executable 的第一个参数是目标名称,第二个及以后是源文件列表。如果有多个源文件,用空格或分号隔开都可以。

4.3 构建与生成:两步走

写好了CMakeLists.txt,接下来就是构建。我个人习惯在项目根目录下创建一个 build 文件夹,把构建产物都放进去,保持源码目录干净。

mkdir build
cd build
cmake ..
make

这里 cmake .. 是配置阶段,它会读取上一级目录的CMakeLists.txt,生成Makefile。make 才是真正的编译阶段。

如果你用的是Ninja(一个更快的构建工具),可以这样:

cmake .. -G Ninja
ninja

嗯,这里要注意:-G 参数指定生成器。Windows上还可以用 -G "Visual Studio 17 2022" 生成VS工程。

4.4 设置C++标准

我记得刚用C++11那会儿,很多编译器默认还是C++98。如果不显式指定标准,代码里用了 auto、nullptr 这些新特性,编译直接报错。所以,设置C++标准是必须的。

有两种方式:

方式一:全局设置(推荐)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
  • CMAKE_CXX_STANDARD:指定标准版本,比如11、14、17、20。
  • CMAKE_CXX_STANDARD_REQUIRED:如果设为 ON,编译器不支持该标准就直接报错,而不是降级。
  • CMAKE_CXX_EXTENSIONS:设为 OFF 表示禁用编译器扩展(比如GCC的 -std=gnu++17),保证代码可移植。

方式二:目标级别设置

add_executable(hello main.cpp)
target_compile_features(hello PUBLIC cxx_std_17)

这种方式更精细,可以针对不同目标设置不同标准。不过对于大多数项目,全局设置就够用了。

避坑指南:我曾经在一个项目里忘了设置 CMAKE_CXX_STANDARD_REQUIRED,结果CI机器上的老编译器默默降级到了C++14,导致一些C++17的特性在运行时行为异常。排查了整整一天……所以,这个选项一定要加上。

4.5 添加多个源文件

项目大了,源文件自然就多了。你当然可以这样写:

add_executable(myapp main.cpp utils.cpp network.cpp database.cpp)

但这样太死板。每次新增文件都要改CMakeLists.txt,容易漏掉。更好的做法是用 file 命令:

file(GLOB SOURCES "src/*.cpp")
add_executable(myapp ${SOURCES})

这样,src 目录下所有 .cpp 文件都会被自动包含。不过要注意,file(GLOB) 不会自动检测新增文件——如果你在 src 里加了一个新 .cpp,需要重新运行 cmake 才能生效。

我的习惯:对于小项目,用 file(GLOB) 省事。对于大项目,还是老老实实手动列出源文件,因为这样CMake能精确追踪依赖关系,增量编译更可靠。

4.6 完整的CMakeLists.txt示例

把上面这些知识点串起来,一个比较完整的CMakeLists.txt长这样:

cmake_minimum_required(VERSION 3.10)
project(MyAwesomeProject)

# 设置C++标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

# 收集源文件
file(GLOB SOURCES "src/*.cpp")

# 添加可执行文件
add_executable(myapp ${SOURCES})

# 添加头文件搜索路径
target_include_directories(myapp PRIVATE include)

这里多了一行 target_include_directories,它告诉编译器去哪里找头文件。如果你的头文件放在 include 目录下,加上这一行,代码里就可以直接用 #include "utils.h" 了。

4.7 知识体系总览

为了让你更直观地理解CMake的构建流程,我画了一张图:

CMake 构建流程 CMakeLists.txt 描述构建规则 源代码 .cpp / .h 文件 cmake .. (配置阶段) 生成 Makefile / Ninja 构建文件 make / ninja (构建阶段) 编译、链接,生成可执行文件 可执行文件 配置阶段只执行一次,构建阶段可反复执行

这张图把CMake的构建流程分成了三个阶段:输入(CMakeLists.txt + 源代码)、配置(生成构建文件)、构建(编译链接)。你只需要维护好CMakeLists.txt,剩下的交给CMake和编译器。

4.8 避坑总结

最后,分享几个我踩过的坑:

  • 路径问题:CMake里的路径都是相对于当前CMakeLists.txt的,不是相对于build目录的。所以 add_executable 里写 src/main.cpp 而不是 ../src/main.cpp。
  • 大小写敏感:CMake命令不区分大小写,但变量名区分。比如 set(CMAKE_CXX_STANDARD 17) 和 set(cmake_cxx_standard 17) 是两回事。
  • 重新配置:修改了CMakeLists.txt后,一定要重新运行 cmake ..,否则修改不会生效。我见过有人只改文件不重新配置,然后跑来问我为什么编译不过……

好了,CMake入门就讲到这里。你现在应该能写出一个像样的CMakeLists.txt了。下一节我们会深入CMake的变量和函数,让构建脚本更灵活。不过那是后话了,先把今天的内容消化掉。

一句话总结:CMakeLists.txt 是你的构建蓝图,cmake 是画图的人,make 是施工队。蓝图画好了,施工就稳了。

可可精品资料库