C++:吃透容器适配器
大家好,这里是彩妙呀~

本篇主要讲解的内容是容器适配器,为之后的三种STL容器做铺垫(stack,queue与priority_queue)。下面我们就开始吧~
什么是容器适配器?
什么是适配器
谈什么是容器适配器之前,我们先来谈谈什么是适配器:
在我们日常生活中,这东西可太常见了。比方说你从国外买了个电器,插头是俩圆脚的,家里插座是扁孔的,插不进去咋办?找个转换插头,这就是适配器。
它没啥高科技,就是个“和事佬”,专门把俩不匹配的东西凑一块儿,让它们能好好配合工作。手机充电头也是,把家里220伏的电转成手机能吃的5伏,没有它,手机就只能干瞪眼。
说白了,适配器就是“转一下”,让原本不搭边的两样东西顺利牵手。

聊完生活中的适配器,咱们就能顺理成章聊容器适配器了——其实就是代码里的“转换插头”。
从适配器延伸到容器适配器
在 C++ 的世界里,这种适配器的名字叫 : 容器适配器。
C++ 中的基础容器,如vector、deque、list等,它们功能强大,提供了众多操作接口,就像一个功能齐全的多功能工具(从被另三个STL的容器适配器来使用了)。
但在很多实际场景中,我们并不需要所有这些功能,而是只需要特定的一组操作来实现特定的数据结构,比如栈(Stack)、队列(Queue)和优先级队列(Priority Queue)。而容器适配器正是为此诞生,它通过封装基础容器,屏蔽那些我们不需要的冗余接口,只暴露出特定数据结构所需要的操作,从而将通用的容器 “转换” 成专用的数据结构。这就好比给一个多功能工具加上特定的 “盖子”,只露出我们需要的功能,让它变成一个专用工具。
//用一个简短的代码来具体展示容器适配器的用法
#include <iostream>
#include <vector>
#include <deque>
#include <list>
// 容器适配器:底层容器现在是个模板参数,默认用 deque
template <typename T, typename Container = std::deque<T>>
class StackAdapter {
private:
Container data; // 底层容器,类型由你决定
public:
void push(const T& value) { data.push_back(value); }
void pop() { if (!data.empty()) data.pop_back(); }
T& top() { return data.back(); }
bool empty() const { return data.empty(); }
};
int main() {
// 默认底层:deque
StackAdapter<int> st1;
st1.push(10);
// 指定底层:vector
StackAdapter<int, std::vector<int>> st2;
st2.push(20);
// 指定底层:list
StackAdapter<int, std::list<int>> st3;
st3.push(30);
std::cout << st1.top() << ", " << st2.top() << ", " << st3.top() << std::endl;
return 0;
}
C++ 标准库三种主要的容器使用了容器适配器:stack(栈)、queue(队列)和priority_queue(优先级队列):
- stack 实现了后进先出(LIFO, Last In First Out)的数据结构,就像一摞盘子,最后放上去的盘子最先被拿走;
- queue 实现了先进先出(FIFO, First In First Out)的数据结构,类似排队,先排队的人先接受服务;
- priority_queue 则是优先级队列,元素按照优先级的高低进出队列,优先级高的元素先出队 。
底层容器的选择逻辑
适配器模式,其实就是一种结构型设计模式。它的核心说白了就是:把一个类的接口,转成客户端期望看到的另一个接口。这样一来,那些原本因为接口不匹配、根本没法一起工作的类,就能通过适配器顺利搭上线、配合起来了。
而这个模式在 C++ 里用得特别多。尤其是你写新代码的时候,想复用老代码或者接一个第三方库,结果人家接口风格完全对不上,那就不用硬改原代码,也不用推翻重写,直接拿适配器裹一层,两边都能舒舒服服工作。
在STL中(以 C++ 容器中使用容器适配器味为底层的容器为例),stack、queue和priority_queue这些容器适配器本质上是对基础容器(如deque、vector、list)的封装。
stack 和 queue 默认使用 deque 作为底层容器
下面源自官网的文档
template <class T, class Container = deque<T> > class stack;template <class T, class Container = deque<T> > class queue;
priority_queue默认使用 vector 作为底层容器
紫色部分是仿函数,在彩妙别的文章会将。
template <class T, class Container = vector<T>, class Compare = less<typename Container::value_type> > class priority_queue;而关于模版的知识,可以参考下面的文章:
c++:详解:模版
https://blog.csdn.net/weixin_66776566/article/details/156945543?spm=1001.2014.3001.5501
而有了容器适配器作为基底,那么我们实现这些容器的底层逻辑就简单起来了。比如 stack(栈),它遵循后进先出(LIFO)的原则,只需要push_back(入栈)、pop_back(出栈)和back(获取栈顶元素)等操作。
通过适配器模式,stack 将底层容器(默认是deque)的这些操作封装起来,屏蔽了insert(插入)、erase(删除)等不符合栈语义的操作,强制使用者只能按照栈的规则来访问数据 。
其余两个也是一样的,下图是stack主要的接口:
而deque接口如下图:
但是要注意:我们在使用容器适配器时,尽量不要使用“不对口”的容器做适配器(显然:list就不太适合当queue的适配器,效率太低。)。而且,尽量不要调用适配器不存在的接口(或者内部实现中返回值错误或传参错误)。
底层容器的选择逻辑
对于 stack 和queue 来说,deque(双端队列)是一个非常合适的选择。deque 具有在两端高效插入和删除元素的特性,其时间复杂度为 O (1)。
在stack中,元素的插入(入栈)和删除(出栈)只在栈顶进行,对应deque的push_back和pop_back操作。
在queue中,元素在队尾插入(入队),在队头删除(出队),对应deque的push_back和pop_front操作 。
此外,deque 在扩容时,不需要像 vector 那样挪动大量的元素(deque底层逻辑是结合了 list 与vector,但是也有明显劣势。),这使得在元素频繁增长的情况下,deque的效率更高。而且,stack和queue通常不需要对元素进行遍历操作(通常就是出入操作),所以它更适合作为stack和queue的底层容器(这就是deque的劣势:遍历/排序等操作效率不如vector,这也是为了适应 list ,底层实现逻辑不同导致的必然结果)。
priority_queue(优先级队列,他在数据结构中也有一个名字:堆)默认选用 vector 作为底层容器,主要是因为vector支持随机访问(满足堆要随机访问才能实现父子关系),这对于维护优先级队列底层的堆结构非常重要。
priority_queue的核心操作是插入元素和删除堆顶元素(即优先级最高的元素,可以根据后面的仿函数来改变优先级:最常见的就是大堆与小堆),在插入元素后,需要通过向上调整操作来维护堆的性质;在删除堆顶元素后,需要通过向下调整操作来重新调整堆。(就是数据结构中堆的基础知识)
这些操作都依赖于随机访问来快速定位和交换元素,而vector的连续内存布局和高效的随机访问特性,使得它能够很好地满足priority_queue的需求 ,保证插入删除操作的 O (log n) 复杂度。
小总结
容器适配器是C++标准库中一种特殊的容器实现,其本质是对已有序列容器(如vector、deque、list)进行接口封装与行为约束,提供一套精简、语义明确的操作集,以满足特定数据结构(栈、队列、优先队列)的抽象要求。
从实现层面看,容器适配器采用适配器设计模式,通过模板参数接收底层容器类型,并在其基础上重新定义公有接口——只对外开放符合数据结构语义的操作(如push、pop、top),屏蔽底层容器原有的其他通用接口。这种设计既复用了现有容器的存储与操作实现,又对外呈现出独立、专注的数据结构外观。
不难发现,容器适配器主要使用了has - a的方式来写代码的。本质上也只是整一个模版来套一个容器或自定义类来让我们使用(本质上也算是一种代码复用的技术)相关内容可以看看下面文章:
本篇博客主要是用来做小的知识点来使用,为之后讲解STL容器做铺垫与转链接,但喜欢的小伙伴也可以点点赞,关注彩妙,获取更多优质好文吧~

更多推荐


所有评论(0)