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 系统
-
前往CMake 官网下载 Windows 安装包
-
安装时务必勾选 Add CMake to system PATH,否则命令行找不到 cmake
-
安装完成后,打开命令行输入cmake --version验证
第二章:CMake 核心工作原理
2.1 什么是源外构建?
CMake 推荐使用源外构建(Out-of-source Build),也就是把源码和编译产物完全分开,这样:
2.2 CMake 的工作流程
CMake 的工作分为两个核心阶段:
-
配置阶段:解析CMakeLists.txt,检测系统环境、编译器,生成对应平台的构建文件(Makefile/VS 工程等)
-
编译阶段:调用对应的构建工具(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
|
各文件内容如下:
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;
}
|
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
|
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;
}
|
1
2
3
4
5
6
7
|
#ifndef LOG_H
#define LOG_H
// 日志打印函数声明
void print_log(const char* msg);
#endif
|
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
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
)
|
不用手动一个个写文件名,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 两件事:
-
头文件在哪里?(不然编译器找不到头文件)
-
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 支持两种库:
示例:把工具模块编译成静态库
我们还是用上面的多目录项目,这次把 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
错误 2:undefined reference to xxx
错误 3:error: ‘for’ loop initial declarations are only allowed in C99 mode
错误 4:CMake Error: The source directory xxx does not appear to contain CMakeLists.txt
错误 5:修改了 CMakeLists.txt 但是不生效
第九章:完整的企业级项目模板
最后给你一个完整的、可以直接用的大型项目 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/ # 第三方源码(可选)
|
第十章:后续学习资源
如果你想深入学习,可以参考这些资源:
-
官方教程:https://cmake.org/cmake/help/latest/guide/tutorial/index.html 最权威的官方教程,一步步带你做项目
-
Modern CMake:https://cliutils.gitlab.io/modern-cmake/ 现代 CMake 的最佳实践
-
CMake 官方文档:https://cmake.org/documentation/ 完整的命令查询手册
写在最后
CMake 的学习其实很简单,核心就是:
跟着这份文档的案例一步步做,你很快就能从 CMake 小白变成高手,搞定所有的跨平台编译问题。