适配器详解:函数适配器、容器适配器、迭代器适配器
适配器这东西,说白了就是「包装」。你有一个现成的组件,但接口不太对胃口,或者功能差那么一点点。适配器就是中间那层胶水,帮你把不匹配的地方抹平。我在项目中经常遇到这种情况——标准库里的东西很好用,但直接拿来用总有点别扭,这时候适配器就派上用场了。
STL 里的适配器分三类:函数适配器、容器适配器、迭代器适配器。咱们一个一个聊。
函数适配器:bind 和 not_fn
函数适配器,就是用来调整函数调用方式的工具。C++11 之后,std::bind 成了主力,C++17 又加了 std::not_fn。这两个东西,我几乎每天都在用。
std::bind:绑定参数,生成新函数
std::bind 可以把一个函数的某些参数固定下来,生成一个新的可调用对象。举个例子:
#include <iostream>
#include <functional>
void print(int a, int b, int c) {
std::cout << a << ", " << b << ", " << c << std::endl;
}
int main() {
// 绑定第一个参数为 10,第二个参数为 20
auto f = std::bind(print, 10, 20, std::placeholders::_1);
f(30); // 输出:10, 20, 30
return 0;
}
这里 std::placeholders::_1 是个占位符,表示调用时传入的第一个参数会填到这个位置。你可以用 _1、_2、_3 等等,对应第几个参数。
我的习惯:我一般用 std::bind 来适配回调函数。比如某个库要求回调是 void(int),但我手里有个 void(int, int) 的函数,用 bind 把第二个参数固定住就行了。省得再写一个 lambda。
std::not_fn:取反谓词
std::not_fn 是 C++17 引入的,用来生成一个谓词的「非」。以前你得用 std::not1、std::not2,但那俩东西用起来很麻烦,还得定义 argument_type。现在简单了:
#include <iostream>
#include <functional>
#include <vector>
#include <algorithm>
bool is_even(int n) { return n % 2 == 0; }
int main() {
std::vector<int> v = {1, 2, 3, 4, 5, 6};
// 找出第一个奇数
auto it = std::find_if(v.begin(), v.end(), std::not_fn(is_even));
if (it != v.end()) {
std::cout << "第一个奇数是: " << *it << std::endl;
}
return 0;
}
std::not_fn(is_even) 返回一个可调用对象,效果等同于 !is_even(x)。简洁、直观。
我曾经踩过的坑:std::not_fn 返回的对象是 const 可调用的,但如果你传入的谓词有状态(比如捕获了变量的 lambda),要注意生命周期问题。别让 lambda 引用的临时变量提前销毁了。
容器适配器:stack、queue、priority_queue
容器适配器,本质上是对底层容器的接口做了一层封装。你想想看,std::stack 底层可以用 std::deque、std::vector 甚至 std::list 来实现,但暴露给用户的只有 push、pop、top 这几个操作。这就是适配器的精髓——隐藏细节,简化接口。
std::stack:后进先出
栈是最简单的容器适配器。默认底层容器是 std::deque,但你可以换成 std::vector 或 std::list。
#include <iostream>
#include <stack>
#include <vector>
int main() {
// 使用 vector 作为底层容器
std::stack<int, std::vector<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;
}
我个人习惯用 std::deque 作为默认底层容器,因为它在两端操作都很快。但如果你确定只需要在一端操作,std::vector 的性能其实更好。
std::queue:先进先出
队列和栈类似,但它是先进先出的。默认底层容器也是 std::deque。
#include <iostream>
#include <queue>
int main() {
std::queue<int> q;
q.push(10);
q.push(20);
q.push(30);
while (!q.empty()) {
std::cout << q.front() << " "; // 输出:10 20 30
q.pop();
}
return 0;
}
注意,std::queue 不能用 std::vector 作为底层容器,因为 std::vector 没有 pop_front 操作。你只能用 std::deque 或 std::list。
std::priority_queue:优先级队列
优先级队列,说白了就是「最大堆」或「最小堆」。默认是最大堆,即 top() 返回最大元素。
#include <iostream>
#include <queue>
#include <vector>
int main() {
// 默认是最大堆
std::priority_queue<int> pq;
pq.push(30);
pq.push(10);
pq.push(50);
pq.push(20);
while (!pq.empty()) {
std::cout << pq.top() << " "; // 输出:50 30 20 10
pq.pop();
}
return 0;
}
如果你想用最小堆,可以指定比较器:
std::priority_queue<int, std::vector<int>, std::greater<int>> min_pq;
避坑指南:std::priority_queue 的底层容器默认是 std::vector。如果你要频繁插入大量元素,可以考虑用 std::deque,因为 std::vector 在扩容时会拷贝所有元素,性能开销较大。我曾经在一个实时系统中吃过这个亏,后来换成 std::deque 就稳了。
迭代器适配器
迭代器适配器,是对迭代器的行为做调整。STL 提供了几种常用的迭代器适配器:反向迭代器、插入迭代器、流迭代器。
反向迭代器
反向迭代器让容器可以从尾部向前遍历。所有标准容器都提供了 rbegin() 和 rend() 方法。
#include <iostream>
#include <vector>
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() 对应的是 end(),rend() 对应的是 begin()。别搞反了。
插入迭代器
插入迭代器可以把赋值操作变成插入操作。有三种:back_inserter、front_inserter、inserter。
#include <iostream>
#include <vector>
#include <iterator>
#include <algorithm>
int main() {
std::vector<int> src = {1, 2, 3};
std::vector<int> dst;
// 把 src 的元素插入到 dst 的末尾
std::copy(src.begin(), src.end(), std::back_inserter(dst));
for (int x : dst) {
std::cout << x << " "; // 输出:1 2 3
}
return 0;
}
插入迭代器在配合 std::copy、std::transform 等算法时特别有用。你想想看,如果没有插入迭代器,你得先手动 resize 目标容器,或者用 push_back 循环。有了它,一行代码搞定。
流迭代器
流迭代器可以把输入输出流当作迭代器来用。比如从标准输入读取整数:
#include <iostream>
#include <vector>
#include <iterator>
#include <algorithm>
int main() {
std::vector<int> v;
std::cout << "请输入一些整数(Ctrl+D 结束):" << std::endl;
std::copy(std::istream_iterator<int>(std::cin),
std::istream_iterator<int>(),
std::back_inserter(v));
for (int x : v) {
std::cout << x << " ";
}
return 0;
}
流迭代器在写一些快速原型或者小工具时特别方便。但我在生产代码中很少用,因为错误处理比较麻烦。比如输入格式不对,流迭代器就直接结束了,你很难知道哪里出了问题。
知识体系总览
下面这张图把适配器的分类和关系梳理清楚了:
总结
适配器是 STL 里非常实用的一类组件。函数适配器帮你调整函数签名,容器适配器帮你简化数据结构的使用,迭代器适配器帮你改变遍历方式。我在实际项目中,几乎每个项目都会用到至少其中一种。
记住一点:适配器不是为了炫技,而是为了让代码更简洁、更安全。如果你发现自己写了很多重复的胶水代码,不妨想想——是不是可以用适配器来简化?
核心要点回顾:
std::bind绑定参数,std::not_fn取反谓词std::stack后进先出,std::queue先进先出,std::priority_queue按优先级出队- 反向迭代器、插入迭代器、流迭代器各有适用场景
- 适配器的底层容器可以替换,但要注意接口约束