C++ 适配器模式:用 deque/list 适配出 stack 和 queue

一、适配器模式到底是个啥?

先说人话:适配器模式就是把一个已经有的东西,包装成另一个你想要的东西,中间不做复杂改造,只做"接口转换"。

打个比方------你家墙上有个三孔插座,但你手头只有两孔插头。怎么办?你掏出一个转换头(适配器)插上去,问题解决。转换头没发明电,也没改造你的插头,它只是把"三孔"这个接口适配成了"两孔"。

在 C++ 里,STL 提供了六大组件,其中**容器(Container)**你已经很熟了------vector、list、deque,各有各的接口。vector 能 push_back、pop_back;list 能 push_back、push_front、pop_back、pop_front。

但有时候你不需要这么丰富的接口,你只想用"栈"或者"队列"这种受限的数据结构。栈只能在一端操作,队列只能一头进一头出。怎么办?重新从零写一个?太蠢了。直接拿现有的容器包一层,只暴露你需要的接口,这就是适配器模式。

STL 里的 stackqueue 就是这么来的------它们本身不是"真正的容器",而是容器适配器(Container Adapter):里面藏着一个真正的容器,对外只暴露栈/队列该有的那几个函数。

二、先看 Stack ------ 用 vector 适配出栈

直接上代码,这是我从头文件中提取出来的精简版实现:

cpp 复制代码
#pragma once
#include <vector>
#include <deque>

namespace box
{
    // 用 Container 适配转换出 stack
    // 模板参数:T 是元素类型,Container 是底层容器,默认用 vector
    template<class T, class Container = std::vector<T> >
    class stack
    {
    public:
        void push(const T& x)
        {
            _con.push_back(x);    // 入栈 → 尾部插入
        }

        void pop()
        {
            _con.pop_back();      // 出栈 → 尾部弹出
        }

        const T& top() const
        {
            return _con.back();   // 栈顶 → 尾部元素
        }

        size_t size() const
        {
            return _con.size();   // 元素个数
        }

        bool empty() const
        {
            return _con.empty();  // 判空
        }

    private:
        Container _con;           // 底层容器,默认是 vector
    };
}

设计思路拆解

栈的核心特性是什么?后进先出(LIFO),所有操作都发生在同一端。

vector 有 push_backpop_back,天然支持"在同一端增删"。所以适配起来非常自然:

  • stack::push → 调用 _con.push_back,把元素塞到 vector 尾部
  • stack::pop → 调用 _con.pop_back,把 vector 尾部元素弹掉
  • stack::top → 返回 _con.back(),vector 尾部就是栈顶

size 和 empty 更简单,直接委托给底层容器就行,完全不用自己维护计数变量。

关键点: 第二个模板参数 Container 给了默认值 std::vector<T>。这意味着你正常声明 stack<int> 的时候,底层就是一个 vector<int>。但如果你想换成别的容器,比如 stack<int, std::deque<int> >,只要新容器支持 push_backpop_backback 这三个操作就能用。

这就是适配器模式的精髓------不关心底层容器是谁,只要你符合我需要的"接口约定",就能上岗。

有同学可能会问:为什么不用 list?list 当然也行,但 vector 在尾部操作的缓存命中率更好,而且 stack 不需要在头部插入,vector 的劣势发挥不出来。所以 STL 标准库里 std::stack 默认用的是 deque,而我们这个简化版用 vector,逻辑上完全说得通。

三、再看 Queue ------ 用 list 适配出队列

同样,直接上代码:

cpp 复制代码
#pragma once
#include <list>
#include <deque>

namespace box
{
    // 用 Container 适配转换出 queue
    // 默认底层容器是 list
    template<class T, class Container = std::list<T> >
    class queue
    {
    public:
        void push(const T& x)
        {
            _con.push_back(x);      // 入队 → 尾部插入
        }

        void pop()
        {
            _con.pop_front();       // 出队 → 头部弹出
        }

        const T& front() const
        {
            return _con.front();    // 队头 → 头部元素
        }

        size_t size() const
        {
            return _con.size();     // 元素个数
        }

        bool empty() const
        {
            return _con.empty();    // 判空
        }

    private:
        Container _con;             // 底层容器,默认是 list
    };
}

设计思路拆解

队列的核心特性是 FIFO(先进先出):一头进,另一头出。

这个需求比栈稍微苛刻一点------底层容器必须同时支持尾部插入和头部弹出 。vector 虽然能在尾部插入,但 pop_front 它没有(或者有但效率极低,需要把后面所有元素往前挪)。所以 vector 不适合。

list 就完美了------双向链表,push_backpop_front 都是 O(1) 的,天生适合做队列的底层。所以我们的 queue 默认用 std::list<T>

接口映射也很直观:

  • queue::push_con.push_back,总是从尾部入队
  • queue::pop_con.pop_front,总是从头部出队
  • queue::front_con.front(),看一眼队头元素

同样,size 和 empty 直接委托。

和 stack 的对比:

stack queue
数据进出 同端(后进先出) 异端(先进先出)
push 映射 push_back push_back
pop 映射 pop_back pop_front
访问顶端 top() → back() front() → front()
默认容器 vector(或 deque) list(或 deque)
对底层容器的要求 支持 push_back / pop_back / back 支持 push_back / pop_front / front

注意,STL 标准库中 std::queue 默认用的是 deque 而不是 list,因为 deque 的双端操作也很高效,且内存碎片更少。我们这里用 list 做默认值,更多是为了教学上的直观------list 的 pop_front 是典型操作,初学者更容易理解"队列出队就是干掉链表第一个节点"。

四、动手写个例子

光看不练假把式,写个小程序验证一下:

cpp 复制代码
​
int main()
{
	box::stack<int, vector<int>>st;
	st.push(1);
	st.push(2);
	st.push(3);
	st.push(4);
	cout << st.top() << endl;
	//st.pop();//按需实例化,用哪个接口才实例化哪个接口,其他接口细节不检查
	//box::queue<int, list<int>>q;//因为没有调用pop(),就没有报错
	box::queue<int>q;
	q.push(1);
	q.push(2);
	q.push(3);
	q.push(4);
	cout << q.front() << endl;
	q.pop();
	return 0;
}

​

运行结果:

五、总结

回顾一下我们做了什么:

  1. 适配器模式:不是从零造轮子,而是在已有轮子外面套一层壳,把复杂接口转换成你需要的简单接口。
  2. stack :限制为只能在一端操作。内部用 vector(或 deque)的 push_back/pop_back/back 三个函数就能实现。
  3. queue :限制为一头进一头出。内部用 list(或 deque)的 push_back/pop_front/front 就能实现。
  4. 模板参数的妙用 :第二个模板参数 Container 让底层容器可以自由替换------只要新容器支持对应的几个操作就行。这就是"面向接口编程"的思想。

说白了,适配器模式的本质就是 "做减法" 。vector 和 list 本身能力很强,前后都能操作,随机访问也不在话下。但有时候能力太强反而是负担------你给用户一个 vector,他可能忍不住在中间 insert,破坏了栈的语义。适配器模式帮你去掉不需要的能力,只暴露最合适的接口,用起来反而更安全、更清晰

下次当你需要"受限数据结构"的时候,想想适配器模式------大概率你不需要从头写,找个现成的容器包一层就够了