阶段五讲义:STL 全貌——扔掉手写循环,学“动词“

对应教材\[A Tour of C 3rd Edition-2022.pdf\|A Tour of C++ 3rd] 第 9、12、13、14、16 章(主线)· 10、11、15、17 章(按需)· \[Programming Principles and Practice Using C++ 3rd Edition-2024.pdf\|PPP 3rd] ch19--21(练手) 阶段定位 :标准库课。这是收获感最强的一段------你终于能扔掉一大半手写循环 。学完这一课,你阶段二那个手写线性查找 + 手写计数的 WordCounter,将被 std::map 和几个标准算法取代,代码量砍掉 60%。 前置\[阶段四-模板与泛型\|阶段四](模板与泛型)------你现在知道 vector<T>、算法、迭代器都是模板,知道 lifting。STL 就是把 lifting 做到极致的结果。 编译验证 :本讲义所有代码均用 g++ -std=c++20 -Wall -Wextra 实际编译运行验证过。


0. 本课地图

这一课解决三个问题:

  1. STL 是什么? ------ 三个东西组成一个三角:容器 (存数据的结构)、算法 (对数据做的操作)、迭代器 (算法与容器之间的桥)。算法只管 [begin, end) 这对迭代器,不知道容器是谁;容器只管提供 begin/end,不知道算法在做什么。
  2. 怎么选容器? ------ 选容器的第一问:随机访问,还是插入删除? 随机访问 → vector;频繁中间插入 → list;按键查 → map/unordered_map。默认永远是 vector
  3. 怎么把代码写得像 STL? ------ 思维转变:从"我该写哪个循环"变成"我该用哪个动词" 。学算法 = 学动词:findsortcountaccumulatetransformcopy。再加 C++20 的 ranges 管道v | views::filter(pred) | views::transform(fn),从左往右读,和英文一致。

为什么 STL 值得单独一课? 因为它救过 C++ 的命。1998 年 C++98 标准化时,Alexander Stepanov 的 STL 进了标准库------\[C++之父交谈录#二、C++ 的历史与演化\|没有 STL,C++ 大概率会变成一门慢慢死掉的 OO 语言]。STL 的架构三根支柱(容器/迭代器/算法)从 1994 年交出来就没变过。这一课学的,是那套让 C++ 活下来的设计。

贯穿全课的主线 :你阶段二的词频统计工具 WordCounter 从 v0.8 重构成 STL 风格:

  • v1.0 (第八讲):std::map<std::string,int> 直接取代 WordCounteradd() 里的手写线性查找 变成 ++freq[w] 一行;按次数排序交给 std::sort
  • v1.1 (第七讲):停用词过滤交给 std::erase_if,输出交给 ranges 管道。
  • 代码量对比:v0.8 三个文件约 45 行(含两个手写循环)→ v1.1 约 20 行,查找、计数、排序、过滤全部零手写循环
章节 关键产出 主线进展
§9 库总览 STL 三角 + 标准库地图 看地图,再进丛林
§12 容器 容器家族、选容器第一问 v0.8 的 vector<Word_freq> 该不该换?
§13.1--13.2 算法是动词 sort/find/count/accumulate/transform 认识要用的动词
§13.4 谓词 find_if/count_if/lambda 停用词过滤变成谓词
§13.5+12.8 erase+remove_if 真正的删除 停用词从容器里删掉
§13.3.1 流迭代器 istream_iterator/ostream_iterator 读文件排序去重,零循环
§14 ranges 管道 + 懒求值 v1.1 终极形态
§16+15 工具 span/string_view/optional/chrono v1.0map 词频统计

一句话总纲 (A Tour §13.5 原话):"标准库算法往往比平均水平的手写循环设计得更仔细、规定得更明确、实现得更好。认识它们,并用它们代替裸语言写出的代码。"


第一讲:先看地图再进丛林------STL 三角与标准库地图(A Tour §9)

是什么

A Tour §9.2 把标准库分成了两大类:

  • STL (Stepanov 1994):容器(vector、map...)+ 算法(find、sort、merge...)+ 迭代器。这个框架可扩展------你可以往里面加自己的容器和算法。
  • 其他部分 :字符串、正则、I/O、文件系统、并发(线程/锁)、数值、variant/optional 等工具、时间(chrono)、智能指针。

§9.1 的原话值得先立住:

"标准库规范占 ISO C++ 标准的三分之二以上。探索它,并用它代替自制的东西。它的设计倾注了大量思考,实现倾注了更多,维护和扩展还将倾注大量努力。"

为什么

  • 先看地图,再进丛林 (教案原话)。你不需要一次学完所有东西,但要先知道"有这么个东西、管什么用、在哪个头文件"。地图的意义是:遇到问题时知道标准库有解,而不是自己造轮子
  • §9.2 讲了收录标准库的三个标准:"几乎每个 C++ 程序员都用得上"、"比简化版本不增加显著开销"(零开销)、"简单用法易学"。这三个标准就是你判断"该不该用库"的准则。
  • 不用裸语言写东西 :§9.1 "没有重要程序只用裸语言写成。先用库打好地基。" 你已经见识过------阶段二的手写线性查找,换成 ++freq[w] 一行。
  • 一句历史注脚 :我在\[C++之父交谈录#三、背后的决策逻辑\|决策逻辑里承认过]------早期没做好基础库是 C++ 的一个失败,"语言只是半成品,共享地基比语言特性更重要"。标准库就是那个"共享地基"。

代码逐行

STL 三角第一次完整亮相(A Tour §13.1 的例子):

cpp 复制代码
// ex1_stl_triangle.cpp ------ STL 三角:容器 + 算法 + 迭代器
#include <algorithm>
#include <iostream>
#include <list>
#include <string>
#include <vector>

struct Entry {
    std::string name;
    int number;
};

bool operator<(const Entry& x, const Entry& y) { return x.name < y.name; }
bool operator==(const Entry& x, const Entry& y) { return x.name == y.name; }

int main() {
    std::vector<Entry> vec {
        {"Karl Popper", 234567},
        {"David Hume", 123456},
        {"Bertrand Russell", 345678},
        {"David Hume", 123456},            // 重复
    };
    std::list<Entry> lst;

    std::sort(vec.begin(), vec.end());                      // 算法 sort
    std::unique_copy(vec.begin(), vec.end(),
                     std::back_inserter(lst));              // 去重后拷进 list

    for (const auto& e : lst)                               // 迭代器/范围 for
        std::cout << e.name << " " << e.number << '\n';
}
  • operator<operator==Entry 自己的比较能力 ------算法不关心 Entry 是什么,只要求"能用 <=="。这是\[阶段四-模板与泛型\|阶段四] lifting 思想的回报。
  • std::sort(vec.begin(), vec.end()):排序 [begin, end) 这个半开区间
  • std::unique_copy(begin, end, std::back_inserter(lst)):把"不重复的元素"拷进 lstback_inserter(lst) 是输出迭代器------它让 unique_copy给容器添加元素(见第五讲:算法本身不改容器大小,back_inserter 是例外通道)。
  • 输出:按名字排好序、去重后的三行(Bertrand RussellDavid HumeKarl Popper)。

标准库地图(§9.3.4 精选头文件,先混个脸熟):<vector><list><map><set><unordered_map> 容器;<algorithm> 算法;<ranges> ranges;<string>/<string_view> 字符串;<iostream>/<fstream>/<sstream> I/O;<memory> 智能指针;<optional>/<variant>/<tuple> 工具;<chrono> 时间;<thread> 并发;<filesystem> 文件系统;<regex> 正则;<random> 随机。

常见坑

  • ❌ 不 #include 就用。标准库设施都要通过头文件引入(§9.3.1)。报 xxx is not a member of std 先查头文件。
  • using namespace std; 图省事。本书教学里可以用(§9.3.1 说"通篇用标准库时无伤大雅"),但你自己的工程里这是坏品味------名字会打架。
  • ❌ 想要"标准库的 range 版"却忘了显式写 ranges::。§9.3.2:std::sort(v) 会报"没有匹配函数";要么 ranges::sort(v),要么 using ranges::sort;。两种风格并存是历史包袱(98 年的 STL 没有 range 版)。

教材指引(可跳转)

  • \[A Tour of C 3rd Edition-2022.pdf\|A Tour] §9.1--9.2(必读):标准库是什么、收录标准、完整组件清单(建立地图)。
  • \[A Tour of C 3rd Edition-2022.pdf\|A Tour] §9.3(必读):命名空间 std、ranges 命名空间、头文件总表。
  • \[The C++ Programming Language 4th Edition-2013.pdf\|TCPL] §14--16(查证):标准库总览。
  • \[C++之父交谈录#六、从 C++98 到现代 C++:迁移指南\|交谈录:STL 的整体地图](重读):三根支柱的完整论述 + 容器家族表 + 迭代器五类。
  • ⏭️ \[阶段四-模板与泛型\|阶段四]衔接 :STL 算法就是阶段四 my_find/my_count_if 的"工业完成版"------你现在终于可以直接用标准库了。

第二讲:容器家族与选型(A Tour §12)

是什么

A Tour §12.8 的容器总表:

容器 是什么 迭代器类别
vector<T> 变长数组,连续内存 contiguous(随机访问)
string 字符序列 contiguous
array<T,N> 固定长度连续内存 contiguous
deque<T> 双端队列 random_access
list<T> 双向链表 bidirectional
forward_list<T> 单向链表 forward
map<K,V> 按键有序(红黑树) bidirectional
unordered_map<K,V> 哈希表,无序 forward
set<T> / multiset / multimap / unordered_* 集合与变体 同 map 系

选容器第一问(教案原文)随机访问,还是插入删除? 随机访问 → vector;频繁中间插入 → list;按键查 → map/unordered_map。默认永远是 vector

为什么

A Tour §12.2 的原话(值得背):

"标准库 vector 非常灵活高效。把它当默认容器 ------除非你有确凿的理由用别的。如果你因为对'效率'的模糊担忧而避开 vector,去测量。我们对容器使用性能的直觉是最靠不住的。"

选容器真正的权衡,不是"哪个快",而是你用什么操作

  • 你要按下标访问、遍历、排序 → vector(连续内存 = 缓存友好)。
  • 你要在中间插入/删除 且元素很大 → list(链表原地改指针,不移动元素)。但 §12.3 警告:"vector 在短序列、小元素上往往比 list 快------即使插入删除也是。"
  • 你要按键查 → map 是红黑树 O(log n);数据量大、只查不改序 → unordered_map 哈希表平均 O(1)。§12.6:map 百万元素只需约 20 次比较。

记忆锚点vector 就是\[阶段一-立世界观\|阶段一]那个 MiniVec------三个指针 [p:q:cap)、24 字节、内存连续。它快不是因为它"聪明",是因为内存连续,缓存里连预取都替你做好了。选容器先想清楚你的操作,这是"边界谁负责"在数据结构层面的体现。

代码逐行

cpp 复制代码
// ex2_containers.cpp ------ 容器家族与选型
#include <iostream>
#include <list>
#include <map>
#include <string>
#include <unordered_map>
#include <vector>

int main() {
    // 默认永远是 vector:随机访问最快
    std::vector<int> vi {3, 1, 4, 1, 5};
    std::cout << "vector 随机访问 vi[2] = " << vi[2] << '\n';

    // 频繁中间插入/删除:list
    std::list<int> li {1, 2, 3};
    auto p = li.begin(); ++p;              // 指向 2
    li.insert(p, 9);                       // 在 2 前插入 9,不移动其他元素
    for (int x : li) std::cout << x << ' ';
    std::cout << '\n';

    // 按键查:map(红黑树,O(log n))
    std::map<std::string, int> phone_book {
        {"David Hume", 123456},
        {"Karl Popper", 234567},
    };
    std::cout << "map lookup: " << phone_book["David Hume"] << '\n';

    // 大表快查:unordered_map(哈希表,平均 O(1))
    std::unordered_map<std::string, int> fast_book {
        {"David Hume", 123456},
        {"Karl Popper", 234567},
    };
    std::cout << "unordered_map lookup: " << fast_book["Karl Popper"] << '\n';
}
  • 输出:vector 随机访问 vi[2] = 41 9 2 3(9 插到了 2 前面)、map lookup: 123456unordered_map lookup: 234567
  • phone_book["David Hume"]map 的下标 。A Tour §12.5 有个重要警告:如果键不存在,[] 会把键插入 map 并给默认值 0 。查"不该有默认值的键"要用 find()/contains(),否则你会在 map 里看到幽灵词条。
  • 两个重要陷阱(§12.2.1):
    • vector<Shape> vs; 是错的 ------虚函数多态需要指针/智能指针,vector<unique_ptr<Shape>> 才对。对象直接放进容器 = 被切片。
    • vector 不保证范围检查book[book.size()] 是未定义行为;要检查就用 at()(会抛 out_of_range)。
  • 迭代器失效 (§12.2):vector 扩容(reserve/push_back)会让已有迭代器和指针失效;list 的插入删除则不会。A Tour 的 Advice 5reserve() 用在你需要元素指针稳定的时候。

常见坑

  • ❌ "我觉得 list 插入快"------没测量 。§12.3 原话:"vector 在短序列、小元素上常比 list 快,即使 insert/erase 也是。"直觉不可信,A Tour 反复强调:测量
  • ❌ 用 map 当"数组"随手 [] 访问,引入不存在的键。查不到就用 contains()/find()
  • ❌ 忘了一开始就问"我要什么操作"。随机访问 vs 插入删除 vs 按键查------先回答这个问题,再谈容器。
  • ❌ 把多态对象直接放进容器。vector<Shape> 是切片,用 vector<unique_ptr<Shape>>

教材指引(可跳转)

  • \[A Tour of C 3rd Edition-2022.pdf\|A Tour] §12.1--12.6(必读):vector(含 capacity/reserve/push_back 实现)、list、forward_list、map、unordered_map。
  • \[A Tour of C 3rd Edition-2022.pdf\|A Tour] §12.8 (必读):容器总表 + 通用操作表 + 30 条 Advice(这是你"背表"的来源)。
  • \[The C++ Programming Language 4th Edition-2013.pdf\|TCPL] §31--32(查证):容器与容器设计。
  • ⏭️ 主线 :你的 WordCounter 内部是 vector<Word_freq>add() 每次 O(n) 线性查找。读完第三讲你会知道:这活该交给 map

第三讲:算法是"动词"(A Tour §13.1--13.2)

是什么

算法 = 泛型函数。A Tour §13.5 的表是核心动词清单,先记这 8 个:

erlang 复制代码
find(b,e,x)        在 [b,e) 里找 x,返回迭代器(找不到返回 e)
find_if(b,e,f)     找第一个满足谓词 f 的元素
count(b,e,x)       数一数等于 x 的有几个
count_if(b,e,f)    数一数满足 f 的有几个
sort(b,e)          排序
accumulate(b,e,v)  从 v 开始累加(<numeric>)
transform(b,e,out,f) 把 f 作用到每个元素,结果写进 out
copy(b,e,out)      拷贝
for_each(b,e,f)    对每个元素执行 f

为什么

A Tour §13.2 讲透了 STL 的设计哲学:

"迭代器把算法和容器分开。 算法通过迭代器操作数据,不知道元素存在哪个容器里;反过来,容器不知道有算法在操作它的元素------它只负责在请求时提供迭代器(比如 begin 和 end)。这种存储与算法的分离,造就了非常通用灵活的软件。"

这就是\[阶段四-模板与泛型\|阶段四]"算法只认迭代器"的完整体现。所以:

  • 写代码的思维要转变:从"我该写哪个循环"变成"我该用哪个动词"(教案原话)。
  • find 返回 end() 表示"没找到" (§13.2):空手而归不用发明特殊值------你在阶段四的 my_find 里已经用了这个约定,现在用标准库同款。
  • 一段手写循环 = 一个算法调用。你越会"动词",循环越少。

零开销的实证std::sortint 调用 operator<编译期定好的直接调用 (内联展开);C 的 qsort 收函数指针,每次比较都得 call 一次。实测 -O2 反汇编:std::sort + lambda 的比较被内联成一条 cmpl 指令散在排序循环里,而 qsort 每次比较都跳一次指针------这就是 std::sort 永远比 qsort 快、而代码还能更短的原因\[C++之父交谈录#四、五个根本技术\|交谈录原话])。

代码逐行

cpp 复制代码
// ex3_verbs.cpp ------ 算法是"动词"
#include <algorithm>
#include <iostream>
#include <numeric>
#include <vector>

int main() {
    std::vector<int> v {3, 1, 4, 1, 5, 9, 2, 6};

    auto it = std::find(v.begin(), v.end(), 5);        // 找:返回迭代器
    std::cout << "找到了 5,在 " << (it - v.begin()) << " 位\n";

    int n = std::count(v.begin(), v.end(), 1);         // 数:等于 1 的个数
    std::cout << "1 出现 " << n << " 次\n";

    int sum = std::accumulate(v.begin(), v.end(), 0);  // 加:从 0 开始累加
    std::cout << "sum = " << sum << '\n';

    std::vector<int> sq;
    std::transform(v.begin(), v.end(), std::back_inserter(sq),
                   [](int x) { return x * x; });       // 变换:每个元素平方
    std::cout << "前三个平方: ";
    for (int i = 0; i < 3; ++i) std::cout << sq[i] << ' ';
    std::cout << '\n';

    std::sort(v.begin(), v.end());                     // 排序:就地
    std::cout << "排序后: ";
    for (int x : v) std::cout << x << ' ';
    std::cout << '\n';
}
  • 输出:找到了 5,在 4 位1 出现 2 次sum = 31前三个平方: 9 1 16排序后: 1 1 2 3 4 5 6 9
  • it - v.begin()随机访问迭代器支持减法,得到下标。list 的迭代器不能这么减(阶段四讲过)。
  • std::accumulate(v.begin(), v.end(), 0):第三个参数是初值 ,决定累加类型。传 0 是 int 累加,传 0.0 是 double 累加------阶段四 sum2(vi, 0.0) 的同款设计。
  • std::transform(..., std::back_inserter(sq), ...):输出迭代器让结果追加进 sq。
  • range-for 与迭代器等价 (§13.1):for (auto& x : v) 就是编译器对 for (auto p = v.begin(); p != v.end(); ++p) 的简写,且"更简单、更不易错、往往更快"。

常见坑

  • ❌ 循环里就为了找/数/累加,手写。先想动词:这是 find?count?accumulate?
  • ❌ 忘了"找不到返回 end()"的约定,去检查返回值是不是别的东西。
  • ❌ 把 accumulate 的初值写错类型(0 vs 0.0),导致精度丢失------初值决定类型。
  • transform 的目标写进已有容器却没预留空间。用 back_inserterresize

教材指引(可跳转)

  • \[A Tour of C 3rd Edition-2022.pdf\|A Tour] §13.1(必读):Introduction + sort/unique_copy 例子 + back_inserter。
  • \[A Tour of C 3rd Edition-2022.pdf\|A Tour] §13.2(必读):find、find_all、迭代器分离容器与算法。
  • \[A Tour of C 3rd Edition-2022.pdf\|A Tour] §13.5 (必读):算法总表(这是一页背单词)。

第四讲:谓词------把"规则"做成参数(A Tour §13.4)

是什么

find_ifcount_ifsort(f) 这类算法,把规则本身 当作参数。这个参数叫谓词 (predicate):一个"调用后返回 bool"的东西。最常用的写法是 lambda\[阶段四-模板与泛型\|阶段四]讲过),或用函数对象(阶段四的 Less_than)。

为什么

  • 算法关心"怎么找/怎么数/怎么排",规则由你给。A Tour §13.4:"我们常常想把动作做成算法的参数。"
  • lambda 是 STL 的转折点\[C++之父交谈录#六、从 C++98 到现代 C++:迁移指南\|交谈录原话]):C++98 时代的 STL 有个巨大痛点------想给 find_if 传个谓词,得先定义一个命名函数对象类,光那几行类定义比调用还长。于是没人用 STL 算法,都手写循环。C++11 的 lambda 一次性解决了这个采纳问题:
cpp 复制代码
// C++98:先写一个完整的仿函数类
struct GreaterThan42 {
    bool operator()(int x) const { return x > 42; }
};
auto it = find_if(v, GreaterThan42());

// C++11:一行
auto it = find_if(v, [](int x){ return x > 42; });

这不是语法糖,是让 STL 算法第一次值得用。 有了 lambda,sort 的自定义比较、find_if 的谓词、accumulate 的累积规则都能内联地写。

  • 谓词不能修改它作用到的元素 (§13.4 原话)------这是约定,也是 predicate 概念的定义(§14.5.1)。

代码逐行

cpp 复制代码
// ex4_predicates.cpp ------ 谓词参数
#include <algorithm>
#include <iostream>
#include <map>
#include <string>
#include <vector>

struct Greater_than {                  // 函数对象:携带阈值 42
    int val;
    bool operator()(const std::pair<const std::string, int>& r) const {
        return r.second > val;
    }
};

int main() {
    std::vector<int> v {3, 1, 4, 1, 5, 9, 2, 6};
    auto it = std::find_if(v.begin(), v.end(), [](int x) { return x > 4; });
    std::cout << "第一个 >4 的是 " << *it << '\n';

    int cnt = std::count_if(v.begin(), v.end(), [](int x) { return x % 2 == 0; });
    std::cout << "偶数个数: " << cnt << '\n';

    std::map<std::string, int> m {
        {"a", 1}, {"b", 50}, {"c", 10}, {"d", 99},
    };
    auto mp = std::find_if(m.begin(), m.end(), Greater_than{42});
    std::cout << "第一个 value>42 的键是 " << mp->first << '\n';
}
  • 输出:第一个 >4 的是 5偶数个数: 3第一个 value>42 的键是 b
  • [](int x) { return x > 4; }:就地谓词。find_if 返回第一个让谓词为真的元素。
  • Greater_than{42}:函数对象携带阈值。注意 operator() 的参数类型是 std::pair<const std::string,int>------map 的元素是 pair<const Key, Value>,const 别忘了 (阶段四 my_count_if 不挑类型,这里 map 的约束写死了)。
  • 两者等价(§13.4 原话):auto p = find_if(m, [](const auto& r) { return r.second > 42; });

常见坑

  • ❌ 谓词里有副作用(改了元素、改了外部状态)。find_if 可能多次调用谓词,且概念 predicate 要求"等值保持"(§14.5.1)------同一输入两次调用结果必须一致。
  • ❌ 为了一个一行规则写个具名类。用 lambda。
  • ❌ 忽略 const:map 的 key 是 const 的,谓词参数类型写错编译不过。

教材指引(可跳转)

  • \[A Tour of C 3rd Edition-2022.pdf\|A Tour] §13.4(必读):find_if + Greater_than + lambda 等价写法。
  • \[A Tour of C 3rd Edition-2022.pdf\|A Tour] §14.5.1 (查证):predicate/relation/strict_weak_order 概念------语义约定的来源。
  • \[C++之父交谈录#六、从 C++98 到现代 C++:迁移指南\|交谈录:lambda 是 STL 的转折点](重读):C++98 的仿函数之痛 → C++11 的一行。

第五讲:算法不改容器大小------erase + remove_if(A Tour §13.5 + §12.8)

是什么

A Tour §13.5 有一条铁律:

"有些算法(如 replace()sort())会修改元素的值,但没有任何算法会向容器添加或删除元素。原因是一个序列并不指明承载它的容器是谁。"

所以"删除所有满足条件的元素"是两步:

  1. std::remove_if(begin, end, pred):把不删的元素搬到前面,返回"新逻辑末尾"的迭代器。注意:它不删,只是搬。
  2. container.erase(新末尾, 原末尾):真正缩小容器。

这个组合叫 erase--remove 惯用法 ,用 v.erase(std::remove_if(v.begin(), v.end(), pred), v.end())

为什么

  • 算法的通用性靠"序列 = 迭代器对":算法只看到一对迭代器,看不到容器,所以没法(也不该)改容器大小。这是第一讲 STL 三角的推论。
  • 例外:输出迭代器back_inserter(lst) 假装是"迭代器",实际每次写都 push_back------它是"容器知道自己是容器"的通道(§13.1)。
  • 关联容器(map/set)不能 erase--remove (元素不是连续内存),C++20 给它们专门加了 std::erase_if(container, pred) 直接一步删。

代码逐行

cpp 复制代码
// ex5_erase_remove.cpp ------ 经典组合:erase + remove_if
#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> v {1, 2, 3, 4, 5, 6, 7, 8};

    // remove_if 把"不删的元素"搬到前面,返回新逻辑末尾的迭代器;
    // erase 用 [新末尾, 原末尾) 真正缩小容器。
    v.erase(std::remove_if(v.begin(), v.end(),
                           [](int x) { return x % 2 == 0; }),
            v.end());

    for (int x : v) std::cout << x << ' ';
    std::cout << '\n';
}
  • 输出:1 3 5 7
  • remove_if 之后、erase 之前,容器的 size() 没变------只是"偶数都被移到了末尾",[新末尾, end) 是一堆无用的旧值。忘了 erase 是新手最常见的 bug:容器看起来没变小。

map 上的版本(教案"过滤停用词"的正解):

cpp 复制代码
// ex10_eraseif_map.cpp ------ 关联容器要用 std::erase_if(C++20)
#include <algorithm>
#include <iostream>
#include <map>
#include <string>

int main() {
    std::map<std::string, int> freq {
        {"the", 40}, {"fox", 10}, {"dog", 5}, {"a", 100}, {"and", 30},
    };
    std::erase_if(freq, [](const auto& kv) {          // 一步删停用词
        const auto& w = kv.first;
        return w == "the" || w == "a" || w == "and";
    });
    for (const auto& [w, c] : freq) std::cout << w << ": " << c << '\n';
}
  • 输出:dog: 5fox: 10(the/a/and 没了)。
  • 为什么 map 不能 erase--removeremove_if 需要把元素"搬到前面",map 是树、元素地址不可搬动。std::erase_if 是 C++20 为关联容器特化的版本------直接遍历删除。

常见坑

  • ❌ 只 remove_iferase,然后困惑"为什么没删掉"。两步缺一不可(对序列容器)。
  • ❌ 对 map 用 remove_if------编译不过,用 std::erase_if
  • ❌ 忘了"算法不加不减元素",试图在 for_eachpush_back 到正在遍历的容器------迭代器失效、未定义行为。

教材指引(可跳转)

  • \[A Tour of C 3rd Edition-2022.pdf\|A Tour] §13.5(必读):算法总表 + "没有算法增删容器元素"的原文。
  • \[A Tour of C 3rd Edition-2022.pdf\|A Tour] §13.1(查证):back_inserter 这个"例外通道"。
  • \[A Tour of C 3rd Edition-2022.pdf\|A Tour] §12.8(查证):erase/insert 的语义。

第六讲:流迭代器------算法作用于 I/O(A Tour §13.3.1)

是什么

迭代器不只适用于容器。输入流产生一个值序列,输出流接收一个值序列------所以流也可以是"容器":

  • istream_iterator<T>:把输入流当只读容器
  • ostream_iterator<T>:把输出流当可写目标

为什么

  • 这样算法就能直接作用在流上:从 cin 读单词 → sort → unique → 写 cout,全程没有一行手写循环。
  • §13.3.1 给出了标准库的"思想实验":读文件、排序、去重、写文件 ,先给出 10 行版本,再指出"用 set 压根不存重复项"只需 5 行。这个例子展示了标准库的威力:数据结构选对了,算法自然简单
  • ifstream 是能接文件的 istreamofstream 同理(§11.7.2)------所以这段代码的输入输出换成文件,只改一行。

代码逐行

cpp 复制代码
// ex6_streamiter.cpp ------ 流迭代器:读单词 -> 排序 -> 去重 -> 输出(零手写循环)
#include <algorithm>
#include <iostream>
#include <iterator>
#include <string>
#include <vector>

int main() {
    // 从 cin 读入所有单词
    std::vector<std::string> b {std::istream_iterator<std::string>{std::cin},
                                std::istream_iterator<std::string>{}};
    std::sort(b.begin(), b.end());
    std::unique_copy(b.begin(), b.end(),
                     std::ostream_iterator<std::string>{std::cout, " "});
    std::cout << '\n';
}
  • 输入 the quick fox the quick brown fox,输出 brown fox quick the

  • std::istream_iterator<std::string>{std::cin}:开始迭代器(从 cin 读 string);{} 默认构造的是结束迭代器 (哨兵)。{a, b} 两个迭代器初始化 vector = "把 cin 里的所有单词读进 b"。这个 vector 构造就是一次读取循环

  • std::ostream_iterator<std::string>{std::cout, " "}:写到 cout,每个值后面跟一个空格。

  • A Tour 的更优解 :需求是"去重输出",为什么不压根不存重复?用 std::set

    cpp 复制代码
    std::set<std::string> b {std::istream_iterator<std::string>{std::cin},
                             std::istream_iterator<std::string>{}};
    std::copy(b.begin(), b.end(),
              std::ostream_iterator<std::string>{std::cout, " "});

    set 自动有序、自动去重,unique_copy 降级为 copy数据结构选对了,代码自然变短。

  • 警告(§13.3.1):sort(b) 这种 range 版和传统 sort(b.begin(),b.end()) 并存可能歧义------要显式 ranges::sort(b)using ranges::sort;

常见坑

  • ❌ 忘记 istream_iterator默认构造就是结束哨兵std::istream_iterator<T>{} 表示"流的末尾"。
  • ❌ 流迭代器用错类型(比如想读 int 却用 string)。读什么类型模板参数说了算。
  • ❌ 用 set 时忘了它默认有序去重------这正是选对数据结构让算法变简单的例子。

教材指引(可跳转)

  • \[A Tour of C 3rd Edition-2022.pdf\|A Tour] §13.3.1(必读):istream_iterator/ostream_iterator 全文 + set 版本对比。
  • \[A Tour of C 3rd Edition-2022.pdf\|A Tour] §11.7.2(查证):ifstream/ofstream------这段代码接文件只改一处。

第七讲:Ranges 管道与懒求值(A Tour §14)

是什么

C++20 的 <ranges> 给了三样东西:

  • range 算法std::ranges::sort(v)------整个容器一次传进去,不用 begin/end(§14.1 原话:"range 这个概念让我们能写 sort(v) 而不是 1994 年以来的 sort(v.begin(),v.end())")。
  • view(视图) :不拥有元素的"观察窗口"。filter_view(挑出满足谓词的)、transform_view(对每个元素做变换)、take_view(取前 n 个)......
  • 管道v | views::filter(pred) | views::transform(fn)------从左往右读,像 Unix 管道。

为什么

  • §14.1 说 range 表达了"99% 的常见算法用法",还消灭一类愚蠢错误 ------sort(v1.begin(), v2.end())(两个不同容器的迭代器)这类错,range 版根本没法写。
  • 懒求值 (教案必懂点):管道里的变换只在被消费时才计算 。§14.4 的例子:r | filter(odd) | take(3) 只消费到第 3 个奇数就停,后面的元素 filter 都不会碰。
  • 可读性 :§14.4 "管道风格(用 Unix 管道符 |)被普遍认为比嵌套调用更可读。管道从左往右工作"。filter(transform(r)) 要从里往外读,r | filter | transform 从前往后读,和英文一致。
  • 这是函数式组合的入口\[C++之父交谈录#四、五个根本技术\|交谈录:泛型的组合能力]):v | views::filter(...) | views::transform(...) 就是 filter → map 的流水线------为什么这要归功于泛型而不是 OO?因为"函数作为参数"必须在编译期定型,模板才干得了;虚函数在运行期只能分派"对象",分派不了"操作"。
  • 性能兜底:§14.4 也承认"view 和管道的实现涉及一些高难模板元编程,如果你关心性能一定要测量;不行就用传统写法"------诚实,且和 A Tour 一贯的"测量"主张一致。

代码逐行

cpp 复制代码
// ex7_ranges.cpp ------ ranges 管道 + 懒求值验证
#include <iostream>
#include <ranges>
#include <vector>

int main() {
    std::vector<int> v {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    // 管道从左往右读:过滤奇数 -> 平方 -> 取前 3 个
    for (int x : v | std::views::filter([](int x) { return x % 2 == 1; })
                   | std::views::transform([](int x) { return x * x; })
                   | std::views::take(3))
        std::cout << x << ' ';
    std::cout << '\n';

    // 懒求值验证:transform 的 lambda 数一数自己被调了几次
    int calls = 0;
    auto counted = [&calls](int x) { ++calls; return x * x; };
    int total = 0;
    for (int x : v | std::views::filter([](int x) { return x % 2 == 1; })
                   | std::views::transform(counted)
                   | std::views::take(3))
        total += x;
    std::cout << "只消费 3 个,transform 实际被调 " << calls << " 次,total=" << total << '\n';
}
  • 输出:1 9 25只消费 3 个,transform 实际被调 3 次,total=35
  • 懒求值的实证v 有 10 个元素,filter 只挑奇数(1、3、5、7、9),take(3) 只要前 3 个------transform 的 lambda 被调了恰好 3 次 ,不是 5 次、更不是 10 次。每个元素只在被需要时才流经管道
  • 管道是表达式v | filter(...) | transform(...) | take(3) 求值后是一个 view,可直接 range-for。
  • view 不拥有元素 (§14.2):它只"看"。所以 auto bad() { return v | views::filter(...); } 里若 v 被销毁,返回的 view 悬空------view 不能活得比它的 range 久
  • 生成器(§14.3):views::iota(42, 52) 生成 42..51;istream_view<T>(cin) 把流变成 view。

range 算法(§14.1 的现代写法,已验证):

cpp 复制代码
// ex11_ranges_alg.cpp ------ ranges 算法:sort(v) 而不是 sort(begin,end)
#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> v {5, 3, 1, 4, 2};
    std::ranges::sort(v);            // 整个容器一次传进去
    for (int x : v) std::cout << x << ' ';
    std::cout << '\n';

    auto it = std::ranges::find(v, 3);
    if (it != v.end())
        std::cout << "ranges::find 找到 3 在 " << (it - v.begin()) << " 位\n";
}
  • 输出:1 2 3 4 5ranges::find 找到 3 在 2 位
  • std::ranges::sort(v)约束算法 (概念版),整个 range 一次传入。std::ranges::find 返回迭代器,找不到返回 v.end()------和传统版同一个约定。

常见坑

  • ❌ 让 view 比 range 活得久。view 不拥有元素,range 销毁 view 即悬空(§14.2 的 bad() 例子)。
  • ❌ 以为管道立即算完。懒求值:管道是"生产式"描述,真正遍历时才触发计算。
  • ❌ 混用传统 std::sort 和 range 版导致歧义。要显式 std::ranges::sort(v)using std::ranges::sort;(§9.3.2)。
  • ❌ 对性能没把握。测量;不行就退回传统算法(§14.4)。

教材指引(可跳转)

  • \[A Tour of C 3rd Edition-2022.pdf\|A Tour] §14.1 (必读):range 概念与 sort(v)§14.4 (必读):管道与懒求值;§14.2(必读):view 与"不拥有元素"。
  • \[A Tour of C 3rd Edition-2022.pdf\|A Tour] §14.3(选读):生成器 iota/istream。
  • \[A Tour of C 3rd Edition-2022.pdf\|A Tour] §14.5(查证):迭代器/range 概念表------你背表的正源。
  • \[The C++ Programming Language 4th Edition-2013.pdf\|TCPL] §18(查证):ranges 与 concepts。
  • \[C++之父交谈录#四、五个根本技术\|交谈录:泛型编程的组合能力](重读):为什么"函数作为参数"必须靠模板。

第八讲:现代实用工具 + 词频统计 v1.0(A Tour §16 + §15 按需)

是什么

按需章节里的高频工具,配主线收官:

  • string_view (§10.3 / §15):字符串的只读视图 ,零拷贝。函数参数用它,接受 stringconst char*、字面量都行。
  • span (§15.2.2):连续内存的视图。函数参数用它,vector、数组、&v[i], n 片段都行,替代裸指针 + 长度
  • optional<T> (§15.4.2):可能没有值。替代"用 -1 表示没找到""用 nullptr 表示空"这类魔法值。
  • pair/tuple:打包几个值。
  • chrono 计时 (§16.2):steady_clock::now() + duration_cast------测量,而不是猜

为什么

  • string_view/span 是现代指针:只读、零拷贝、自动携带长度,消灭"指针 + 长度"这堆易错参数。A Tour Advice 21:用它们替换裸指针。它们正是\[C++之父交谈录#四、五个根本技术\|"直接映射硬件"和"类型安全"在同一种类型里的交汇]------硬件看连续内存,类型看长度。
  • optional 消灭魔法值 :函数"可能不返回值"这个事实,用类型表达(std::optional<int>),编译器强制你检查。这是"类型表达意图"(\[阶段一-立世界观\|阶段一]三问题之三)的标准答案。
  • chrono 让"测量"成为一等公民 :A Tour §16.2.1 原话:"不要先做时间测量就说代码'效率'如何。关于性能的猜测最不可靠。"这是全书的基调,也是你判断"vector vs list""管道 vs 手写"的唯一依据。

代码逐行

cpp 复制代码
// ex9_utilities.cpp ------ span / string_view / optional / chrono 计时
#include <chrono>
#include <iostream>
#include <optional>
#include <span>
#include <string_view>
#include <vector>

// span:连续内存的只读视图,函数不用关心容器类型
int sum(std::span<const int> s) {
    int t = 0;
    for (int x : s) t += x;
    return t;
}

// string_view:字符串的只读视图,零拷贝
void print_word(std::string_view sv) { std::cout << '[' << sv << "] "; }

int main() {
    using namespace std::chrono;
    auto t0 = steady_clock::now();

    std::vector<int> v {1, 2, 3, 4, 5};
    std::cout << "sum(v) = " << sum(v) << '\n';
    std::cout << "sum(中间 3 个) = " << sum(std::span<const int>{&v[1], 3}) << '\n';

    std::string s = "hello world";
    print_word(std::string_view{s}.substr(0, 5));   // "hello"
    print_word(std::string_view{s}.substr(6));      // "world"
    std::cout << '\n';

    std::optional<int> maybe{42};                   // 可能没有值
    if (maybe) std::cout << "optional 有值: " << *maybe << '\n';
    std::optional<int> none{};
    if (!none) std::cout << "optional 空: " << none.value_or(-1) << '\n';

    auto t1 = steady_clock::now();
    std::cout << "全程耗时: " << duration_cast<microseconds>(t1 - t0).count() << "us\n";
}
  • 输出:sum(v) = 15sum(中间 3 个) = 9[hello] [world]optional 有值: 42optional 空: -1全程耗时: ...us
  • sum(std::span<const int>{&v[1], 3})span 可以盯住 vector 的任意一段------这是"指针+长度"的现代替代。
  • std::string_view{s}.substr(0, 5):子串是视图 ,不拷贝,print_word 直接收下。
  • optionalif (maybe) 检查 + *maybe 取值;value_or(-1) 给默认值。
  • steady_clock 计时:duration_cast<microseconds> 转成好读的单位。

主线收官:词频统计 v1.0

阶段二 WordCounter(45 行,手写线性查找 + 手写计数 + 手写输出)重构成 STL 风格:

cpp 复制代码
// ex8_wordcount_v10.cpp ------ 词频统计 v1.0:map + 算法,零手写查找/计数循环
#include <algorithm>
#include <iostream>
#include <map>
#include <string>
#include <vector>

int main() {
    std::map<std::string, int> freq;         // 取代整个 WordCounter!
    std::string w;
    while (std::cin >> w) ++freq[w];         // 查词 + 计数:map 一趟搞定

    // 按次数降序输出:map 按键序,拷贝出来按 value 排
    std::vector<std::pair<std::string, int>> v(freq.begin(), freq.end());
    std::sort(v.begin(), v.end(),
              [](const auto& a, const auto& b) { return b.second < a.second; });

    for (const auto& [word, count] : v)
        std::cout << word << ": " << count << '\n';
}
  • 输入 the fox the dog the fox the,输出:the: 4fox: 2dog: 1
  • ++freq[w] 这一行顶掉了 WordCounter::add() 里整个手写线性查找 + 计数逻辑。map 的 [] 在键不存在时自动插入、默认 0------正好配合 ++
  • std::sort(v.begin(), v.end(), lambda)按次数降序 。lambda 用 const auto& 参数(泛型 lambda)匹配 pair<const string,int>
  • 结构化绑定 for (const auto& [word, count] : v):一次取到键和值。
  • 代码量 :45 行 → 18 行,砍掉 60%。唯一剩下的循环是 while (std::cin >> w)------那是输入循环,不是算法循环;查找、计数、排序全部交给标准库。

常见坑

  • ❌ 用 optional 却不解引用就打印------*maybevalue_or() 二选一。
  • ❌ 让 string_view/span 指向被销毁的对象。它们是视图,不拥有数据(和 view 同理)。
  • ❌ 计时只用一次、依赖单次结果。A Tour §16.2.1:多次测量,小心缓存和罕见事件。
  • ❌ 用 std::move() 表演。A Tour §16.6:std::move 不搬动任何东西(它只是 cast 成右值引用);能省就省,编译器对返回值已经会做 RVO/移动

教材指引(可跳转)

  • \[A Tour of C 3rd Edition-2022.pdf\|A Tour] §15.2.2 (必读):span;§15.4.2(必读):optional。
  • \[A Tour of C 3rd Edition-2022.pdf\|A Tour] §16.2.1(必读):chrono 计时与"先测量再说效率"。
  • \[A Tour of C 3rd Edition-2022.pdf\|A Tour] §16.6(选读):move/forward------认识就好,别滥用。
  • \[A Tour of C 3rd Edition-2022.pdf\|A Tour] §10.3(按需):string_view。

练习与参考答案

练习 1(教案必做):词频统计重构为 map<string, int>

把阶段二 v0.8(freq.h/freq.cpp/main.cpp)重构成第八讲的 v1.0,对比重构前后的代码量 ,并逐行解释 ++freq[w] 为什么能顶掉整个手写查找。

参考答案要点 :见第八讲 ex8。45 行 → 18 行。++freq[w] 做的事:freq[w] 若键不存在则插入 {w, 0} 并返回引用,++ 自增------查找、插入、计数三合一 。这正是 map 的价值:把"线性查找 O(n)"变成"红黑树 O(log n)"。试着把 map 换成 unordered_map,数据量大时用 chrono 计时对比(这就是"测量")。

练习 2(教案必做):用 erase+remove_if 过滤停用词

给词频统计加"过滤停用词(the、a、and...)"。注意:map 不能用 erase+remove_if ------用 std::erase_if(第五讲 ex10);如果你先把 map 拷成 vector<pair<...>> 再处理,就可以用 erase+remove_if(第五讲 ex5)。两条路都写一遍,说出区别。

参考答案要点 :map 版用 std::erase_if(freq, lambda),一步删(ex10)。vector 版:先 v.erase(std::remove_if(v.begin(), v.end(), pred), v.end())区别在于容器 :序列容器(vector/list/deque/string)用 erase--remove 惯用法;关联容器(map/set)没有"搬运元素"的概念,C++20 直接给 std::erase_if教案说"用 erase+remove_if",在 vector 上照做,在 map 上要用对工具------这正是"选容器"的影响延伸到算法层面。

练习 3:给一个循环场景,说出该用哪个算法

逐一说出下面每个手写循环对应的标准库算法:

  1. 找第一个值 > 10 的元素
  2. 数一数有多少个 "the"
  3. 把所有单词转大写
  4. 把排序后的结果写进文件
  5. 从 vector 里删掉所有值为 0 的

参考答案 :1. std::find_if(begin, end, [](int x){ return x > 10; })。2. std::count(begin, end, "the")(或 count_if)。3. std::transform(begin, end, out, toupper_fn)。4. std::copy(begin, end, std::ostream_iterator<string>{ofs, "\n"})(第六讲)。5. v.erase(std::remove(v.begin(), v.end(), 0), v.end())这是过关题"给循环场景说出算法"的专项练习------答不上来的动词,回到 §13.5 的表复习。

练习 4:容器家族与迭代器类别(背表)

默写容器家族表,并说出每个的迭代器类别(至少 vector/list/forward_list/map/unordered_map)。

参考答案 :vector→contiguous(随机访问);string/array→contiguous;deque→random_access;list→bidirectional;forward_list→forward;map/set(有序树)→bidirectional;unordered_(哈希表)→forward。记忆法:连续内存的都是 random_access+(vector/array/deque);双向链表→bidirectional;单向→forward;树(map/set)是双向;哈希表(unordered_ )是 forward。sort 需要 random_access(阶段四的过关题),所以 list/forward_list/map/unordered_map 都不能 std::sort

练习 5:用 ranges 重写过滤 + 变换

给定 vector<int> v {1..10},用一条 ranges 管道输出"奇数的平方的前 4 个",并解释懒求值发生在哪里。

参考答案

cpp 复制代码
for (int x : v | std::views::filter([](int x) { return x % 2 == 1; })
               | std::views::transform([](int x) { return x * x; })
               | std::views::take(4))
    std::cout << x << ' ';    // 1 9 25 49

懒求值:filter 逐个问 v 的每个元素"奇数吗",只放行奇数;transform 只在需要输出下一个值 时才平方;take(4) 拿到第 4 个就停------第 7 个奇数 13 及以后,filter 根本不会去碰 。你可以用第七讲的 calls 计数器实证:transform 恰好被调 4 次。

练习 6(动手做):词频统计 v1.1 终极形态

把词频统计做成:map 计数 → std::erase_if 删停用词 → ranges::sort 按次数降序 → take(3) 输出前三。对照 v0.8,数一数还剩几个手写循环。

参考答案要点 :见 ex12(已验证,输出示例 fox: 2 / dog: 1 / quick: 1)。剩下的"循环"只有输入循环 while (std::cin >> w)------查找、计数、过滤、排序、截取全部是标准库调用。 这就是教案过关标准"没有手写循环"的含义:算法循环归算法,数据读取的循环是 I/O 本质,不算。

练习 7(选做):string_viewspan

写一个函数 void print_len(std::span<const std::string>),打印每个字符串长度;再写 void shout(std::string_view),把 view 的每个字符转大写输出(用 range-for + std::toupper)。体会"视图不拷贝"。

参考答案要点

cpp 复制代码
void print_len(std::span<const std::string> sv) {
    for (const auto& s : sv) std::cout << s.size() << ' ';
    std::cout << '\n';
}
void shout(std::string_view sv) {
    for (char c : sv) std::cout << static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
    std::cout << '\n';
}

print_len 能接 vector<string>,也能接 std::array<string,3>shout("hello") 直接收字符串字面量------视图让函数签名不再绑定具体容器/字符串类型 。注意 std::toupper 的参数要 cast 成 unsigned char(避免负值 UB)。

练习 8(教材精读):读 A Tour §13.5 的算法总表

把总表里没在本讲展开的算法(replace/replace_if/merge/equal_range/copy_if/move)各查一遍,用自己的话写一行注释。选出三个"你接下来最会用到的",说明为什么。

参考答案 :示例------replace(v, x, y):把所有 x 换成 y;copy_if(b,e,out,pred):把满足谓词的拷出来;equal_range:有序序列上二分查某个值的区间。最会用到的三个,通常是 sort+find_if+copy_if (排序、查找、过滤------覆盖大多数任务)。这题的用意:§13.5 的表是"动词字典",不要背,要用时查;读代码遇到陌生算法时回这页。


验收自测(对应交谈录"怎么算过关")

读完这一课,不看资料,你应该能答出:

  1. STL 三角是哪三样?各自职责? ------ 容器(存数据)、算法(操作数据)、迭代器(桥)。算法只认迭代器对 [begin,end),容器只负责提供 begin/end。
  2. 选容器第一问是什么? ------ 随机访问,还是插入删除?随机访问→vector;频繁中间插入→list;按键查→map/unordered_map。默认永远是 vector。
  3. 背容器家族 + 迭代器类别(vector→contiguous、list→bidirectional、forward_list→forward、map/set→bidirectional、unordered_*→forward)。
  4. 为什么默认 vector? ------ 连续内存、缓存友好、遍历/排序/搜索都快;A Tour:"除非有确凿理由,否则用 vector。直觉不可信,去测量。"
  5. 算法是"动词"是什么意思? ------ 思维从"写哪个循环"变成"用哪个动词"(find/sort/count/accumulate/transform/copy)。
  6. 算法会改容器大小吗? ------ 不会。序列容器删除 = erase+remove_if 两步;关联容器 = std::erase_if(C++20)。
  7. find 找不到返回什么? ------ end()。这是 STL 约定(阶段四 my_find 同款)。
  8. 流迭代器能干什么? ------ 让算法直接作用在流上:istream_iterator 读、ostream_iterator 写,读文件排序去重零手写循环。
  9. range 版算法怎么用? ------ std::ranges::sort(v)(整个 range 一次传入);与传统迭代器版并存需显式 ranges:: 限定。
  10. ranges 管道怎么写?为什么可读? ------ v | views::filter(pred) | views::transform(fn),从左往右读,与英文一致。
  11. 懒求值是什么? ------ 管道里的变换只在被消费时才计算;take(n) 之后的元素根本不会被处理。
  12. view 和容器什么关系? ------ view 不拥有元素,只是观察窗口;view 不能活得比它的 range 久。
  13. string_view/span 是什么? ------ 只读视图,零拷贝,自动携带长度,替代"裸指针+长度"。
  14. optional 解决什么? ------ "可能没有值"的类型安全表达,替代 -1/nullptr 魔法值。
  15. 阶段二工具重构后还剩几个手写循环? ------ 只剩输入循环 while (std::cin >> w);查找/计数/排序/过滤全交给标准库,代码量砍 60%。
  16. 验收红线 :不看讲义,把阶段二 v0.8 的 WordCounterstd::map + 标准算法重写成不超过 20 行、零手写算法循环的版本,编译运行通过。写不出这一步,本课不算过关。

走到这里,你已经完成了从"手写一切"到"标准库一切"的转变:阶段二你自己写了 my_findmy_count_ifmedian,阶段四把它们泛型化,阶段五------你终于可以直接用标准库了 。你的词频统计从 45 行缩到 18 行,从"数组+手写查找"变成"map+算法+ranges"。 下一课(\[阶段六-并发与资源\|阶段六])是另一座高峰:并发与资源 ------C++98 连线程的标准支持都没有,而你要学的是 A Tour §18 的生产者-消费者模型,用 jthread + scoped_lock + 条件变量,让两个线程安全地共享你的词频数据。

相关推荐
handler012 小时前
【Linux】动态库、静态库使用与原理
linux·运维·服务器·c++·c·动态库·静态库
raindayinrain2 小时前
c++并发--同步
c++·同步
蛋先生DX3 小时前
内存安全翻车现场的罪魁祸首,根源就在这三步里?
c++·后端·编程语言
小七在进步3 小时前
C++入门(1)
java·jvm·c++
晴天的雨.9923 小时前
[C++算法]快乐数
数据结构·c++·算法
影视飓风TIM3 小时前
C++11 核心新特性完整梳理
数据结构·c++·算法
艾莉丝努力练剑3 小时前
【AI大模型接入SDK】Ollama本地大语言模型部署
c++·人工智能·语言模型·自然语言处理·面试
晴天的雨.9923 小时前
[C++]算法双指针 复写0
数据结构·c++·算法
啦啦啦啦啦zzzz4 小时前
Appender抽象类的实现c++20
linux·服务器·c++·c++20