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

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

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

学习

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

更多

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

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
C++ 语言入门/课时/现代 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综合练习

现代 C++ 特性

c++11/14/17、lambdas、move semantics、ranges 预览。

30 分钟

现代 C++ 特性

这一课你将能做到

  1. 用 auto、范围 for、结构化绑定写出更短的循环与遍历代码,并说出 auto 的推导边界
  2. 用 std::optional 表达「可能没有结果」,替代「返回 -1 / 空指针」的隐式约定
  3. 用 std::variant + std::visit 表达「多种可能结果之一」,理解它和 Java Optional/switch 表达式的对应关系

为什么要学

C++11/17/20 三波更新让 C++ 的书写体验追平甚至部分超越 Java:lambda、auto、范围 for 这些你在 Java 里熟悉的(var、增强 for、lambda)全有了 C++ 版;std::optional、std::variant 更是解决 Java 社区还在用库模拟的问题。

判题沙箱的现代代码里这些特性随处可见:解析提交记录用 optional 表达「这行可能解析失败」,评测结果用 variant 表达「AC / WA / TLE 三选一」,遍历统计用范围 for + 结构化绑定。本课之后,你写的 C++ 就不再是「带类的 C」,而是现代 C++。

先建立图像

同一件事的旧写法 vs 新写法:

旧:for (std::vector<Submission>::iterator it = v.begin(); it != v.end(); ++it)
新:for (const auto& s : v)                    ← 范围 for + auto

旧:int idx = find(subs, "li");  if (idx == -1) ...   ← -1 既是数据又是信号,埋雷
新:auto r = find(subs, "li");   if (r.has_value()) ... ← optional 把「没有」说在类型里

旧:结果码 int verdict,0/1/2/3 全靠注释记
新:std::variant<AC, WA, TLE> result           ← 类型系统替你记住所有可能

跟着做

步骤 1:auto——让编译器替你写类型

新建 modern.cpp:

#include <iostream>
#include <map>
#include <string>
#include <vector>

int main() {
    auto n = 42;                  // int
    auto d = 3.14;                // double
    auto s = std::string("AC");   // std::string(auto s = "AC" 推导出 const char*,要小心!)

    std::vector<int> scores{60, 87, 95};
    auto it = scores.begin();     // std::vector<int>::iterator——没有 auto 就得写全这一长串
    std::cout << *it << "\n";

    // 整数默认推导成 int;要 64 位就写 auto n = 42LL
    return 0;
}

auto 对应 Java 10 的 var,但 C++ 的 auto 能用在更多位置(返回类型、lambda 参数、模板),且没有装箱陷阱——它是纯编译期推导,运行期零差异。

步骤 2:范围 for + 结构化绑定

#include <iostream>
#include <map>
#include <string>
#include <vector>

int main() {
    std::vector<std::string> users{"zhang", "li", "wang"};
    for (const auto& u : users) {          // 值拷贝用 auto u;只读用 const auto&(首选)
        std::cout << u << "\n";
    }

    std::map<std::string, int> costs{{"zhang", 120}, {"li", 45}};
    // C++17 结构化绑定:把 pair 拆成两个命名变量
    for (const auto& [user, cost] : costs) {
        std::cout << user << " 耗时 " << cost << "ms\n";
    }
    return 0;
}

结构化绑定对 std::pair、std::tuple、struct(按成员顺序)都能拆,Java 的 record 解构(Java 21 模式匹配)对应的就是这个。

步骤 3:lambda——匿名函数

#include <algorithm>
#include <iostream>
#include <string>
#include <vector>

int main() {
    std::vector<int> costs{120, 45, 200, 88};

    int limit = 100;                                  // 捕获外部变量
    auto count = std::count_if(costs.begin(), costs.end(),
        [limit](int c) { return c > limit; });        // [limit]:按值拷贝
    std::cout << "超时个数 " << count << "\n";

    int total = 0;
    std::for_each(costs.begin(), costs.end(),
                  [&total](int c) { total += c; });   // [&total]:按引用,能修改外部
    std::cout << "总耗时 " << total << "\n";

    // lambda 本质是编译器生成的匿名类对象(重载了 operator()),对应 Java 的函数式接口实例
    auto square = [](int x) { return x * x; };
    std::cout << square(9) << "\n";
    return 0;
}

与 Java lambda 的差异:Java 捕获的局部变量必须 effectively final;C++ 允许按引用捕获并修改外部变量,但要自己承担悬垂引用的风险(见第 9 课)。

步骤 4:std::optional——「可能没有」

判题里经典需求:按 ID 查提交,可能查不到。

#include <iostream>
#include <optional>
#include <string>
#include <vector>

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

// 返回 optional:类型层面声明「可能没有结果」
std::optional<Submission> findById(const std::vector<Submission>& subs, int id) {
    for (const auto& s : subs) {
        if (s.id == id) return s;         // 隐式构造 optional<Submission>
    }
    return std::nullopt;                  // 「没有」的标准答案,对应 Java 的 Optional.empty()
}

int main() {
    std::vector<Submission> subs{{101, "zhang", 120}, {102, "li", 45}};

    auto r = findById(subs, 102);
    if (r.has_value()) {                              // 也可写 if (r)
        std::cout << "找到: " << r->user << " " << r->cost << "ms\n";
    }

    auto miss = findById(subs, 999);
    std::cout << miss.value_or(Submission{0, "未知", 0}).user << "\n";   // 兜底值
    return 0;
}

对比 Java:Optional<Submission> + isPresent()/orElse() 几乎逐字对应。关键区别:Java 的 optional.get() 不检查就取会抛异常,C++ 的 *r/r-> 不检查就是 UB——取值前必判空的纪律两边都一样。旧写法(返回 -1、nullptr、全局错误标志)的问题都是「信号混进数据」,optional 把语义写进函数签名。

步骤 5:std::variant——「多选一」的类型安全

真实判题还有 TLE、RE、CE。variant 让「一组可能」成为类型:

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

struct AC { int cost; };
struct WA { int caseIdx; };
struct TLE { int limitMs; };
using Verdict = std::variant<AC, WA, TLE>;    // 三选一

void report(const Verdict& v) {
    // std::visit:像 switch 一样按「当前装的是什么类型」分派,全部分支必须覆盖
    std::visit([](const auto& result) {
        using T = std::decay_t<decltype(result)>;     // 取出实际类型的技巧
        if constexpr (std::is_same_v<T, AC>) {        // C++17 if constexpr:编译期 if
            std::cout << "AC,耗时 " << result.cost << "ms\n";
        } else if constexpr (std::is_same_v<T, WA>) {
            std::cout << "WA,第 " << result.caseIdx + 1 << " 个测试点\n";
        } else {
            std::cout << "TLE,限制 " << result.limitMs << "ms\n";
        }
    }, v);
}

int main() {
    std::vector<Verdict> results{AC{128}, WA{2}, TLE{1000}};
    for (const auto& v : results) report(v);

    Verdict v = WA{1};
    std::cout << std::get<AC>(v).cost;    // 危险写法:装的是 WA 却按 AC 取 → 抛异常
    return 0;
}

对照 Java:等价于 sealed interface + switch 模式匹配(Java 21)。variant 保证你不可能拿到第四种状态,也没有「注释与现实脱节」的问题。

讲清楚要点

  1. auto 是编译期推导,不是动态类型:类型在编译那一刻就定死,运行期与手写类型完全一致。它消灭的是冗长类型名,不是类型系统。
  2. 范围 for 的底层就是 begin()/end() 迭代器:第 6 课的「迭代器失效」在范围 for 里同样存在——遍历时往 vector 里 push_back 是 UB。
  3. optional 是值语义的「或无」:内存 = T 的大小 + 一个标志位。*opt、opt->、opt.value() 都要求确有值,安全取值用 value_or(默认值) 或先 has_value()。
  4. variant 是类型安全的 union:同一时刻持有其中一种类型;std::get<T> 类型不匹配会抛异常,安全取值用 get_if 或 visit。
  5. lambda 捕获即闭包成员:[limit] 复制成闭包成员,[&] 存引用;按值捕获的副本默认不可改,要改写 mutable。

你可能会懵的地方

  • auto x = {1, 2} 会推导成什么? std::initializer_list<int>,常见的意外。列表初始化场景别用 auto,直接写明类型。

  • 结构化绑定能修改值吗? auto& [k, v] : m 绑定引用就能改(map 的 value 可改,key 不行);auto [k, v] 是拷贝,改了也不影响原容器。

  • variant 什么时候比继承多态合适? 类型集合固定且简单(结果状态、消息类型、配置项二选一)、无需运行期扩展新类型时,variant 无虚表开销、分支可穷尽检查。需要「第三方随时注册新子类」的开放扩展才用继承。

  • if constexpr 和普通 if 的区别? 普通 if 两个分支都要编译通过;if constexpr 的假分支在模板实例化时直接丢弃,所以 result.caseIdx 只在 WA 分支出现也能编译。

错法对照

错法问题正解
auto s = "hello";推导为 const char*,不是 stringauto s = std::string("hello"); 或显式类型
不判空就 *opt空 optional 解引用 → UBif (opt) 或 value_or 兜底
范围 for 中 v.push_back(...)可能触发扩容,迭代器失效 UB先收集要加的,遍历完再插
[&] 捕获后 lambda 被异步使用原作用域已结束 → 悬垂引用按值捕获 [=] 或明确列出捕获项
std::get<AC>(v) 不 try类型不符抛异常std::get_if<AC>(&v) 返回指针判空,或用 visit

卡点急救

  1. 范围 for 报错「no match for operator++」:遍历的对象不是容器(比如 C 数组传参退化成指针后没有 begin/end)。改用指针 + 长度,或换 std::array/vector。
  2. visit 编译报错「未处理所有备选类型」:visit 的调用对象必须能接受 variant 的每一种类型。检查 lambda 是否漏了某个分支。
  3. 结构化绑定报「cannot be decomposed」:类型不是 pair/tuple/聚合 struct。先确认成员,或手动 kv.first/kv.second。

面试常问

  • std::optional 解决什么问题?和指针判空的区别? optional 用值语义显式表达「可能有值可能没有」,函数签名即文档,强迫调用方处理空分支;指针判空靠约定,且指针还背负「所有权」歧义(要不要 delete?)。optional 适合「函数结果可能缺席」,不适合表达「对象在别处、我只是引用」。

  • std::variant 与虚函数多态如何取舍? variant:类型集合编译期固定、无堆分配、无虚表开销、visit 可做穷尽性检查;继承:开放扩展、支持跨模块多态。封闭集合选 variant,开放扩展选继承——与 Rust enum、Java sealed + 模式匹配的取舍一致。

  • lambda 的捕获列表有哪些形式?按引用捕获有什么坑? [v] 按值、[&v] 按引用、[=]/[&] 隐式全捕获、[v = expr] 初始化捕获(C++14)。坑:按引用捕获的局部变量若先于 lambda 销毁(异步场景)就是悬垂引用,按值捕获或捕获智能指针规避。

自测三问

  1. [limit]、[&limit]、[&] 三种捕获分别什么语义?
  2. opt.value_or(x) 在 opt 有值和无值时分别返回什么?
  3. if constexpr 相比普通 if 在模板里多给了你什么能力?

小结一句话

auto 消灭冗长类型、范围 for 与结构化绑定简化遍历、lambda 表达行为、optional 与 variant 把「可能没有」「多选一」写进类型系统——C++17/20 的语法让 C++ 的表达力对齐甚至反超现代 Java。

延伸

  • cppreference:std::optional
  • cppreference:std::variant
  • 下一课:多线程——std::thread、互斥锁与原子操作。
加载本节练习…
← 上一课模板基础下一课 →多线程