24、适配器:容器适配器(stack、queue)、迭代器适配器(reverse_iterator、back_insert_iterator等)、函数适配器(bind、negator)

适配器,说白了就是「包装」。它不自己造轮子,而是把现有的组件拿来,套上一层新的接口,让你用起来更顺手。我刚开始学STL时,总觉得适配器是个可有可无的东西——直到有一次写代码,发现标准容器缺了某个功能,适配器正好补上了这个缺口。

适配器分三类:容器适配器、迭代器适配器、函数适配器。咱们一个一个聊。

24.1 容器适配器:stack 与 queue

容器适配器,底层其实是个容器,但对外暴露的接口被限制了。比如 stack,它底层默认用 deque,但你只能从顶部操作元素。为什么要这么干?因为有些场景下,你根本不需要随机访问,限制接口反而能避免误操作。

24.1.1 stack:后进先出

stack 的接口非常少:pushpoptopemptysize。就这些,没了。你想想看,如果你用 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,但你可以换成 vectorlist。比如 std::stack<int, std::vector<int>>。不过我个人建议保持默认,除非你有特殊需求。

24.1.2 queue:先进先出

queuestack 类似,但它是先进先出。接口有 pushpopfrontbackemptysize。注意,queue 没有 top,而是用 frontback 分别访问队首和队尾。

#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() 指向第一个元素的前一个位置。别搞反了。我曾经在面试题里见过有人把 rbeginrend 用反,结果遍历出来是空的。

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_inserterstd::copy 会覆盖 dst 的前三个元素,结果变成 {1, 2, 3}。用 back_inserter 就自动扩容了。我个人习惯在需要动态追加数据时,优先用 back_inserter

核心要点:迭代器适配器不改变迭代器的本质,只是改变了它的行为。比如 reverse_iterator 让正向变反向,back_insert_iterator 让赋值变插入。

24.2.3 其他迭代器适配器

  • front_insert_iterator:类似 back_insert_iterator,但插入到容器头部。注意,只有 dequelist 支持头部插入,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 绑定一个固定参数,就完美适配了。

注意:C++11 之后,std::bind 逐渐被 lambda 表达式取代。我个人也更推荐用 lambda,因为更直观。但 bind 在泛型编程中仍有其价值,比如配合 std::function 做类型擦除。

24.3.2 negator:取反

std::not1std::not2 可以对一元谓词和二元谓词取反。不过它们要求谓词继承 std::unary_functionstd::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 适配器知识结构图

下面这张图帮你理清适配器的分类和关系:

STL 适配器分类 适配器 (Adapter) 容器适配器 迭代器适配器 函数适配器 stack (LIFO) queue (FIFO) priority_queue reverse_iterator back_insert_iterator front_insert_iterator move_iterator (C++11) bind (参数绑定) not_fn / not1 / not2 mem_fn (成员函数适配)

24.5 总结

适配器是 STL 中「小而美」的设计。它不引入新数据结构,只是对现有组件做了一层包装。容器适配器限制了接口,让你用得更安全;迭代器适配器改变了遍历方式,让你用得更灵活;函数适配器调整了函数行为,让你用得更顺手。

我个人觉得,适配器最妙的地方在于:它让你在不改变底层实现的前提下,获得全新的使用体验。你想想看,stack 底层就是个 deque,但用起来完全不像 deque。这就是适配器的魅力。

一句话总结:适配器 = 现有组件 + 新接口。理解了这个,你就掌握了 STL 适配器的精髓。

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