迭代器与算法库:C++ 标准库的脊梁
迭代器与算法库,说白了就是 C++ 标准库的灵魂。我刚开始用 STL 时,总觉得迭代器就是个指针的包装,后来踩过坑才明白——这东西远比我想象的深。今天咱们就把迭代器分类、适配器、算法库、Lambda 以及 C++20 的 Ranges 库串起来讲,保证你听完能写出更地道、更高效的代码。
迭代器分类:不只是五种类型
标准库定义了五种迭代器类别,从弱到强依次是:输入迭代器、输出迭代器、前向迭代器、双向迭代器、随机访问迭代器。你想想看,这其实是个能力递增的层级结构。
| 类别 | 能力 | 典型容器 |
|---|---|---|
| 输入迭代器 | 只读、单次遍历 | istream_iterator |
| 输出迭代器 | 只写、单次遍历 | ostream_iterator |
| 前向迭代器 | 读写、多次遍历 | forward_list |
| 双向迭代器 | 前向能力 + 反向移动 | list, set, map |
| 随机访问迭代器 | 双向能力 + 随机跳跃 | vector, deque, array |
我在项目中遇到过一个问题:用 std::list 的迭代器调用了 std::sort,结果编译报错。为什么?因为 std::sort 要求随机访问迭代器,而 list 只提供双向迭代器。嗯,这里要注意——选算法前先看迭代器类型。
核心原则:算法对迭代器的最低要求,决定了你能用哪些容器。别拿双向迭代器去跑随机访问算法,编译器会教你做人。
迭代器适配器:让迭代器更强大
标准库提供了几种迭代器适配器,我个人习惯把它们分成三类:反向迭代器、插入迭代器、流迭代器。
反向迭代器——用 rbegin() 和 rend() 就能反向遍历容器。我曾经在写一个日志分析工具时,需要从最新日志往前查,反向迭代器直接省了我写循环的功夫。
std::vector<int> v = {1, 2, 3, 4, 5};
for (auto it = v.rbegin(); it != v.rend(); ++it) {
std::cout << *it << " "; // 输出 5 4 3 2 1
}
插入迭代器——back_inserter、front_inserter、inserter。它们能把赋值操作变成插入操作。我建议你在用 std::copy 往空容器里填数据时,一定用插入迭代器,否则会越界。
std::vector<int> src = {1, 2, 3};
std::vector<int> dst;
std::copy(src.begin(), src.end(), std::back_inserter(dst)); // 安全!
避坑指南:我曾经直接用 dst.begin() 作为目标迭代器,结果程序崩溃了——因为 dst 是空的,根本没有空间可写。记住:目标容器要有足够空间,或者用插入迭代器。
std::algorithm 算法精讲:别重复造轮子
标准库算法大概有 100 多个,但常用的也就二三十个。我按用途把它们分了四类:
- 查找类:
find、find_if、search、binary_search - 排序类:
sort、stable_sort、partial_sort、nth_element - 修改类:
copy、transform、replace、fill - 数值类:
accumulate、inner_product、adjacent_difference
举个例子,nth_element 这个算法很多人不知道。它能把第 n 大的元素放到正确位置,左边的都比它小,右边的都比它大——但两边不排序。我在做性能分析时,需要找中位数,用 nth_element 比全排序快了一个数量级。
std::vector<int> data = {9, 3, 7, 1, 5, 8, 2, 6, 4};
auto mid = data.begin() + data.size() / 2;
std::nth_element(data.begin(), mid, data.end());
// 现在 *mid 就是中位数,左边都比它小,右边都比它大
std::cout << "中位数: " << *mid << std::endl;
个人经验:能用标准算法就别自己写循环。标准库算法经过了极致优化,而且代码意图更清晰。你想想看,std::find 一眼就知道是在查找,而手写 for 循环还得读一遍逻辑。
Lambda 表达式进阶:从入门到精通
Lambda 表达式是 C++11 引入的,但很多人只用到了皮毛。我见过不少代码把 Lambda 写得很长很乱,其实掌握几个关键点就够了。
捕获列表的讲究:
[=]按值捕获所有外部变量——小心拷贝开销[&]按引用捕获——注意生命周期[this]捕获当前对象指针——在成员函数里常用[x = std::move(y)]C++14 的初始化捕获——移动语义的好帮手
std::vector<std::string> names = {"Alice", "Bob", "Charlie"};
std::string prefix = "Mr.";
// 按值捕获 prefix,按引用捕获 names
std::for_each(names.begin(), names.end(),
[prefix](const std::string& name) {
std::cout << prefix << " " << name << std::endl;
});
泛型 Lambda(C++14):用 auto 参数让 Lambda 变成模板函数。我在写通用排序比较器时特别喜欢用这个。
auto comparator = [](const auto& a, const auto& b) {
return a.size() < b.size();
};
std::sort(strings.begin(), strings.end(), comparator);
注意:Lambda 默认是 const 的,如果你想修改按值捕获的变量,得加上 mutable 关键字。不过我个人建议少用 mutable,容易让代码逻辑变混乱。
范围库(Ranges):C++20 的现代写法
Ranges 库是 C++20 的重磅特性。它把「迭代器对」的概念升级成了「范围」的概念,配合管道操作符 |,代码写起来就像流水线一样流畅。
我刚开始接触 Ranges 时觉得有点别扭,但用了一个月后就回不去了。你想想看,以前写 std::sort(v.begin(), v.end()),现在直接 std::ranges::sort(v)——少写了多少重复代码。
#include <ranges>
#include <vector>
#include <iostream>
std::vector<int> v = {5, 2, 8, 1, 9, 3, 7, 4, 6};
// 传统写法
std::sort(v.begin(), v.end());
auto it = std::find_if(v.begin(), v.end(), [](int x) { return x > 5; });
// Ranges 写法
std::ranges::sort(v);
auto result = v | std::views::filter([](int x) { return x > 5; })
| std::views::transform([](int x) { return x * 2; });
for (int x : result) {
std::cout << x << " "; // 输出 12 14 16 18
}
视图(Views)是惰性求值的——这一点很重要。视图不会拷贝数据,也不会立即计算,只有在遍历时才真正执行。我在处理大数据集时,用视图链式操作,性能比手写循环还好。
避坑指南:视图不拥有数据,它只是数据的「窗口」。如果原始容器被销毁了,视图就变成了悬空引用。我曾经在函数返回视图时踩过这个坑——视图返回了,但容器已经析构了,程序直接崩溃。
知识体系总览
下面这张图把本章的核心知识点串起来了,你可以看到迭代器、算法、Lambda 和 Ranges 之间的关系。
从这张图你能看出来,迭代器是底层基础,算法库构建在迭代器之上,Lambda 让算法更灵活,而 Ranges 则把这一切提升到了新的抽象层次。我个人觉得,掌握了这条主线,C++ 标准库你就拿下了大半。
我的建议:先从传统迭代器和算法入手,写熟了再过渡到 Ranges。别一上来就追求最现代的写法,基础不牢,地动山摇。
公众号:蓝海资料掘金营,微信deep3321