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_inserter、front_inserter、inserter,自动调用容器的插入操作 - 流迭代器:
istream_iterator、ostream_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才能这么灵活。
嗯,到这里,迭代器和仿函数的核心内容就讲完了。你可能会问:lambda表达式是不是可以替代仿函数?没错,C++11之后,很多场景下lambda确实更方便。但仿函数在需要携带复杂状态、或者需要多次复用时,依然有它的优势。两者不冲突,看场景选就行。
记住一句话:迭代器让你“怎么走”,仿函数让你“做什么”。把这两个搞明白,STL算法你就掌握了一半。