CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
C++ 语言入门/课时/综合练习
RawBlame
课时目录12 节

C++ 语言入门

  1. 01C++ 是什么
  2. 02基础语法
  3. 03指针与引用
  4. 04面向对象
  5. 05内存与 RAII
  6. 06STL 容器与算法
  7. 07模板基础
  8. 08现代 C++ 特性
  9. 09多线程
  10. 10构建与依赖
  11. 11调试与测试
  12. 12综合练习

综合练习

小型 CLI 工具 + 单元测试 + CMake 构建。

40 分钟

综合练习

这一课你将能做到

  1. 用类 + std::vector + std::variant + CMake 搭一个可运行的迷你判题器 CLI(submit / find)
  2. 把 RAII 计时、optional 查询、variant 分派、移动语义串成一条完整链路
  3. 为核心逻辑写 doctest 单测,并用 ASan 验证一次内存安全,完成工程闭环

为什么要学

前十一课的技能点是散落的,拼成真实项目才算内化。本课用最熟悉的在线判题场景串起它们:类建模、vector、variant、optional、RAII、CMake、单测加 ASan。写完这约 150 行,你对「C++ 工程长什么样」就有了肌肉记忆,再读真实判题沙箱等开源代码就有了坐标系。

先建立图像

mini-judge/
├── CMakeLists.txt        ← 构建声明(对应 pom.xml)
├── include/judge.hpp     ← 公共接口
├── src/judge.cpp         ← 判题核心
├── src/main.cpp          ← CLI 入口(main 唯一)
└── tests/test_judge.cpp  ← 单测(对应 JUnit)

跟着做

步骤 1:定义数据模型

新建 include/judge.hpp:

#pragma once

#include <optional>
#include <string>
#include <variant>
#include <vector>

namespace mj {

struct Submission {
    int id;
    std::string user;
    std::string code;
};

struct AC { int costMs; };
struct WA { int caseIdx; };
struct TLE { int limitMs; };
using Verdict = std::variant<AC, WA, TLE>;

class Judge {
public:
    explicit Judge(int timeLimitMs) : timeLimitMs_(timeLimitMs) {}
    Verdict judge(const std::string& code) const;
    std::optional<Verdict> resultOf(int submissionId) const;
    void submit(Submission s);
    const std::vector<Submission>& submissions() const { return subs_; }
private:
    int timeLimitMs_;
    std::vector<Submission> subs_;
    std::vector<std::pair<int, Verdict>> results_;
};

}  // namespace mj

步骤 2:实现判题核心

新建 src/judge.cpp:

#include "judge.hpp"

#include <algorithm>
#include <chrono>

namespace mj {

namespace {

// RAII 计时器:析构时写入耗时,异常路径也不漏
class ScopedTimer {
public:
    explicit ScopedTimer(int& outMs)
        : start_(std::chrono::steady_clock::now()), out_(outMs) {}
    ~ScopedTimer() {
        out_ = static_cast<int>(
            std::chrono::duration<double, std::milli>(
                std::chrono::steady_clock::now() - start_).count());
    }
private:
    std::chrono::steady_clock::time_point start_;
    int& out_;
};

// 模拟运行用户代码;真实沙箱在此 fork 子进程并限制资源
int runOneCase(const std::string& code, int caseIdx) {
    int h = 0;
    for (char c : code) h = h * 31 + c + caseIdx;
    return h % 200 + 1;
}

}  // namespace

Verdict Judge::judge(const std::string& code) const {
    for (int i = 0; i < 5; ++i) {
        int cost = 0;
        {
            ScopedTimer t(cost);
            cost += runOneCase(code, i);
        }
        if (cost > timeLimitMs_) return TLE{timeLimitMs_};
        if (i == 2 && code.find("wrong") != std::string::npos)
            return WA{i};
    }
    return AC{runOneCase(code, 0)};
}

void Judge::submit(Submission s) {
    int id = s.id;
    subs_.push_back(std::move(s));               // 移动避免深拷贝
    results_.emplace_back(id, judge(subs_.back().code));
}

std::optional<Verdict> Judge::resultOf(int submissionId) const {
    auto it = std::find_if(results_.begin(), results_.end(),
        [submissionId](const auto& rv) { return rv.first == submissionId; });
    if (it == results_.end()) return std::nullopt;
    return it->second;
}

}  // namespace mj

步骤 3:CLI 入口

新建 src/main.cpp:

#include "judge.hpp"

#include <iostream>
#include <string>
#include <variant>

void printVerdict(const mj::Verdict& v) {
    std::visit([](const auto& r) {
        using T = std::decay_t<decltype(r)>;
        if constexpr (std::is_same_v<T, mj::AC>)
            std::cout << "AC (" << r.costMs << "ms)";
        else if constexpr (std::is_same_v<T, mj::WA>)
            std::cout << "WA (第" << r.caseIdx + 1 << "个测试点)";
        else
            std::cout << "TLE (限" << r.limitMs << "ms)";
    }, v);
    std::cout << "\n";
}

int main() {
    mj::Judge judge{100};
    std::string cmd;
    while (std::cout << "> " && std::cin >> cmd) {
        if (cmd == "quit") break;
        if (cmd == "submit") {
            mj::Submission s{};
            std::cin >> s.id >> s.user >> s.code;
            judge.submit(std::move(s));
            std::cout << "评测完成: ";
            printVerdict(*judge.resultOf(s.id));
        } else if (cmd == "find") {
            int id;
            std::cin >> id;
            auto r = judge.resultOf(id);
            if (r) printVerdict(*r);
            else std::cout << "没有这个提交\n";
        }
    }
    return 0;
}

步骤 4:CMake 组装

新建 CMakeLists.txt:

cmake_minimum_required(VERSION 3.16)
project(mini_judge CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_library(judge_core src/judge.cpp)
target_include_directories(judge_core PUBLIC include) # PUBLIC 传递 include 路径

add_executable(mini_judge src/main.cpp)
target_link_libraries(mini_judge PRIVATE judge_core)

cmake -B build && cmake --build build 后运行 ./build/mini_judge:submit 1 zhang fast 得 AC (17ms),submit 2 li wrong 得 WA (第3个测试点)。

步骤 5:单测锁定核心

新建 tests/test_judge.cpp:

#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include "doctest.h"
#include "judge.hpp"

TEST_CASE("超时代码判 TLE") {
    mj::Judge j{10};
    j.submit({1, "li", "slow slow slow slow slow"});
    CHECK(std::holds_alternative<mj::TLE>(*j.resultOf(1)));
}

TEST_CASE("含 wrong 的代码在测试点 3 判 WA") {
    mj::Judge j{1000};
    j.submit({2, "wang", "wrong answer"});
    auto* wa = std::get_if<mj::WA>(&*j.resultOf(2));   // get_if:不抛异常的取法
    REQUIRE(wa != nullptr);
    CHECK(wa->caseIdx == 2);
}

CMake 追加测试 target,重新构建后在 build 目录跑 ctest,全绿即通过:

add_executable(test_judge tests/test_judge.cpp)
target_link_libraries(test_judge PRIVATE judge_core)
enable_testing()
add_test(NAME judge_tests COMMAND test_judge)

再用 ASan 把整个工程过一遍(第 11 课):

cmake -B build-asan -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS="-fsanitize=address" -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address"
cmake --build build-asan && ./build-asan/test_judge

无输出即无内存错误——代码没有裸 new,本就该干净。进阶可自选:批量 submit 加线程与互斥锁(第 9 课)、vcpkg 装 nlohmann-json 落盘(第 10 课)、对比 Debug/Release 耗时(第 11 课)。

讲清楚要点

  1. 库与可执行分离是工程化第一步:核心逻辑进 judge_core 库,被 CLI、测试、未来接口复用,对应 Java 的 core/app 分层。
  2. variant + visit 替代结果码:新增一种结果只需加一个 struct 和一个分支,编译器强制补全所有分发点。
  3. RAII 贯穿始终:ScopedTimer 管计时窗口,vector 管存储,string 管缓冲——零裸 new、零手动清理。
  4. 接口用 optional/const& 表达契约:返回 optional 说明「可能没有」,返回 const& 说明「只读视图」。
  5. 提交前三连:ctest 全绿 + ASan 无输出 + Release 冒烟——判题沙箱的 CI 就是这三步的放大版。

你可能会懵的地方

  • 为什么 main.cpp 和 judge.cpp 要分开? main 只做 IO 与分发,业务逻辑全在库层,单测直接链接 judge_core——Java 的 Controller/Service 分层同理。

  • results_ 为什么存 pair 而不是 map? 提交 ID 可能重复也可能不连续,map 会去重;vector 保序留全历史,规模大再换 unordered_map<int, Verdict>。

  • judge 是 const 成员函数,为什么能返回 TLE{...}? const 只承诺不改成员,构造返回值、读成员都合法;它把「judge 只读、submit 才写」画在接口上。

  • 想做成「真」判题器还差什么? 把 runOneCase 换成真进程执行:fork、管道重定向、限时限存、超时 kill、信号分类 RE;工程结构不变,系统调用层更深。

错法对照

错法问题正解
所有代码塞进 main.cpp无法单测无法复用拆库 + 入口,CMake 分 target
结果用 int 码 + 注释魔法数字散落,漏分支无警告variant + visit,编译器穷尽检查
submit 参数用值传且不 move字符串深拷贝传值后 std::move 入容器

卡点急救

  1. 链接错误 undefined reference to mj::Judge::judge:judge.cpp 没进 add_library,或 main 没挂 judge_core。
  2. 单测找到两个 main:忘了 #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN。
  3. CLI 输入错乱:>> 读数字后残留换行被 getline 吞掉,>> 后接 cin.ignore() 清缓冲。

面试常问

  • 介绍一个你用 C++ 写过的完整项目,如何组织的? 按分层讲:数据模型(struct/variant)→ 核心逻辑(类 + RAII)→ 入口适配 → 构建(CMake 分 target)→ 质量(doctest + ASan);强调所有权清晰、接口语义显式、构建可复现。

  • C++ 项目里如何保证内存安全? 编译期禁裸 new/delete,用智能指针表达所有权;测试期单测叠加 ASan 与 TSan 跑 CI;运行期日志 + core dump 兜底。原则:能移动不复制、能栈不堆。

  • 你的项目里为什么用 variant 而不是继承多态? 结果类型集合封闭、对象小且值语义友好,variant 无堆分配无虚表、visit 可穷尽检查;要做开放扩展(第三方注册新类型)才切继承 + 虚函数。

自测三问

  1. 为什么把 judge 做成库 target 而不是把逻辑全写进 main.cpp?
  2. std::get_if<mj::WA>(&v) 相比 std::get<mj::WA>(v) 的优势是什么?
  3. 项目里哪些对象的生命周期由 RAII 管理?各举一例。

小结一句话

类 + vector + variant + RAII 计时 + optional + CMake 分层 + 单测/ASan——150 行迷你判题器串起十二课,骨架直接对应真实判题沙箱的工程结构。

延伸

  • cppreference:C++ 教程索引
  • cplusplus.com:C++ 完整教程目录
  • 下一课预告:本轨完结。进阶可深入 C++20 Concepts/协程与开源判题沙箱源码。
加载本节练习…
← 上一课调试与测试