一、适配器模式到底是个啥?
先说人话:适配器模式就是把一个已经有的东西,包装成另一个你想要的东西,中间不做复杂改造,只做"接口转换"。
打个比方------你家墙上有个三孔插座,但你手头只有两孔插头。怎么办?你掏出一个转换头(适配器)插上去,问题解决。转换头没发明电,也没改造你的插头,它只是把"三孔"这个接口适配成了"两孔"。
在 C++ 里,STL 提供了六大组件,其中**容器(Container)**你已经很熟了------vector、list、deque,各有各的接口。vector 能 push_back、pop_back;list 能 push_back、push_front、pop_back、pop_front。
但有时候你不需要这么丰富的接口,你只想用"栈"或者"队列"这种受限的数据结构。栈只能在一端操作,队列只能一头进一头出。怎么办?重新从零写一个?太蠢了。直接拿现有的容器包一层,只暴露你需要的接口,这就是适配器模式。
STL 里的 stack 和 queue 就是这么来的------它们本身不是"真正的容器",而是容器适配器(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_back 和 pop_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_back、pop_back、back 这三个操作就能用。
这就是适配器模式的精髓------不关心底层容器是谁,只要你符合我需要的"接口约定",就能上岗。
有同学可能会问:为什么不用 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_back 和 pop_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;
}
运行结果:

五、总结
回顾一下我们做了什么:
- 适配器模式:不是从零造轮子,而是在已有轮子外面套一层壳,把复杂接口转换成你需要的简单接口。
- stack :限制为只能在一端操作。内部用 vector(或 deque)的
push_back/pop_back/back三个函数就能实现。 - queue :限制为一头进一头出。内部用 list(或 deque)的
push_back/pop_front/front就能实现。 - 模板参数的妙用 :第二个模板参数
Container让底层容器可以自由替换------只要新容器支持对应的几个操作就行。这就是"面向接口编程"的思想。
说白了,适配器模式的本质就是 "做减法" 。vector 和 list 本身能力很强,前后都能操作,随机访问也不在话下。但有时候能力太强反而是负担------你给用户一个 vector,他可能忍不住在中间 insert,破坏了栈的语义。适配器模式帮你去掉不需要的能力,只暴露最合适的接口,用起来反而更安全、更清晰。
下次当你需要"受限数据结构"的时候,想想适配器模式------大概率你不需要从头写,找个现成的容器包一层就够了
