适配器详解:函数适配器、容器适配器、迭代器适配器

适配器这东西,说白了就是「包装」。你有一个现成的组件,但接口不太对胃口,或者功能差那么一点点。适配器就是中间那层胶水,帮你把不匹配的地方抹平。我在项目中经常遇到这种情况——标准库里的东西很好用,但直接拿来用总有点别扭,这时候适配器就派上用场了。

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::not1std::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::dequestd::vector 甚至 std::list 来实现,但暴露给用户的只有 pushpoptop 这几个操作。这就是适配器的精髓——隐藏细节,简化接口。

std::stack:后进先出

栈是最简单的容器适配器。默认底层容器是 std::deque,但你可以换成 std::vectorstd::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::dequestd::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_inserterfront_inserterinserter

#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::copystd::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 反向迭代器 插入迭代器 流迭代器 适配器本质:对现有组件进行接口封装,不改变底层实现 核心思想:复用 + 简化 + 隔离

总结

适配器是 STL 里非常实用的一类组件。函数适配器帮你调整函数签名,容器适配器帮你简化数据结构的使用,迭代器适配器帮你改变遍历方式。我在实际项目中,几乎每个项目都会用到至少其中一种。

记住一点:适配器不是为了炫技,而是为了让代码更简洁、更安全。如果你发现自己写了很多重复的胶水代码,不妨想想——是不是可以用适配器来简化?

核心要点回顾

  • std::bind 绑定参数,std::not_fn 取反谓词
  • std::stack 后进先出,std::queue 先进先出,std::priority_queue 按优先级出队
  • 反向迭代器、插入迭代器、流迭代器各有适用场景
  • 适配器的底层容器可以替换,但要注意接口约束
公众号:蓝海资料掘金营,微信deep3321