23、STL迭代器与仿函数:迭代器分类、迭代器适配器、仿函数(函数对象)、谓词

好,咱们今天聊点硬核的——迭代器和仿函数。这两个东西,说白了就是STL的“胶水”和“灵魂”。

我刚开始学C++那会儿,总觉得迭代器不就是指针的升级版吗?仿函数不就是重载了括号的类吗?嗯,这么理解也没错,但太浅了。等你真正在项目里写泛型算法、搞自定义排序、做条件过滤的时候,才会发现——不懂迭代器分类和仿函数,你连std::sort都用不顺手。

23.1 迭代器分类:不只是指针那么简单

迭代器,本质上是算法和容器之间的桥梁。但不同的容器,内部结构不一样,能支持的操作也不一样。所以STL把迭代器分成了五类,每一类都有自己的“能力范围”。

迭代器类别 能力 典型容器
输入迭代器 只读,单向,一次遍历 istream_iterator
输出迭代器 只写,单向,一次遍历 ostream_iterator
前向迭代器 读写,单向,可多次遍历 forward_list
双向迭代器 读写,双向 list, set, map
随机访问迭代器 读写,双向,支持跳跃 vector, deque, array

你想想看,vector的迭代器支持it + 5这种操作,但list的迭代器就不行。为什么?因为list是链表,内存不连续,没法直接算偏移量。这就是迭代器分类存在的意义——算法通过迭代器类别来决定自己能做什么。

核心原则:算法只要求它需要的“最低能力”迭代器。比如std::find只需要输入迭代器,而std::sort必须要求随机访问迭代器。

我在项目中遇到过一个问题:用std::sort去排std::list,编译直接报错。当时我还纳闷,后来一看文档——list的迭代器是双向的,不支持随机访问。解决方案是用list::sort()成员函数。嗯,这就是不懂分类吃的亏。

23.2 迭代器适配器:给迭代器“加Buff”

迭代器适配器,说白了就是在原有迭代器的基础上,包装一层,提供新的行为。STL里常用的有这么几种:

  • 反向迭代器rbegin() / rend(),从尾到头遍历
  • 插入迭代器back_inserterfront_inserterinserter,自动调用容器的插入操作
  • 流迭代器istream_iteratorostream_iterator,把输入输出流当成容器来用

我个人最常用的是back_inserter。比如你要把一个vector里的数据拷贝到另一个vector后面,直接用std::copy配合back_inserter,一行搞定:

#include <iostream>
#include <vector>
#include <iterator>
#include <algorithm>

int main() {
    std::vector<int> src = {1, 2, 3, 4, 5};
    std::vector<int> dst;

    // 不用back_inserter的话,你得先resize,或者手动push_back
    std::copy(src.begin(), src.end(), std::back_inserter(dst));

    for (int v : dst) std::cout << v << " ";
    return 0;
}

我曾经在写日志模块时,用ostream_iterator直接把容器内容输出到文件流,配合std::copy,代码简洁到让同事直呼“优雅”。

23.3 仿函数(函数对象):把函数“装进”对象里

仿函数,也叫函数对象,就是重载了operator()的类。为什么不用普通函数?因为仿函数可以携带状态,而且可以被STL算法模板化地接受。

说白了,普通函数是“死”的,仿函数是“活”的。你可以给它成员变量,每次调用时状态都不一样。

#include <iostream>

class Adder {
private:
    int base;
public:
    explicit Adder(int b) : base(b) {}
    int operator()(int x) const {
        return base + x;
    }
};

int main() {
    Adder add5(5);
    std::cout << add5(10) << std::endl;  // 输出15
    std::cout << add5(20) << std::endl;  // 输出25
    return 0;
}

你看,add5这个对象“记住”了base = 5,每次调用都基于这个状态计算。普通函数做不到这一点,除非你用全局变量——但那太丑了。

小技巧:STL里预定义了很多仿函数,比如std::plus<T>std::greater<T>std::logical_and<T>等。在<functional>头文件里,直接用就行。

23.4 谓词:返回bool的仿函数

谓词,就是返回bool的仿函数。它通常用在条件判断、筛选、排序等场景里。

谓词分两种:

  • 一元谓词:接受一个参数,比如判断一个数是不是偶数
  • 二元谓词:接受两个参数,比如比较两个数的大小

举个例子,你想从vector里找出所有大于10的元素:

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

class GreaterThan {
private:
    int threshold;
public:
    explicit GreaterThan(int t) : threshold(t) {}
    bool operator()(int x) const {
        return x > threshold;
    }
};

int main() {
    std::vector<int> data = {5, 12, 3, 18, 7, 20};
    auto it = std::find_if(data.begin(), data.end(), GreaterThan(10));
    if (it != data.end()) {
        std::cout << "第一个大于10的元素是: " << *it << std::endl;
    }
    return 0;
}

这里GreaterThan(10)就是一个一元谓词。它“记住”了阈值10,然后find_if内部对每个元素调用它,直到返回true

注意:谓词的调用次数是不确定的。比如std::find_if找到目标就停,但std::count_if会遍历整个范围。所以谓词里不要有副作用,比如修改全局变量——否则结果可能让你抓狂。

我曾经在写一个过滤模块时,在谓词里偷偷改了外部计数器的值,结果多线程环境下数据全乱了。从那以后,我写谓词都加const,确保它是个“纯函数”。

23.5 知识体系总览

下面这张图,把本章的核心逻辑串起来了。你一看就明白:迭代器是算法和容器的桥梁,仿函数是算法的“自定义逻辑”,两者配合,STL才能这么灵活。

STL迭代器与仿函数知识体系 STL泛型编程 迭代器(Iterator) 输入/输出迭代器 前向迭代器 双向迭代器 随机访问迭代器 适配器:反向/插入/流 仿函数(Functor) 算术仿函数 关系仿函数 逻辑仿函数 谓词(返回bool) 迭代器连接容器与算法,仿函数提供自定义逻辑,两者结合实现泛型编程

嗯,到这里,迭代器和仿函数的核心内容就讲完了。你可能会问:lambda表达式是不是可以替代仿函数?没错,C++11之后,很多场景下lambda确实更方便。但仿函数在需要携带复杂状态、或者需要多次复用时,依然有它的优势。两者不冲突,看场景选就行。

记住一句话:迭代器让你“怎么走”,仿函数让你“做什么”。把这两个搞明白,STL算法你就掌握了一半。

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