All writing Notes & observations

CMake Tutorial

CMake零基础学习文档

前言

如果你是 C/C++ 开发者,你一定遇到过这样的问题:

  • 在 Windows 上用 VS 写的代码,到 Linux 上编译不了?

  • 项目越来越大,手动写 Makefile 越来越复杂,维护不动?

  • 第三方库的链接路径、头文件路径总是配不对,各种报错?

CMake 就是为了解决这些问题而生的。它是一个跨平台的构建工具生成器,一次编写配置,就能在 Windows、Linux、Mac 等所有平台上生成对应的构建文件(Makefile、VS 工程、Xcode 工程等),彻底解决跨平台编译的痛点,现在已经是 C/C++ 项目的标准构建方案。

这份文档会带你从完全的零基础,一步步掌握 CMake,从最简单的 Hello World,到企业级的大型项目配置,所有知识点都配有实战案例,看完就能上手。

演示的代码和 CMake 配置文件都放在 GitHub 上了,你可以直接克隆下来跟着做.


第一章:环境准备:安装 CMake

在开始之前,我们需要先安装 CMake 工具,不同系统的安装方式如下:

1.1 Linux 系统(Ubuntu/Debian)

1
2
3
4
5
6
# 直接通过包管理器安装
sudo apt-get update
sudo apt-get install cmake

# 验证安装
cmake --version

1.2 Mac 系统

1
2
3
4
5
# 使用Homebrew安装
brew install cmake

# 验证安装
cmake --version

1.3 Windows 系统

  1. 前往CMake 官网下载 Windows 安装包

  2. 安装时务必勾选 Add CMake to system PATH,否则命令行找不到 cmake

  3. 安装完成后,打开命令行输入cmake --version验证


第二章:CMake 核心工作原理

2.1 什么是源外构建?

CMake 推荐使用源外构建(Out-of-source Build),也就是把源码和编译产物完全分开,这样:

  • 源码目录永远干净,不会被编译产物污染

  • 可以同时为不同平台、不同配置(Debug/Release)创建不同的构建目录

  • 清理编译产物只需要删除 build 目录即可,完全不会影响源码

2.2 CMake 的工作流程

CMake 的工作分为两个核心阶段:

  1. 配置阶段:解析CMakeLists.txt,检测系统环境、编译器,生成对应平台的构建文件(Makefile/VS 工程等)

  2. 编译阶段:调用对应的构建工具(make/msbuild 等),真正编译源码生成二进制文件

整个流程的示意图如下:

标准的编译命令永远是这 4 步,所有项目通用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 1. 进入项目根目录
cd 你的项目目录

# 2. 创建并进入构建目录
# mkdir build && cd build  # Linux/Mac用这个
mkdir build; cd build  # Windows用这个

# 3. 配置阶段:生成构建文件
cmake ..

# 4. 编译阶段:编译源码
# make  # Linux/Mac用这个
cmake --build . # Windows用这个

第三章:基础入门:第一个 CMake 项目

这里以Windows系统为例,我们从最简单的单文件 Hello World 开始,一步步上手。

我使用的编译器是 Visual Studio 的 MSVC,VS 2019 及以上版本都支持 CMake 项目,VS Code 也可以通过插件支持 CMake,非常方便:

3.1 单文件项目:Hello World

项目结构

1
2
3
hello_cmake/
├── main.cpp          # 源码文件
└── CMakeLists.txt   # CMake配置文件,名字必须是这个!大小写敏感!

1. 编写源码 main.cpp

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

2. 编写 CMake 配置文件 CMakeLists.txt

这是 CMake 的核心配置文件,所有规则都写在这里,最简单的配置只需要 3 行:

1
2
3
4
5
6
7
8
# 1. 声明CMake的最低版本要求,必须写在第一行!
cmake_minimum_required(VERSION 3.10)

# 2. 声明项目名称
project(HelloCMake)

# 3. 告诉CMake:把main.cpp编译成名为hello的可执行文件
add_executable(hello main.cpp)

3. 编译运行

按照我们之前说的标准流程执行:

1
2
3
4
5
cd hello_cmake
mkdir build; cd build
cmake ..
cmake --build .
.\Debug\hello.exe # 没有指定编译类型的话,默认是Debug,生成的可执行文件在build/Debug目录下

你会看到输出:Hello CMake!,恭喜你,你的第一个 CMake 项目成功了!

如果要制定编译类型的话,可以通过 CMAKE_BUILD_TYPE 来切换这两个模式,比如:

1
2
3
4
5
# 编译调试版本
set(CMAKE_BUILD_TYPE Debug)

# 编译发布版本
set(CMAKE_BUILD_TYPE Release)

除此之外,也可以在VS Code里直接打开项目根目录,安装 CMake 插件,然后点击箭头所指的按钮,VS Code 会自动帮你执行上面的编译流程,非常方便:


第四章:进阶:多文件与模块化项目

当项目变大,有多个源文件、多个模块的时候,CMake 怎么处理?

4.1 同一目录下的多文件项目

比如我们有计算模块calc.cpp、日志模块log.cpp,还有主函数main.cpp,都在同一个目录下。

项目结构

1
2
3
4
5
6
7
multi_file/
├── main.cpp
├── calc.h
├── calc.cpp
├── log.h
├── log.cpp
└── CMakeLists.txt

各文件内容如下:

  • main.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <iostream>

#include "calc.h"
#include "log.h"

int main() {
    print_log("multiple file compiled and running!\n");

    int a = 10, b = 5;
    std::cout << "=== 计算模块测试 ===" << std::endl;
    std::cout << a << " + " << b << " = " << add(a,b) << std::endl;
    std::cout << a << " * " << b << " = " << mul(a,b) << std::endl;

    return 0;
}
  • calc.h
1
2
3
4
5
6
7
8
9
#ifndef CALC_H
#define CALC_H

// 加法函数声明
int add(int a, int b);
// 乘法函数声明
int mul(int a, int b);

#endif
  • calc.cpp
1
2
3
4
5
6
7
8
9
#include "calc.h"

int add(int a, int b) {
    return a + b;
}

int mul(int a, int b) {
    return a * b;
}
  • log.h
1
2
3
4
5
6
7
#ifndef LOG_H
#define LOG_H

// 日志打印函数声明
void print_log(const char* msg);

#endif
  • log.cpp
1
2
3
4
5
6
#include "log.h"
#include <iostream>

void print_log(const char* msg) {
    std::cout<< "[LOG] " << msg << std::endl;
}

CMakeLists.txt的两种写法

  • 写法 1:手动罗列所有源文件(适合文件少的情况)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 1. 最低CMake版本要求
cmake_minimum_required(VERSION 3.10)

# 2. 项目名称
project(MultiFileTest)

# 3. 核心:给VS编译器加UTF-8编码支持,解决中文乱码报错
if(MSVC)
    add_compile_options(/utf-8)
endif()

# 4. 把所有的源文件都加进来,一起编译
add_executable(multi_test 
    main.cpp 
    calc.cpp 
    log.cpp
)

  • 写法 2:自动收集源文件(适合文件多的情况)

不用手动一个个写文件名,CMake 帮你自动扫描目录下的所有源文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 1. 最低CMake版本要求
cmake_minimum_required(VERSION 3.10)

# 2. 项目名称
project(MultiFileTest)

# 3. 核心:给VS编译器加UTF-8编码支持,解决中文乱码报错
if(MSVC)
    add_compile_options(/utf-8)
endif()

# 4. 自动扫描当前目录下所有.cpp文件,存入SRC_LIST变量
aux_source_directory(. SRC_LIST)
add_executable(multi_test ${SRC_LIST})

注意:aux_source_directory不会递归扫描子目录,如果有子目录需要单独处理。


4.2 多目录项目:标准企业级结构

当项目越来越大,我们会把源码和头文件分开,不同模块放在不同目录,这是大型项目的标准结构:

标准项目结构

1
2
3
4
5
6
7
8
9
my_project/
├── main.cpp               # 主函数
├── include/               # 所有头文件放在这里
│   ├── calc.h
│   └── log.h
├── src/                   # 所有模块源码放在这里
│   ├── calc.cpp
│   └── log.cpp
└── CMakeLists.txt

核心配置

这个场景下,我们需要告诉 CMake 两件事:

  1. 头文件在哪里?(不然编译器找不到头文件)

  2. src 目录下的源文件要一起编译

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# 1. 最低CMake版本要求
cmake_minimum_required(VERSION 3.10)

# 2. 项目名称
project(MyProject)

# 3. 核心:给VS编译器加UTF-8编码支持,解决中文乱码报错
if(MSVC)
    add_compile_options(/utf-8)
endif()

# ========== 核心配置1:添加头文件搜索路径 ==========
# 告诉编译器:去include目录找头文件,这样代码里写#include "calc.h"就能找到了
include_directories(./include)

# ========== 核心配置2:收集src目录的源文件 ==========
aux_source_directory(./src SRC_LIST)

# 编译:主文件 + src的所有模块文件
add_executable(main main.cpp ${SRC_LIST})

编译运行的流程和之前完全一样。


4.3 编译库文件:静态库与动态库

当项目的模块越来越多,我们可以把公共模块编译成库文件,实现代码复用,主程序只需要链接库即可,这是模块化开发的核心。

CMake 支持两种库:

  • 静态库(STATIC):编译时把库的代码打包进可执行文件,运行时不需要依赖外部文件,体积大。

  • 动态库(SHARED):运行时才加载库文件,可执行文件体积小,但是需要配套发布.so/.dll 文件。

示例:把工具模块编译成静态库

我们还是用上面的多目录项目,这次把 src 的模块编译成库:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 1. 最低CMake版本要求
cmake_minimum_required(VERSION 3.10)

# 2. 项目名称
project(LibProject)

# 3. 核心:给VS编译器加UTF-8编码支持,解决中文乱码报错
if(MSVC)
    add_compile_options(/utf-8)
endif()

include_directories(./include)

# 4. 收集src的源文件
aux_source_directory(./src SRC_LIST)

# 5. 把src的源码编译成名为utils的静态库
add_library(utils STATIC ${SRC_LIST})
# 如果要编译动态库,只需要把STATIC改成SHARED即可:
# add_library(utils SHARED ${SRC_LIST})

# 6. 编译主程序
add_executable(main main.cpp)

# 7. 把utils库链接到主程序,这样主程序就能调用库里的函数了
target_link_libraries(main utils)

编译完成后,你会在 build 目录下看到:

  • 静态库:libutils.a(Linux)或者utils.lib(Windows)

这里的.pdb是调试符号文件,编译器生成的调试(Debug)版本会有这个文件,发布(Release)版本则没有。

  • 动态库:libutils.so(Linux)或者utils.dll(Windows)


第五章:现代 CMake 核心:作用域(PRIVATE/PUBLIC/INTERFACE)

这是现代 CMake 最重要的概念,也是新手最容易搞混的地方,搞懂这个,你就超过了 80% 的 CMake 使用者。

5.1 为什么需要作用域?

之前我们用的include_directories是全局的,所有目标都会生效,这在大型项目里很容易造成污染,比如我只想给某个库加头文件路径,结果所有项目都加上了。

现代 CMake 推荐用目标级的配置,也就是只给某个特定的目标设置属性,而且可以控制这个属性会不会传递给依赖它的其他目标,这就是三个作用域的作用:

5.2 三个作用域的区别

作用域 含义 适用场景
PRIVATE 这个依赖只给当前目标自己用,依赖它的其他目标拿不到 库内部用的依赖,比如库自己用了个 json 库,外部不需要
PUBLIC 这个依赖当前目标自己用,而且依赖它的其他目标也需要用 库的对外接口用到的依赖,比如库的头文件里用到了 Boost,那用这个库的人也需要 Boost
INTERFACE 当前目标自己不用,但是依赖它的其他目标需要用 纯头文件库,本身没有源码,只是给使用者传递依赖

举个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 我们有一个utils库
add_library(utils STATIC ${SRC_LIST})

# 给utils库设置头文件路径:
# PUBLIC意味着:不仅utils自己能找到这个头文件,所有链接utils的目标也能自动找到
target_include_directories(utils PUBLIC ./include)

# 然后我们的主程序只需要链接utils,就自动获得了include路径,不用再写一遍include_directories了!
add_executable(main main.cpp)
target_link_libraries(main utils)

这就是现代 CMake 的强大之处:依赖会自动传递,你不需要在每个地方都重复配置头文件、链接库,只要链接了目标,所有的依赖都会自动带过来。


第六章:常用进阶功能

6.1 指定 C/C++ 标准

我们可以指定项目要用的 C/C++ 标准,比如 C++11、C++17:

1
2
3
4
5
6
7
# 设置C++标准为17
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) # 强制要求,不满足就报错

# 如果是C语言
set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED ON)

6.2 条件编译:按需编译

我们可以通过开关来控制是否编译某些功能,比如是否编译测试代码,是否开启调试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# 定义一个编译选项,默认是OFF,用户可以通过cmake -DBUILD_TESTS=ON来开启
option(BUILD_TESTS "是否编译测试代码" OFF)
option(ENABLE_DEBUG "是否开启调试模式" OFF)

# 根据选项执行不同的逻辑
if(BUILD_TESTS)
    # 如果开启了测试,就编译tests目录
    add_subdirectory(tests)
endif()

if(ENABLE_DEBUG)
    # 调试模式,加调试信息
    add_compile_options(-g)
    set(CMAKE_BUILD_TYPE Debug)
else()
    # 发布模式,加优化
    add_compile_options(-O2)
    set(CMAKE_BUILD_TYPE Release)
endif()

用户在执行 cmake 的时候,可以直接传参数来控制:

1
cmake .. -DBUILD_TESTS=ON -DENABLE_DEBUG=ON

6.3 第三方依赖管理

实际项目中我们总会用到第三方库,比如 Boost、OpenCV、protobuf 等,CMake 提供了两种方式来管理:

方式 1:find_package:使用系统已安装的库

如果用户已经安装了第三方库,CMake 可以自动找到它:

1
2
3
4
5
# 查找Boost库,要求1.65以上版本,需要filesystem和system组件
find_package(Boost 1.65 REQUIRED COMPONENTS filesystem system)

# 找到之后,直接链接即可,所有的头文件、库路径都会自动配置好
target_link_libraries(main PRIVATE Boost::filesystem Boost::system)

不需要手动找路径,CMake 帮你全部搞定。

方式 2:FetchContent:自动下载编译第三方库

如果用户没有安装第三方库,我们可以让 CMake 自动从 Git 下载源码,自动编译,完全不需要用户手动安装:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 引入FetchContent模块
include(FetchContent)

# 声明要下载的abseil库
FetchContent_Declare(
  abseil
  GIT_REPOSITORY https://github.com/abseil/abseil-cpp.git
  GIT_TAG 20240116.0 # 指定版本,避免拉取最新代码出问题
)

# 自动下载并编译
FetchContent_MakeAvailable(abseil)

# 直接链接即可,和本地安装的一样用
target_link_libraries(main PRIVATE absl::strings absl::base)

这个功能简直是神器,再也不用让用户手动安装各种依赖了,CMake 帮你全部搞定。


第七章:跨平台编译技巧

CMake 的最大优势就是跨平台,我们可以针对不同系统写不同的配置:

7.1 平台判断

CMake 内置了很多变量可以判断当前的系统:

1
2
3
4
5
6
7
8
9
if(WIN32)
    # Windows系统的配置
    add_compile_definitions(_CRT_SECURE_NO_WARNINGS) # 关闭VS的安全警告
elseif(APPLE)
    # Mac系统的配置
elseif(UNIX)
    # Linux/Unix系统的配置
    target_link_libraries(main PRIVATE pthread) # 链接线程库
endif()

7.2 编译器判断

也可以针对不同的编译器做配置:

1
2
3
4
5
6
7
8
9
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
    # GCC编译器,加GCC专属的警告选项
    add_compile_options(-Wall -Wextra)
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
    # Clang编译器
elseif(MSVC)
    # MSVC编译器
    add_compile_options(/W4)
endif()

第八章:常见错误与解决方案

新手用 CMake 最容易遇到这些错误,直接对照解决即可:

错误 1:fatal error: xxx.h: No such file or directory

  • 原因:编译器找不到头文件,你没加头文件搜索路径

  • 解决方案:添加include_directories(头文件目录)或者target_include_directories

错误 2:undefined reference to xxx

  • 原因:链接错误,函数有声明但是没实现,或者没链接库

  • 解决方案:

    1. 检查有没有把实现的.cpp 文件加入到add_executable里

    2. 如果是第三方库,检查有没有用target_link_libraries链接库

错误 3:error: ‘for’ loop initial declarations are only allowed in C99 mode

  • 原因:你用了 C99 的特性,但是没指定 C 标准

  • 解决方案:添加set(CMAKE_C_STANDARD 99)和set(CMAKE_C_STANDARD_REQUIRED ON)

错误 4:CMake Error: The source directory xxx does not appear to contain CMakeLists.txt

  • 原因:你执行 cmake 的目录不对,上级目录没有 CMakeLists.txt

  • 解决方案:确认你在 build 目录,而且 CMakeLists.txt 在项目根目录

错误 5:修改了 CMakeLists.txt 但是不生效

  • 原因:CMake 有缓存,旧的配置没更新

  • 解决方案:删除 build 目录,重新执行 cmake 和 make


第九章:完整的企业级项目模板

最后给你一个完整的、可以直接用的大型项目 CMake 模板,覆盖了所有常用配置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# 最低版本要求
cmake_minimum_required(VERSION 3.14)

# 项目信息
project(MyAwesomeProject 
    VERSION 1.0.0
    LANGUAGES CXX
)

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

# 输出路径配置
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib)

# 编译选项
if(MSVC)
    add_compile_options(/W4 /utf-8)
else()
    add_compile_options(-Wall -Wextra -Wpedantic)
endif()

# 编译选项
option(BUILD_TESTS "Build test programs" OFF)
option(BUILD_SHARED_LIBS "Build shared libraries" OFF)

# 头文件路径
include_directories(${PROJECT_SOURCE_DIR}/include)

# 编译子模块
add_subdirectory(src)

# 测试代码
if(BUILD_TESTS)
    enable_testing()
    add_subdirectory(tests)
endif()

# 第三方依赖
find_package(Threads REQUIRED)
target_link_libraries(MyLib PRIVATE Threads::Threads)

# 自动下载依赖示例
include(FetchContent)
FetchContent_Declare(
    googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG v1.14.0
)
FetchContent_MakeAvailable(googletest)

对应的项目结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
MyAwesomeProject/
├── CMakeLists.txt
├── include/          # 头文件
│   └── myproject/
│       ├── xxx.h
├── src/              # 源码
│   ├── CMakeLists.txt
│   ├── xxx.cpp
├── tests/            # 测试代码
│   ├── CMakeLists.txt
│   ├── test_xxx.cpp
└── third_party/      # 第三方源码(可选)

第十章:后续学习资源

如果你想深入学习,可以参考这些资源:

  1. 官方教程:https://cmake.org/cmake/help/latest/guide/tutorial/index.html 最权威的官方教程,一步步带你做项目

  2. Modern CMake:https://cliutils.gitlab.io/modern-cmake/ 现代 CMake 的最佳实践

  3. CMake 官方文档:https://cmake.org/documentation/ 完整的命令查询手册


写在最后

CMake 的学习其实很简单,核心就是:

  • 记住标准的 4 步编译流程

  • 搞懂目标和作用域的概念

  • 优先用现代 CMake 的目标级配置,少用全局命令

跟着这份文档的案例一步步做,你很快就能从 CMake 小白变成高手,搞定所有的跨平台编译问题。