构建与依赖
这一课你将能做到
- 写出最小
CMakeLists.txt,用 out-of-source 方式配置并编译多文件可执行项目
- 用 CMake 声明 C++ 标准版本、链接第三方库(以 Threads 为例),理解 target 的概念
- 用 vcpkg 安装第三方依赖(fmt、nlohmann-json)并接入 CMake,对应出你熟悉的 Maven/Gradle 工作流
为什么要学
第一课你还在用 g++ hello.cpp -o hello 编译单文件,可判题沙箱有几十个源文件、依赖 JSON 解析、进程隔离等三方库,命令行手写根本不现实。C++ 世界的答案是 CMake:它不编译代码,而是生成各平台构建系统(Makefile/Visual Studio/Ninja 工程)的「构建的构建」——地位相当于 Maven,但管的是编译链接而不是依赖仓库本身。
依赖管理用 vcpkg(微软出品,C++ 生态事实标准之一):一条 vcpkg install fmt 下载编译安装,CMake 三行接入。对比 Java:Maven = CMake(构建)+ vcpkg(依赖)两个角色的合体。本课结束,你就能从单文件玩具升级为可维护的工程结构——这也是最后综合练习课的地基。
先建立图像
构建工具栈全景:
你的代码 (src/*.cpp, include/*.hpp)
↓ CMakeLists.txt(声明:有哪些源文件、什么标准、依赖谁)
CMake(配置 generate)
↓ 生成
构建系统(Makefile / Ninja / VS 工程)
↓ 编译构建(build)
可执行文件 / 库
Java 对照:
pom.xml / build.gradle ──mvn package──> target/*.jar
CMakeLists.txt ──cmake --build──> build/judge
依赖对照:
Maven 中央仓库 + pom 声明 ↔ vcpkg install + CMakeLists find_package
跟着做
步骤 1:先感受「不用 CMake」的痛
把第一课的 hello 拆成三个文件:
judge/
├── main.cpp
├── verdict.cpp
└── verdict.hpp
// verdict.hpp
#pragma once // 防止头文件被重复包含(现代写法,替代 #ifndef 守卫)
#include <string>
std::string verdictName(int code); // 声明:0=CE 1=TLE 2=WA 3=AC
// verdict.cpp
#include "verdict.hpp"
std::string verdictName(int code) {
switch (code) {
case 0: return "CE";
case 1: return "TLE";
case 2: return "WA";
default: return "AC";
}
}
// main.cpp
#include <iostream>
#include "verdict.hpp"
int main() {
for (int code : {0, 1, 2, 3}) {
std::cout << code << " -> " << verdictName(code) << "\n";
}
return 0;
}
手动编译:
g++ -std=c++17 main.cpp verdict.cpp -o judge # 文件少还行……
问题是:改一个文件要全量重编;几十个文件时命令行失控;换平台命令全变;依赖第三方库还要手写 -I、-L、-l。CMake 就是为消灭这些而生。
步骤 2:写最小 CMakeLists.txt
在 judge/ 下新建 CMakeLists.txt:
cmake_minimum_required(VERSION 3.16) # 声明要求的最低 CMake 版本
project(judge CXX) # 工程名 + 语言(CXX = C++)
set(CMAKE_CXX_STANDARD 17) # 全局 C++ 标准:对应 g++ 的 -std=c++17
set(CMAKE_CXX_STANDARD_REQUIRED ON) # 编译器不支持就报错,不静默降级
add_executable(judge # 目标:一个叫 judge 的可执行文件
main.cpp
verdict.cpp
)
要点:add_executable 的参数叫 target,CMake 3 的一切配置(标准、依赖、编译选项)都挂在 target 上——思想等同于 Maven 的一个 module。
步骤 3:out-of-source 构建
cd judge
cmake -B build # 配置:在 build/ 目录里生成构建系统(不污染源码目录)
cmake --build build # 构建:实际编译,Windows 自动调 MSBuild/Ninja
成功后可执行文件在 build/ 里(Windows 是 build\Debug\judge.exe,Linux 是 build/judge),运行:
0 -> CE
1 -> TLE
2 -> WA
3 -> AC
out-of-source 是铁律:源码目录保持干净,build/ 整个删掉重来毫无心理负担。改了代码只需重跑 cmake --build build——CMake 自动增量编译(只编改过的 .cpp,和 Maven 增量同理)。改了 CMakeLists.txt 则重跑 cmake -B build。
步骤 4:链接系统库——以多线程为例
add_executable(judge
main.cpp
verdict.cpp
concurrency.cpp
)
find_package(Threads REQUIRED) # 查找系统的线程库
target_link_libraries(judge PRIVATE Threads::Threads) # 给 judge 这个 target 挂上依赖
这就是「显式声明依赖」:可执行 judge 依赖 Threads。对比 Maven 的 <dependency>——声明的是 target 间/库间的依赖关系,构建系统自己解决顺序与链接参数。
编译模式切换(对应 Maven 的 profile):
cmake -B build -DCMAKE_BUILD_TYPE=Release # Release:-O3 优化,判题跑分用它
cmake -B build -DCMAKE_BUILD_TYPE=Debug # Debug:-g 调试信息,gdb 用它(下一课)
cmake --build build
步骤 5:vcpkg 安装第三方库
以最常用的两个库为例:fmt(现代格式化,未来将进标准库)、nlohmann-json(JSON 解析,判题沙箱和后端交换数据必备)。
git clone https://github.com/microsoft/vcpkg.git
cd vcpkg && ./bootstrap-vcpkg.bat # Windows;Linux/macOS 是 bootstrap-vcpkg.sh
vcpkg install fmt nlohmann-json # 下载源码 → 本机编译 → 注册到 vcpkg 仓库
CMake 接入(配置时传一个参数即可,无需改源码):
cmake -B build -DCMAKE_TOOLCHAIN_FILE=<vcpkg根目录>/scripts/buildsystems/vcpkg.cmake
find_package(fmt CONFIG REQUIRED)
find_package(nlohmann_json CONFIG REQUIRED)
target_link_libraries(judge PRIVATE
fmt::fmt
nlohmann_json::nlohmann_json
)
#include <fmt/core.h>
#include <nlohmann/json.hpp>
#include <string>
std::string makeResultJson(int id, const std::string& verdict, int costMs) {
nlohmann::json j{
{"submissionId", id},
{"verdict", verdict},
{"costMs", costMs},
};
return j.dump(); // 序列化为 JSON 字符串
// 格式化输出:fmt::print("提交#{} 状态 {} 耗时 {}ms\n", id, verdict, costMs);
}
注意流程与 Maven 的差异:Maven 在 mvn package 时按 pom 拉取预编译 jar;vcpkg 是源码编译安装到本机,CMake 构建时通过 toolchain 文件找到它们。库的版本管理写在 vcpkg.json 清单文件里(放在工程根目录,vcpkg install 自动按清单安装),相当于 pom 的 <dependencies> 版本锁定。
讲清楚要点
- CMake 生成构建系统,不直接编译:
cmake -B build(配置)+ cmake --build build(编译)两阶段。跨平台能力来自「生成」:Windows 生成 VS/Ninja 工程,Linux 生成 Makefile,你写一份 CMakeLists 全平台通吃。
- 一切围绕 target:
add_executable / add_library 声明目标,target_link_libraries 声明依赖,target_include_directories 声明头文件路径。现代 CMake 惯例全部用带 target_ 前缀的命令,不再用全局 include_directories。
- PUBLIC / PRIVATE / INTERFACE 三种传播:PRIVATE 依赖只自己用;PUBLIC 依赖随头文件暴露(使用者也需要);INTERFACE 纯头文件库用 INTERFACE。类比 Maven 的 compile/provided scope,想清楚再写,避免依赖泄漏。
- find_package 是「找到已安装的库」:它不下载!库必须先由 vcpkg/系统包管理器装好。vcpkg 的 toolchain 文件让 find_package 自动找到 vcpkg 安装的库,二者是「仓库 + 桥接」关系。
- 构建目录即产物:
build/ 加进 .gitignore;CI 上每次全新 cmake -B build 保证可复现。判题沙箱编译用户代码同样是「临时目录构建 → 提取产物 → 即毁」,只是直接调编译器。
你可能会懵的地方
-
CMake 和 Make 是什么关系?
Make 读 Makefile 执行编译(增量、依赖拓扑);CMake 读 CMakeLists.txt 生成 Makefile/Ninja 文件。C++ 工程写 Makefile 太痛苦(跨平台、依赖检测),所以用 CMake 这一「元构建」工具。Ninja 是更快的现代替代品,CMake 两者都能生成。
-
为什么不用 Maven 直接管 C++?
依赖形态不同:Java 依赖是平台无关的字节码 jar;C++ 依赖要匹配编译器、标准库实现(libstdc++/libc++/MSVC STL)与构建模式,预编译包兼容性差,主流方案都选源码编译(vcpkg、Conan)。
-
#pragma once 和 include 守卫哪个好?
#pragma once 简洁且所有主流编译器支持,但非标准;#ifndef X_H / #define X_H / #endif 是标准写法但要保证宏名唯一。工程二选一全库统一即可,本轨用 #pragma once。
-
为什么我的 CMake 找不到 vcpkg 装的库?
配置时漏了 -DCMAKE_TOOLCHAIN_FILE 参数。注意它必须在 cmake -B build 时传,写在 CMakeLists 里传无效(时序问题)。也有环境变量 VCPKG_ROOT 的写法,团队统一其一。
-
静态库 .a/.lib 和动态库 .so/.dll 怎么选?
静态库编进可执行文件:单文件部署、体积大、改库要重编;动态库运行期加载:多程序共享、可热更新,但要解决部署时「DLL 地狱」。判题沙箱通常静态链接用户不关心的依赖,只暴露必要接口。
错法对照
| 错法 | 问题 | 正解 |
|---|
在源码目录直接跑 cmake . | 生成物散落源码目录,无法清理 | cmake -B build out-of-source |
| 漏写一个 .cpp 进 add_executable | 链接错误 undefined reference | 每新增源文件同步登记(或用 file GLOB 但慎用) |
| C++ 标准只写在编译选项里不设 CMAKE_CXX_STANDARD_REQUIRED | 编译器静默降级到旧标准 | set(CMAKE_CXX_STANDARD_REQUIRED ON) |
在 CMakeLists 里写死 /usr/local/include 路径 | 换机器就崩 | 用 find_package + target_link_libraries |
| build/ 目录提交进 git | 仓库爆炸,冲突不断 | .gitignore 掉 build/ |
卡点急救
CMake Error: Could not find fmt:vcpkg 没装这个库,或配置时没传 toolchain 文件。先 vcpkg install fmt,再带参数重新 cmake -B build(可先删 build/ 干净重来)。
- 改了 CMakeLists 但构建没生效:CMake 有缓存,重新跑
cmake -B build 刷新;顽固问题删 build/ 全新配置。
- Windows 下编译出来是
judge.exe 却双击闪退:工作目录不对导致找不到依赖 DLL,或控制台窗口随程序结束关闭。在终端里运行,并把 vcpkg 的 installed/x64-windows/bin 加入 PATH(动态库场景)。
面试常问
-
CMake 的作用是什么?为什么不直接写 Makefile?
CMake 是跨平台元构建系统:读 CMakeLists.txt 生成对应平台的构建工程(Makefile/Ninja/VS),并内置编译器探测、库查找、依赖传播。直接写 Makefile 要为每个平台手写规则与路径,第三方库的发现与链接更难维护,跨平台成本极高。
-
vcpkg 的工作原理,和 Maven 有何区别?
vcpkg 把库源码编译成匹配本机编译器/模式的二进制并注册,CMake 经 toolchain 文件在 find_package 时接入;版本由 vcpkg.json 清单或 registry 锁定。Maven 直接分发平台无关的预编译 jar,一次构建处处可用;C++ 依赖绑定编译器与平台,必须本地构建,这是两个生态最大的差异。
-
静态链接与动态链接的取舍?
静态:部署简单(单文件)、启动快、无版本冲突,体积大、库升级需重编整个程序;动态:多进程共享内存、库可独立升级,但有部署依赖与符号版本问题。服务端常静态链接以简化部署,基础系统库走动态。
自测三问
cmake -B build 和 cmake --build build 分别做什么?
- target_link_libraries 里 PRIVATE 和 PUBLIC 的区别是什么?
- vcpkg 安装的库,CMake 靠什么机制找到它们?
小结一句话
CMake 声明 target 与依赖、生成平台构建系统,vcpkg 源码编译并供给第三方库——两个工具合体就是 C++ 版的 Maven:一份 CMakeLists.txt + vcpkg.json,全平台可复现构建。
延伸