Linux C++ 之 CMake:从入门到工程化构建

Linux C++ 之 CMake:从入门到工程化构建

摘要:在 Linux 下开发 C++ 程序时,手写 Makefile 往往面临跨平台适配难、依赖管理混乱、大型项目维护成本高的问题。而 CMake 作为跨平台构建系统生成器,能通过简洁的 CMakeLists.txt 自动生成适配不同平台/生成器的构建文件(Makefile、Ninja、VS 工程等),并提供目标化(target-based)的依赖、编译选项、安装导出等工程能力。本文从核心价值、基础语法、进阶工程化、依赖管理、测试安装、常见坑与命令速查出发,形成一份CMake 使用笔记。


一、为什么 Linux C++ 开发必须学 CMake?

Linux 下 C++ 开发绕不开"构建"环节,但传统方式存在痛点:

  • 手写 Makefile 复杂:语法晦涩;多文件/多目录项目依赖关系复杂,维护成本随规模上升;
  • 跨平台兼容差:为 Linux 写的 Makefile 很难无痛迁移到 macOS/Windows;
  • 依赖管理难:链接第三方库(Boost/OpenCV/fmt...)需手动指定 include/lib 路径,容易出错;
  • 配置混乱:Debug/Release、编译选项、宏定义等切换易遗漏;
  • 大型工程不可控:缺少标准化构建流程、安装导出、测试集成等能力。

CMake 的核心价值:一次写规则,多处生成构建系统 。只需维护 CMakeLists.txt,在不同平台上用不同生成器(Make/Ninja/VS)构建,同时通过 target 机制把"include/define/link/options"绑定到目标,构建规则更可维护、更可复用。


二、核心概念与环境准备

2.1 核心概念

概念 含义 作用
CMakeLists.txt CMake 配置入口 定义项目、目标、依赖、选项、安装等
构建目录(Build Dir) 编译产物目录 推荐源码外构建,保持源码干净
生成器(Generator) 生成构建系统类型 Unix Makefiles、Ninja、VS
目标(Target) 可执行/库的抽象 以 target 为中心配置 include/link/options
配置阶段 vs 构建阶段 configure 生成规则;build 执行编译 cmake -S/-B 配置;cmake --build 构建
Cache(缓存) build 目录里的配置缓存 改变量/选项常需要重新配置或清缓存
find_package 查找已安装依赖 优先使用"目标导入"(Imported Targets)
install/export 安装与导出包 让别人能 find_package(YourLib)

2.2 安装

bash 复制代码
# Debian / Ubuntu
sudo apt-get install cmake cmake-data

# CentOS / RHEL
sudo yum install cmake

cmake --version   # 推荐 3.10+,更建议 3.16+ / 3.20+(现代工程更顺滑)

推荐安装 Ninja(更快、更适合增量):

bash 复制代码
sudo apt-get install ninja-build

三、基础实操:CMake 入门

3.1 最简示例:单文件程序

main.cpp

cpp 复制代码
#include <iostream>
int main() {
    std::cout << "Hello CMake!" << std::endl;
    return 0;
}

CMakeLists.txt

cmake 复制代码
cmake_minimum_required(VERSION 3.10)
project(HelloCMake LANGUAGES CXX)

add_executable(hello_cmake main.cpp)

# 推荐:用 target 方式声明标准
target_compile_features(hello_cmake PRIVATE cxx_std_17)

构建:

bash 复制代码
cmake -S . -B build          # 配置阶段:指定源码目录为当前目录(.),构建目录为 build/;在 build/ 里生成构建系统文件(如 Makefile/Ninja)和 CMake 缓存
cmake --build build -j4      # 构建阶段:在 build/ 目录中调用底层构建工具进行编译,-j4 表示使用 4 个并行线程加速编译
./build/hello_cmake

四、基础语法(围绕 target 写配置)

CMake 最重要的习惯:不要把选项写成全局,尽量把配置绑定到 target。

4.1 变量与文件列表

cmake 复制代码
set(SOURCES main.cpp utils.cpp)
add_executable(app ${SOURCES})

4.2 编译选项(只作用于某个目标)

cmake 复制代码
target_compile_options(app PRIVATE -Wall -Wextra -Werror)

4.3 宏定义(-D)

cmake 复制代码
target_compile_definitions(app PRIVATE ENABLE_LOG=1)

4.4 头文件目录

cmake 复制代码
target_include_directories(app PRIVATE ${CMAKE_SOURCE_DIR}/include)

4.5 生成静态库/动态库并链接

cmake 复制代码
add_library(utils STATIC utils.cpp)   # libutils.a
# add_library(utils SHARED utils.cpp) # libutils.so

target_include_directories(utils PUBLIC ${CMAKE_SOURCE_DIR}/include)

add_executable(app main.cpp)
target_link_libraries(app PRIVATE utils)

4.6 PUBLIC / PRIVATE / INTERFACE(必须掌握)

关键字 对自己生效 对依赖自己的目标生效 常用场景
PRIVATE 仅当前 target 使用
PUBLIC 既自己用也要"传递"给下游
INTERFACE 自己不编译(头文件库/导入目标)

五、工程化:多目录模块化

典型结构:

复制代码
my_project/
├── CMakeLists.txt
├── include/
│   └── utils.h
├── src/
│   └── main.cpp
└── utils/
    ├── CMakeLists.txt
    ├── utils.cpp
    └── utils.h

utils/CMakeLists.txt

cmake 复制代码
add_library(utils STATIC utils.cpp)
target_include_directories(utils
    PUBLIC
        ${CMAKE_CURRENT_SOURCE_DIR}
        ${CMAKE_SOURCE_DIR}/include
)
target_compile_features(utils PUBLIC cxx_std_17)

根目录 CMakeLists.txt

cmake 复制代码
cmake_minimum_required(VERSION 3.10)
project(MyProject LANGUAGES CXX)

add_subdirectory(utils)

add_executable(my_app src/main.cpp)
target_link_libraries(my_app PRIVATE utils)

六、第三方依赖管理

6.1 优先:find_package(目标导入)

示例:Boost filesystem

cmake 复制代码
find_package(Boost REQUIRED COMPONENTS filesystem)

add_executable(app main.cpp)
target_link_libraries(app PRIVATE Boost::filesystem)

优先写 Boost::filesystem 这种目标,而不是手动用 include/lib 变量。


6.2 Linux 常见:PkgConfig(例如 glib、libcurl 等)

cmake 复制代码
find_package(PkgConfig REQUIRED)
pkg_check_modules(CURL REQUIRED libcurl)

add_executable(app main.cpp)
target_include_directories(app PRIVATE ${CURL_INCLUDE_DIRS})
target_link_directories(app PRIVATE ${CURL_LIBRARY_DIRS})
target_link_libraries(app PRIVATE ${CURL_LIBRARIES})

若模块支持导入目标(有的环境会提供 PkgConfig::xxx),优先用导入目标;否则用变量也可以。


6.3 不想手装:FetchContent 拉源码构建(fmt、gtest 等)

cmake 复制代码
include(FetchContent)

FetchContent_Declare(
  fmt
  GIT_REPOSITORY https://github.com/fmtlib/fmt.git
  GIT_TAG        10.2.1
)
FetchContent_MakeAvailable(fmt)

add_executable(app main.cpp)
target_link_libraries(app PRIVATE fmt::fmt)

FetchContent 适合"开发期依赖",生产环境也可配合锁版本、离线镜像。


七、构建类型(Debug/Release)与多配置生成器差异

7.1 单配置生成器(Make/Ninja)

CMAKE_BUILD_TYPE 生效:

bash 复制代码
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j

7.2 多配置生成器(VS/Xcode)

CMAKE_BUILD_TYPE 通常不管用,需要:

bash 复制代码
cmake --build build --config Release -j

八、CMake Presets:把"标准化构建命令"固化下来

在项目根目录新增 CMakePresets.json(示例):

json 复制代码
{
  "version": 3,
  "cmakeMinimumRequired": { "major": 3, "minor": 19, "patch": 0 },
  "configurePresets": [
    {
      "name": "debug",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/debug",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug",
        "CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
      }
    },
    {
      "name": "release",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/release",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Release"
      }
    }
  ],
  "buildPresets": [
    { "name": "debug", "configurePreset": "debug" },
    { "name": "release", "configurePreset": "release" }
  ]
}

使用:

bash 复制代码
cmake --preset debug
cmake --build --preset debug -j

好处:团队统一命令、统一 build 目录结构、统一开关(比如导出 compile_commands)。


九、测试:CTest +(可选)GoogleTest

9.1 最小 CTest

cmake 复制代码
enable_testing()

add_executable(test_math test/test_math.cpp)
add_test(NAME test_math COMMAND test_math)

运行:

bash 复制代码
ctest --test-dir build -j

9.2 FetchContent 引入 GoogleTest(常用模板)

cmake 复制代码
include(FetchContent)
FetchContent_Declare(
  googletest
  GIT_REPOSITORY https://github.com/google/googletest.git
  GIT_TAG        release-1.14.0
)
FetchContent_MakeAvailable(googletest)

add_executable(unit_tests test/test_main.cpp)
target_link_libraries(unit_tests PRIVATE GTest::gtest_main)

include(GoogleTest)
gtest_discover_tests(unit_tests)

十、安装与发布:install + export,让别人能 find_package(你的库)

如果你写的是"库项目",建议做到:

  1. make install/cmake --install 能安装
  2. 下游能 find_package(MyLib CONFIG REQUIRED) 直接用 MyLib::mylib

10.1 库项目示例(简化版)

cmake 复制代码
cmake_minimum_required(VERSION 3.15)
project(MyUtils LANGUAGES CXX)

add_library(my_utils STATIC src/utils.cpp)
target_include_directories(my_utils
  PUBLIC
    $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>
)
target_compile_features(my_utils PUBLIC cxx_std_17)

include(GNUInstallDirs)

install(TARGETS my_utils
  EXPORT MyUtilsTargets
  ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
  LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
  RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})

install(EXPORT MyUtilsTargets
  NAMESPACE MyUtils::
  DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyUtils
)

安装:

bash 复制代码
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
sudo cmake --install build --prefix /usr/local

进阶:再加 MyUtilsConfig.cmake / write_basic_package_version_file 可实现更完整的包配置。


十一、Linux 常见大坑:运行时找不到动态库(RPATH / LD_LIBRARY_PATH)

现象:编译链接成功,运行时报:

  • error while loading shared libraries: libxxx.so: cannot open shared object file

排查:

bash 复制代码
ldd ./build/app

解决思路(按推荐顺序):

  1. 安装到系统路径 (/usr/lib、/usr/local/lib)并执行 ldconfig(需要 root)
  2. 临时:export LD_LIBRARY_PATH=/path/to/lib:$LD_LIBRARY_PATH
  3. 工程化:设置 RPATH(让可执行文件记住运行时库路径)

常用 RPATH(示意):

cmake 复制代码
set(CMAKE_BUILD_RPATH "$ORIGIN")
set(CMAKE_INSTALL_RPATH "$ORIGIN")

$ORIGIN 表示可执行文件所在目录;实际项目可能需要 $ORIGIN/../lib 等。


十二、性能优化:Ninja / ccache / 并行构建

  • Ninja:增量更快,推荐默认使用;
  • ccache:重复编译显著加速;
cmake 复制代码
# CMake 3.4+ 可用
set(CMAKE_CXX_COMPILER_LAUNCHER ccache)
  • 并行
bash 复制代码
cmake --build build -j"$(nproc)"

十三、实操案例

案例1:单文件快速工具

cmake 复制代码
cmake_minimum_required(VERSION 3.10)
project(SimpleTool LANGUAGES CXX)

add_executable(simple_tool main.cpp)
target_compile_features(simple_tool PRIVATE cxx_std_17)
target_compile_options(simple_tool PRIVATE -Wall -Wextra -O0 -g)

案例2:多目录 + Boost

cmake 复制代码
cmake_minimum_required(VERSION 3.10)
project(FileTool LANGUAGES CXX)

find_package(Boost REQUIRED COMPONENTS filesystem)

add_subdirectory(src/utils)

add_executable(file_tool src/main.cpp)
target_link_libraries(file_tool PRIVATE utils Boost::filesystem)
target_include_directories(file_tool PRIVATE ${CMAKE_SOURCE_DIR}/include)

案例3:库 + 安装导出(见第十章模板)

适合"公共工具库/SDK"。


十四、标准化构建流程

推荐目录:

复制代码
project/
├── CMakeLists.txt
├── CMakePresets.json      # 推荐
├── build/                 # .gitignore
├── include/
├── src/
├── test/                  # 可选
├── scripts/               # 可选
├── toolchain/             # 可选
└── libs/                  # 可选(预编译三方库)

推荐命令(统一 -S/-B):

bash 复制代码
# 配置
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug

# 构建
cmake --build build -j"$(nproc)"

# 测试
ctest --test-dir build -j

# 安装(如果有 install)
sudo cmake --install build --prefix /usr/local

十五、CMake 常用命令速查表

15.1 配置(Configure)

目的 命令
基本配置 cmake -S . -B build
指定生成器 cmake -S . -B build -G Ninja
指定构建类型 cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
导出 compile_commands cmake -S . -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
查看缓存变量 cmake -S . -B build -LAH

15.2 构建(Build)

目的 命令
编译 cmake --build build -j4
指定目标 cmake --build build --target my_target -j
列出可构建目标 cmake --build build --target help
多配置生成器指定配置 cmake --build build --config Release -j

15.3 清理(Clean)

目的 命令
清理目标 cmake --build build --target clean
彻底清缓存(最稳) rm -rf build

15.4 安装(Install)

目的 命令
安装到默认前缀 sudo cmake --install build
指定安装前缀 sudo cmake --install build --prefix /usr/local

15.5 测试(CTest)

目的 命令
运行测试 ctest --test-dir build
并行测试 ctest --test-dir build -j
失败时输出详细 ctest --test-dir build --output-on-failure

15.6 Presets

目的 命令
配置 preset cmake --preset debug
构建 preset cmake --build --preset debug -j

十六、避坑指南

16.1 常见错误与解决方案

现象 原因 解决
xxx.h: No such file 未加 include 目录 target_include_directories
undefined reference 未链接库/顺序问题 target_link_libraries,优先导入目标
找不到编译器 未装 g++/clang 安装编译器
Could NOT find Boost 缺组件 安装对应 dev 包
改了变量不生效 build cache 仍旧 重新 cmake -S/-B 或删 build

16.2 Debug 体验差(被误开优化)

Debug 推荐:

  • -O0 -g
  • Release 推荐:
  • -O3 -DNDEBUG

建议:不要自己手写 -O3,尽量通过 CMAKE_BUILD_TYPE 管理。


十七、总结

CMake核心价值在于:

  1. 简化构建配置:用简洁的语法替代晦涩的Makefile,降低维护成本;

  2. 标准化工程管理:支持模块化、依赖管理、多编译模式,适配大型项目;

  3. 跨平台兼容:一次配置可在Linux/macOS/Windows生成适配的构建文件;

  4. 高效可扩展:支持自定义规则、脚本集成,满足个性化构建需求。

相关推荐
寻寻觅觅☆10 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
时代的凡人10 小时前
0208晨间笔记
笔记
fpcc10 小时前
并行编程实战——CUDA编程的Parallel Task类型
c++·cuda
小白同学_C10 小时前
Lab4-Lab: traps && MIT6.1810操作系统工程【持续更新】 _
linux·c/c++·操作系统os
今天只学一颗糖10 小时前
1、《深入理解计算机系统》--计算机系统介绍
linux·笔记·学习·系统架构
testpassportcn11 小时前
AWS DOP-C02 認證完整解析|AWS DevOps Engineer Professional 考試
网络·学习·改行学it
ceclar12311 小时前
C++使用format
开发语言·c++·算法
lanhuazui1012 小时前
C++ 中什么时候用::(作用域解析运算符)
c++
charlee4412 小时前
从零实现一个生产级 RAG 语义搜索系统:C++ + ONNX + FAISS 实战
c++·faiss·onnx·rag·语义搜索
不做无法实现的梦~12 小时前
ros2实现路径规划---nav2部分
linux·stm32·嵌入式硬件·机器人·自动驾驶