24、适配器:容器适配器(stack、queue)、迭代器适配器(reverse_iterator、back_insert_iterator等)、函数适配器(bind、negator)
适配器,说白了就是「包装」。它不自己造轮子,而是把现有的组件拿来,套上一层新的接口,让你用起来更顺手。我刚开始学STL时,总觉得适配器是个可有可无的东西——直到有一次写代码,发现标准容器缺了某个功能,适配器正好补上了这个缺口。
适配器分三类:容器适配器、迭代器适配器、函数适配器。咱们一个一个聊。
24.1 容器适配器:stack 与 queue
容器适配器,底层其实是个容器,但对外暴露的接口被限制了。比如 stack,它底层默认用 deque,但你只能从顶部操作元素。为什么要这么干?因为有些场景下,你根本不需要随机访问,限制接口反而能避免误操作。
24.1.1 stack:后进先出
stack 的接口非常少:push、pop、top、empty、size。就这些,没了。你想想看,如果你用 vector 模拟栈,很容易写出 vec[0] 这种越界访问。用 stack 就安全得多。
#include <stack>
#include <iostream>
int main() {
std::stack<int> s;
s.push(10);
s.push(20);
s.push(30);
while (!s.empty()) {
std::cout << s.top() << " "; // 输出:30 20 10
s.pop();
}
return 0;
}
我个人习惯用 stack 做括号匹配、表达式求值。记得有一次写一个简单的计算器,用 stack 处理运算符优先级,代码干净利落,比手写数组栈舒服多了。
stack 底层默认是 deque,但你可以换成 vector 或 list。比如 std::stack<int, std::vector<int>>。不过我个人建议保持默认,除非你有特殊需求。
24.1.2 queue:先进先出
queue 和 stack 类似,但它是先进先出。接口有 push、pop、front、back、empty、size。注意,queue 没有 top,而是用 front 和 back 分别访问队首和队尾。
#include <queue>
#include <iostream>
int main() {
std::queue<int> q;
q.push(1);
q.push(2);
q.push(3);
while (!q.empty()) {
std::cout << q.front() << " "; // 输出:1 2 3
q.pop();
}
return 0;
}
我在项目中用 queue 做过任务调度。生产者往队列里塞任务,消费者从队列里取任务。用 queue 天然就是 FIFO,不用自己维护顺序。嗯,这里要注意:多线程环境下要加锁,queue 本身不是线程安全的。
queue 为空时调用 front(),结果程序直接崩溃。记住:front() 和 back() 不会检查空队列,调用前务必用 empty() 确认。
24.2 迭代器适配器:让迭代器更灵活
迭代器适配器,说白了就是给迭代器「加功能」。标准迭代器只能单向或双向移动,但适配器可以让你反向遍历、自动插入元素等。
24.2.1 reverse_iterator:反向遍历
reverse_iterator 把正向迭代器包装成反向的。你想想看,如果你有一个 vector,想从尾到头遍历,用 rbegin() 和 rend() 就行了。
#include <vector>
#include <iostream>
int main() {
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
}
return 0;
}
这里有个细节:rbegin() 指向最后一个元素,rend() 指向第一个元素的前一个位置。别搞反了。我曾经在面试题里见过有人把 rbegin 和 rend 用反,结果遍历出来是空的。
24.2.2 back_insert_iterator:自动尾部插入
back_insert_iterator 是个好东西。它让你用 std::copy 这类算法时,自动往容器尾部插入元素,而不是覆盖已有元素。
#include <vector>
#include <iterator>
#include <algorithm>
#include <iostream>
int main() {
std::vector<int> src = {1, 2, 3};
std::vector<int> dst = {10, 20};
std::copy(src.begin(), src.end(), std::back_inserter(dst));
// dst 现在是 {10, 20, 1, 2, 3}
for (int x : dst) std::cout << x << " ";
return 0;
}
如果没有 back_inserter,std::copy 会覆盖 dst 的前三个元素,结果变成 {1, 2, 3}。用 back_inserter 就自动扩容了。我个人习惯在需要动态追加数据时,优先用 back_inserter。
reverse_iterator 让正向变反向,back_insert_iterator 让赋值变插入。
24.2.3 其他迭代器适配器
- front_insert_iterator:类似
back_insert_iterator,但插入到容器头部。注意,只有deque和list支持头部插入,vector不行。 - insert_iterator:可以指定插入位置。比如
std::inserter(c, c.begin())就相当于front_inserter。 - move_iterator:把元素移动而不是拷贝。C++11 引入,适合转移资源所有权。
24.3 函数适配器:调整函数行为
函数适配器,说白了就是「改函数」。比如你想让一个函数取反,或者绑定某个参数,函数适配器就能干这个。
24.3.1 bind:绑定参数
std::bind 可以把函数的某些参数固定下来,生成一个新的可调用对象。这在回调函数、算法中特别有用。
#include <functional>
#include <iostream>
void print(int a, int b) {
std::cout << a << ", " << b << std::endl;
}
int main() {
auto f = std::bind(print, 10, std::placeholders::_1);
f(20); // 输出:10, 20
return 0;
}
这里 std::placeholders::_1 是个占位符,表示调用 f 时传入的第一个参数。你可以用 _1、_2 等占位多个参数。
我在项目中用 bind 做过事件回调。比如某个库要求回调函数是 void(int),但我的处理函数需要两个参数。用 bind 绑定一个固定参数,就完美适配了。
std::bind 逐渐被 lambda 表达式取代。我个人也更推荐用 lambda,因为更直观。但 bind 在泛型编程中仍有其价值,比如配合 std::function 做类型擦除。
24.3.2 negator:取反
std::not1 和 std::not2 可以对一元谓词和二元谓词取反。不过它们要求谓词继承 std::unary_function 或 std::binary_function,这在现代 C++ 中已经很少用了。C++17 之后,std::not_fn 是更好的选择。
#include <functional>
#include <vector>
#include <algorithm>
#include <iostream>
bool is_even(int n) { return n % 2 == 0; }
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
auto it = std::find_if(v.begin(), v.end(), std::not_fn(is_even));
// 找到第一个奇数,即 1
std::cout << *it << std::endl;
return 0;
}
std::not_fn 返回一个函数对象,对原谓词取反。它不需要继承任何基类,用起来更灵活。嗯,这里要注意:not_fn 是 C++17 引入的,如果你的编译器不支持,可以用 lambda 替代。
24.4 适配器知识结构图
下面这张图帮你理清适配器的分类和关系:
24.5 总结
适配器是 STL 中「小而美」的设计。它不引入新数据结构,只是对现有组件做了一层包装。容器适配器限制了接口,让你用得更安全;迭代器适配器改变了遍历方式,让你用得更灵活;函数适配器调整了函数行为,让你用得更顺手。
我个人觉得,适配器最妙的地方在于:它让你在不改变底层实现的前提下,获得全新的使用体验。你想想看,stack 底层就是个 deque,但用起来完全不像 deque。这就是适配器的魅力。
公众号:蓝海资料掘金营,微信deep3321