刚开始写 C++ 时,项目里只有一个 main.cpp,直接执行一条 g++ 命令就能完成编译。等源文件、头文件和第三方库逐渐增多,编译命令也会越来越长。这时继续手写命令不仅麻烦,还容易漏掉文件或链接选项。
CMake 的作用就是把这些构建规则统一写在 CMakeLists.txt 中,再根据当前平台生成 Makefile、Ninja 或 IDE 工程。它本身通常不直接完成编译,而是先生成构建系统,再由对应的编译工具执行真正的编译和链接。
一、CMake 的基本工作流程
一个 CMake 项目通常经历三个阶段:
CMakeLists.txt
↓ 配置和生成
构建系统(Makefile、Ninja 等)
↓ 编译和链接
可执行程序或库文件
推荐把构建产物放在单独的 build 目录,不要和源码混在一起:
cmake -S . -B build
cmake –build build
这两条命令分别表示:
- -S .:源码目录是当前目录;
- -B build:生成文件写入 build 目录;
- cmake –build build:调用当前生成器完成构建。
这种写法不依赖底层使用的是 Make 还是 Ninja,也比手动进入 build 目录再执行 cmake .. 更直观。
需要重新构建时,通常直接再次执行:
cmake –build build
如果修改了 CMakeLists.txt,CMake 会在构建前检查并重新生成必要的文件。
二、第一个 CMakeLists.txt
先准备一个最简单的目录:
hello_cmake/
├── 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.16)
project(HelloCMake LANGUAGES CXX)
add_executable(hello main.cpp)
这里出现了三个最基础的命令。
1. cmake_minimum_required
cmake_minimum_required(VERSION 3.16)
它声明项目要求的最低 CMake 版本,同时确定一组与该版本对应的策略行为。一般放在文件开头,不建议省略。
2. project
project(HelloCMake LANGUAGES CXX)
HelloCMake 是工程名,LANGUAGES CXX 表示项目使用 C++。还可以同时声明版本号:
project(HelloCMake VERSION 1.0 LANGUAGES CXX)
3. add_executable
add_executable(hello main.cpp)
这条命令创建名为 hello 的可执行目标,后面列出参与编译的源文件。目标名不必与 project() 中的工程名相同。
在项目根目录执行:
cmake -S . -B build
cmake –build build
Linux 下可以运行:
./build/hello
如果使用 Visual Studio 这类多配置生成器,可执行文件可能位于 build/Debug 或 build/Release 中。
三、指定 C++ 标准和构建类型
项目使用 C++17 时,可以在 CMakeLists.txt 中设置:
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
三行配置分别表示:
- 使用 C++17;
- 编译器不支持时直接报错,不自动退回更旧的标准;
- 尽量使用标准模式,而不是编译器特有的 GNU 扩展模式。
也可以只给某个目标声明它需要的语言特性:
target_compile_features(hello PRIVATE cxx_std_17)
对于单配置生成器,可以在配置阶段指定 Debug 或 Release:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
切换为发布构建:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
Visual Studio、Xcode 等多配置生成器通常在构建阶段选择配置:
cmake –build build –config Release
四、变量和列表
CMake 使用 set() 定义变量:
set(APP_NAME calculator)
set(APP_SOURCES main.cpp add.cpp sub.cpp)
add_executable(${APP_NAME} ${APP_SOURCES})
${APP_NAME} 表示读取变量的值。CMake 的列表本质上由分号分隔,因此下面两种写法都能创建列表:
set(SOURCES main.cpp add.cpp sub.cpp)
set(SOURCES main.cpp;add.cpp;sub.cpp)
需要追加或删除元素时,可以使用 list():
list(APPEND SOURCES mul.cpp)
list(REMOVE_ITEM SOURCES sub.cpp)
五、源文件应该手写还是自动搜索
源文件较少时,直接列出来最稳妥:
add_executable(calculator
main.cpp
add.cpp
sub.cpp
)
新增或删除源文件时,构建规则的变化在代码审查中一目了然。
也可以使用 file(GLOB) 搜索目录中的文件:
file(GLOB APP_SOURCES CONFIGURE_DEPENDS
"${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp"
)
add_executable(calculator ${APP_SOURCES})
CONFIGURE_DEPENDS 会让 CMake 在构建时检查匹配结果是否发生变化。不过对于长期维护的工程,仍然更推荐显式列出源文件;自动搜索更适合示例项目或文件数量较多、结构固定的场景。
常见路径变量有:
- PROJECT_SOURCE_DIR:最近一次 project() 对应的源码根目录;
- PROJECT_BINARY_DIR:该工程对应的构建目录;
- CMAKE_CURRENT_SOURCE_DIR:当前 CMakeLists.txt 所在的源码目录;
- CMAKE_CURRENT_BINARY_DIR:当前目录对应的构建目录。
六、添加头文件目录
假设头文件放在 include 目录:
project/
├── include/
│ └── calc.h
├── src/
│ └── calc.cpp
└── main.cpp
不建议一上来使用影响整个目录的 include_directories(),更推荐把头文件路径绑定到具体目标:
add_executable(calculator
main.cpp
src/calc.cpp
)
target_include_directories(calculator PRIVATE
${PROJECT_SOURCE_DIR}/include
)
这样可以明确看出 include 是 calculator 目标的编译需求,不会无意间影响其他目标。
七、生成静态库和动态库
使用 add_library() 可以创建库目标:
add_library(calc STATIC
src/add.cpp
src/sub.cpp
)
STATIC 表示静态库。在 Linux 下通常生成 libcalc.a,在 Windows 下通常生成 .lib 文件。
把 STATIC 改为 SHARED 即可生成动态库:
add_library(calc SHARED
src/add.cpp
src/sub.cpp
)
Linux 下通常生成 libcalc.so,Windows 下通常生成 .dll 及相应的导入库。
如果希望由构建选项决定库类型,可以不写 STATIC 或 SHARED:
add_library(calc
src/add.cpp
src/sub.cpp
)
配置时通过 BUILD_SHARED_LIBS 选择:
cmake -S . -B build -DBUILD_SHARED_LIBS=ON
八、链接库以及 PUBLIC、PRIVATE、INTERFACE
创建库后,可以通过目标名直接链接:
add_executable(calculator main.cpp)
target_link_libraries(calculator PRIVATE calc)
如果 calc 是同一个 CMake 工程中的目标,不需要手动写它生成后的绝对路径,也不需要调用 link_directories()。CMake 能识别目标之间的依赖关系,并按正确顺序完成构建。
target_link_libraries() 和 target_include_directories() 中经常出现三个关键字:
- PRIVATE:只供当前目标使用;
- PUBLIC:当前目标使用,同时传递给依赖它的目标;
- INTERFACE:当前目标自己不用,只传递给使用者。
以库的头文件为例:
target_include_directories(calc
PUBLIC ${PROJECT_SOURCE_DIR}/include
)
calc 编译时需要该目录,链接 calc 的程序也需要通过该目录找到公开头文件,因此这里使用 PUBLIC。
而应用程序链接 calc 时通常写:
target_link_libraries(calculator PRIVATE calc)
应用程序本身不会再被其他目标当作库来使用,所以没有必要向外传递这个依赖。
九、用多级 CMakeLists.txt 管理工程
下面把计算器拆成库和应用程序两个部分:
calculator/
├── CMakeLists.txt
├── include/
│ └── calc/
│ └── calc.h
├── src/
│ ├── CMakeLists.txt
│ └── calc.cpp
└── app/
├── CMakeLists.txt
└── main.cpp
头文件 include/calc/calc.h:
#pragma once
namespace calc
{
int add(int left, int right);
int sub(int left, int right);
}
实现文件 src/calc.cpp:
#include "calc/calc.h"
namespace calc
{
int add(int left, int right)
{
return left + right;
}
int sub(int left, int right)
{
return left – right;
}
}
程序入口 app/main.cpp:
#include <iostream>
#include "calc/calc.h"
int main()
{
std::cout << "20 + 10 = " << calc::add(20, 10) << '\\n';
std::cout << "20 – 10 = " << calc::sub(20, 10) << '\\n';
return 0;
}
根目录的 CMakeLists.txt 负责工程级配置和添加子目录:
cmake_minimum_required(VERSION 3.16)
project(Calculator VERSION 1.0 LANGUAGES CXX)
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)
add_subdirectory(src)
add_subdirectory(app)
src/CMakeLists.txt 创建静态库:
add_library(calc STATIC
calc.cpp
)
target_include_directories(calc
PUBLIC ${PROJECT_SOURCE_DIR}/include
)
app/CMakeLists.txt 创建可执行程序并链接库:
add_executable(calculator
main.cpp
)
target_link_libraries(calculator PRIVATE calc)
构建并运行:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake –build build
./build/bin/calculator
输出:
20 + 10 = 30
20 – 10 = 10
这个结构的关键不是把一个很长的 CMakeLists.txt 拆成几个短文件,而是让每个子目录只负责定义自己的目标。根目录负责组织,库目录负责公开使用要求,应用目录负责组合目标,依赖关系会更清楚。
十、添加宏定义和日志
如果只想给某个目标定义 DEBUG 宏,可以使用:
target_compile_definitions(calculator PRIVATE DEBUG)
对应的 C++ 代码:
#ifdef DEBUG
std::cout << "debug mode" << std::endl;
#endif
相比全局的 add_definitions(-DDEBUG),目标级命令不会影响无关的库或程序。
排查配置问题时,可以使用 message() 输出变量:
message(STATUS "source dir: ${PROJECT_SOURCE_DIR}")
message(STATUS "binary dir: ${PROJECT_BINARY_DIR}")
需要主动终止配置时,可以写:
if(NOT EXISTS "${PROJECT_SOURCE_DIR}/include")
message(FATAL_ERROR "include directory not found")
endif()
STATUS 用于普通提示,WARNING 会显示警告但继续配置,FATAL_ERROR 会立即终止配置。
网硕互联帮助中心


评论前必须登录!
注册