第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 + 源代码)、配置(生成构建文件)、构建(编译链接)。你只需要维护好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 是施工队。蓝图画好了,施工就稳了。
可可精品资料库