迭代器与算法库: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_inserterfront_inserterinserter。它们能把赋值操作变成插入操作。我建议你在用 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 多个,但常用的也就二三十个。我按用途把它们分了四类:

  • 查找类:findfind_ifsearchbinary_search
  • 排序类:sortstable_sortpartial_sortnth_element
  • 修改类:copytransformreplacefill
  • 数值类:accumulateinner_productadjacent_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 表达式(捕获、泛型、mutable) Ranges 库(视图、管道操作符、惰性求值) 迭代器是算法的桥梁,Lambda 是算法的灵魂,Ranges 是算法的现代化表达

从这张图你能看出来,迭代器是底层基础,算法库构建在迭代器之上,Lambda 让算法更灵活,而 Ranges 则把这一切提升到了新的抽象层次。我个人觉得,掌握了这条主线,C++ 标准库你就拿下了大半。

我的建议:先从传统迭代器和算法入手,写熟了再过渡到 Ranges。别一上来就追求最现代的写法,基础不牢,地动山摇。


公众号:蓝海资料掘金营,微信deep3321