1. 引言:STL容器适配器概念
在C++ STL(标准模板库)中,stack和queue并不是独立的数据结构,它们被称为容器适配器(Container Adapter)。
适配器模式:将一个类的接口转换成客户希望的另一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
对于stack和queue而言,它们并没有直接管理内存或存储元素,而是通过封装另一个底层容器(如deque、list、vector)来实现其特定的接口。这种设计带来了高度的复用性:我们只需要提供符合要求的底层容器,stack和queue就能提供栈或队列的语义。
为什么需要容器适配器?
代码复用:直接利用现有容器(如
deque)的强大功能,无需重新实现内存管理、迭代器等复杂部分。接口约束:栈和队列只允许在特定位置操作(栈:栈顶;队列:队尾进,队头出)。适配器通过封装底层容器的通用接口,只对外暴露符合逻辑的特定接口,从而避免了误操作。
灵活性:开发者可以根据性能需求指定不同的底层容器。
2. Stack的深度剖析与模拟实现
2.1 Stack的核心特性与接口
栈(Stack)是一种后进先出(LIFO, Last In First Out)的数据结构。它只允许在容器的一端(称为栈顶)进行元素的添加(压栈)和移除(出栈)。
标准接口(基于SGI STL):
| 成员函数 | 功能描述 |
|---|---|
top() | 返回栈顶元素的引用(不删除元素) |
push(const T& val) | 将元素压入栈顶 |
pop() | 移除栈顶元素(无返回值) |
empty() | 判断栈是否为空 |
size() | 返回栈中元素个数 |
关键点:
pop()返回void而非元素值,这是为了异常安全(如果返回元素,拷贝构造时可能抛异常,导致元素已删除但未返回)。top()提供可修改的引用。
2.2 底层容器选择:为什么是deque?
在SGI STL的实现中,stack和queue的默认底层容器是deque(双端队列)。为什么选择deque而非vector或list?
对
deque的考量:push_back和pop_back高效:deque在尾部插入和删除是O(1)的,且不会像vector那样频繁发生内存重新分配和元素拷贝。随机访问能力:虽然栈不需要随机访问,但
deque提供此功能并不妨碍使用。内存效率:
deque是分段连续空间,相比list(每个节点有前后指针)更节省内存,且缓存局部性更好。
为什么不直接用
vector?vector在尾部操作也是O(1)均摊,但vector在push_back时,如果容量不足,会重新分配一块更大的内存,将所有元素拷贝过去,这在栈频繁操作时可能产生较大开销。而deque通过分段缓冲区避免了这种全局拷贝。不过,如果需要严格的连续内存布局(如与C API交互),可以指定
vector作为底层容器。
为什么不直接用
list?list在头尾操作也是O(1),但每个节点需要额外的指针开销(通常8或16字节),并且节点在内存中分散,缓存命中率低。对于栈这种只在尾部操作的结构,deque是更优选择。
2.3 Stack的模拟实现(模板类)
下面我们实现一个简化但功能完备的MyStack,使用deque作为默认底层容器,并允许用户指定其他容器。
cpp
#include <deque> #include <stdexcept> // for std::out_of_range namespace mystl { // 模板参数: T - 元素类型, Container - 底层容器类型,默认deque<T> template <typename T, typename Container = std::deque<T>> class MyStack { public: // 类型别名,方便使用 using value_type = typename Container::value_type; using size_type = typename Container::size_type; using reference = typename Container::reference; using const_reference = typename Container::const_reference; // 构造函数:默认使用底层容器的默认构造函数 MyStack() = default; // 允许通过已有的容器构造(可选) explicit MyStack(const Container& cont) : c(cont) {} explicit MyStack(Container&& cont) : c(std::move(cont)) {} // 核心接口 // 判断是否为空 bool empty() const { return c.empty(); } // 返回元素个数 size_type size() const { return c.size(); } // 返回栈顶元素(可修改) reference top() { // 栈顶是底层容器的末尾元素 if (empty()) { throw std::out_of_range("MyStack::top(): stack is empty"); } return c.back(); } // 返回栈顶元素(只读) const_reference top() const { if (empty()) { throw std::out_of_range("MyStack::top(): stack is empty"); } return c.back(); } // 压栈:在末尾添加元素 void push(const value_type& value) { c.push_back(value); } // 压栈:右值引用版本(移动语义) void push(value_type&& value) { c.push_back(std::move(value)); } // 出栈:移除末尾元素(不返回值) void pop() { if (empty()) { throw std::out_of_range("MyStack::pop(): stack is empty"); } c.pop_back(); } // 交换两个栈的内容 void swap(MyStack& other) noexcept(noexcept(c.swap(other.c))) { c.swap(other.c); } // 为了支持范围for或迭代器(通常栈不提供,但我们可以提供底层容器的访问,一般不推荐) // 这里为了调试或特殊需求,可以暴露底层容器的引用(谨慎使用) const Container& get_container() const { return c; } private: Container c; // 底层容器 }; // 比较运算符(通常使用底层容器的比较) template <typename T, typename Container> bool operator==(const MyStack<T, Container>& lhs, const MyStack<T, Container>& rhs) { return lhs.get_container() == rhs.get_container(); } template <typename T, typename Container> bool operator!=(const MyStack<T, Container>& lhs, const MyStack<T, Container>& rhs) { return !(lhs == rhs); } template <typename T, typename Container> bool operator<(const MyStack<T, Container>& lhs, const MyStack<T, Container>& rhs) { return lhs.get_container() < rhs.get_container(); } // ... 其他比较运算符类似实现 } // namespace mystl代码关键点解析:
模板参数:
Container的默认类型为std::deque<T>,体现了适配器模式的灵活性。底层成员:
Container c,所有操作都转发给c。top()与pop()分离:严格遵循STL设计,pop()不返回值,避免异常安全问题。异常安全:在
top()和pop()中对空栈进行检查并抛出异常,模拟标准库行为(标准库通常要求用户保证非空,这里为了安全性加入检查)。类型别名:使用
typename Container::xxx将底层容器的类型暴露出来,使得MyStack的使用者可以获取元素类型等。
2.4 性能分析与复杂度
所有操作均委托给底层容器,因此时间复杂度取决于底层容器:
push(): O(1) 均摊(deque尾部插入)pop(): O(1)top(): O(1)size(): O(1)(通常容器维护大小)empty(): O(1)
空间复杂度:O(N),N为元素个数。
3. Queue的深度剖析与模拟实现
3.1 Queue的核心特性与接口
队列(Queue)是一种先进先出(FIFO, First In First Out)的数据结构。它允许在队尾(back)添加元素,在队头(front)移除元素。
标准接口:
| 成员函数 | 功能描述 |
|---|---|
front() | 返回队头元素的引用 |
back() | 返回队尾元素的引用 |
push(const T& val) | 在队尾添加元素 |
pop() | 移除队头元素(无返回值) |
empty() | 判断队列是否为空 |
size() | 返回队列中元素个数 |
3.2 底层容器的限制
queue要求底层容器必须支持以下操作:
front()back()push_back()pop_front()size()empty()
在STL容器中,deque和list都满足这些要求,而vector不满足(不支持高效的pop_front()),因此默认底层容器也是deque。
3.3 Queue的模拟实现(模板类)
cpp
#include <deque> #include <stdexcept> namespace mystl { template <typename T, typename Container = std::deque<T>> class MyQueue { public: using value_type = typename Container::value_type; using size_type = typename Container::size_type; using reference = typename Container::reference; using const_reference = typename Container::const_reference; MyQueue() = default; explicit MyQueue(const Container& cont) : c(cont) {} explicit MyQueue(Container&& cont) : c(std::move(cont)) {} bool empty() const { return c.empty(); } size_type size() const { return c.size(); } // 返回队头元素 reference front() { if (empty()) throw std::out_of_range("MyQueue::front(): queue is empty"); return c.front(); } const_reference front() const { if (empty()) throw std::out_of_range("MyQueue::front(): queue is empty"); return c.front(); } // 返回队尾元素 reference back() { if (empty()) throw std::out_of_range("MyQueue::back(): queue is empty"); return c.back(); } const_reference back() const { if (empty()) throw std::out_of_range("MyQueue::back(): queue is empty"); return c.back(); } // 入队:在尾部添加 void push(const value_type& value) { c.push_back(value); } void push(value_type&& value) { c.push_back(std::move(value)); } // 出队:移除头部元素 void pop() { if (empty()) throw std::out_of_range("MyQueue::pop(): queue is empty"); c.pop_front(); } void swap(MyQueue& other) noexcept(noexcept(c.swap(other.c))) { c.swap(other.c); } const Container& get_container() const { return c; } private: Container c; }; // 比较运算符类似Stack实现 template <typename T, typename Container> bool operator==(const MyQueue<T, Container>& lhs, const MyQueue<T, Container>& rhs) { return lhs.get_container() == rhs.get_container(); } // ... } // namespace mystl3.4 性能分析与复杂度
push(): O(1)pop(): O(1)(deque和list头部删除均为O(1))front()/back(): O(1)size()/empty(): O(1)
4. 底层容器deque的深度解析
要真正理解stack和queue的默认行为,必须深入剖析deque(双端队列)的实现原理。deque是最复杂的STL容器之一,它提供了在两端进行O(1)插入删除的能力,同时支持随机访问。
4.1deque的设计哲学:分段连续空间
vector是连续的线性空间,list是分散的节点。deque则采用了一种折中方案:分段连续空间。
它由一段一段的连续缓冲区(buffer)组成。
这些缓冲区通过一个中央控制器(map)来管理。
从用户角度看,
deque像一个连续的数组,支持[]操作符,但实际上元素可能分布在多个不连续的缓冲区中。
这种设计带来了以下优点:
两端插入删除高效:当在头部插入时,如果第一个缓冲区前面有空间,则直接使用;否则,在头部新增一个缓冲区。
无需整体搬迁:与
vector不同,deque在扩容时不会将所有元素拷贝到新内存,只需在map中增加新的缓冲区即可。内存利用率较高:比
list节省指针开销,比vector避免了预留大量内存。
4.2 中央控制器的实现原理
deque的核心是一个名为map的数组(注意:不是std::map),它是一个指针数组,每个指针指向一个缓冲区(buffer)。
cpp
// 伪代码示意 template <typename T> class deque { private: T** map; // 指向缓冲区的指针数组 size_t map_size; // map的大小 // 迭代器需要知道当前缓冲区、当前位置、以及map中的位置 // ... // 缓冲区大小通常是一个固定值(如512字节 / sizeof(T)) // 或者由编译器决定 };关键点:
缓冲区大小:通常是一个固定值,比如
sizeof(T) <= 256 ? 4096 / sizeof(T) : 16,保证缓冲区不会太大也不会太小。迭代器:
deque的迭代器不是普通的指针,而是一个封装了当前节点、当前缓冲区、以及map中位置的复杂对象。
4.3 迭代器的设计(跨越缓冲区)
deque的迭代器需要实现以下功能:
解引用:返回当前缓冲区中当前位置的元素。
自增/自减:当到达当前缓冲区的末尾(或开头)时,需要跳转到下一个(或上一个)缓冲区。
迭代器结构(简化):
cpp
template <typename T> struct _Deque_iterator { T* cur; // 当前缓冲区中的当前元素 T* first; // 当前缓冲区的起始位置 T* last; // 当前缓冲区的结束位置(最后一个元素的下一个位置) T** node; // 指向map中当前缓冲区指针的指针 // 自增操作 void operator++() { ++cur; if (cur == last) { // 当前缓冲区结束 set_node(node + 1); // 切换到下一个缓冲区 cur = first; } } void set_node(T** new_node) { node = new_node; first = *new_node; last = first + buffer_size(); } // 其他操作... };这种复杂的迭代器设计使得deque的随机访问(operator[])比vector慢一些,因为它需要计算元素位于哪个缓冲区以及偏移量。
4.4deque与vector、list的对比
| 特性 | deque | vector | list |
|---|---|---|---|
| 内存布局 | 分段连续 | 连续 | 非连续节点 |
| 头部插入/删除 | O(1) | O(N) | O(1) |
| 尾部插入/删除 | O(1) | O(1) 均摊 | O(1) |
| 随机访问 | O(1)(但常数较大) | O(1) | O(N) |
| 迭代器类型 | 随机访问迭代器 | 随机访问迭代器 | 双向迭代器 |
| 内存开销 | 低(除缓冲区外,有map开销) | 低(无额外指针) | 高(每个节点两个指针) |
| 重新分配 | 添加缓冲区,不移动元素 | 整体搬迁,可能移动元素 | 不重新分配,只分配节点 |
| 缓存局部性 | 较好(连续缓冲区) | 最好 | 差 |
结论:对于stack(只需尾部操作)和queue(需头尾操作),deque在性能、内存和灵活性上达到了最佳平衡。
5. 扩展与优化
5.1 指定底层容器
在STL和我们的实现中,都可以指定底层容器。例如:
cpp
// 使用vector作为栈的底层容器(注意vector不支持pop_front,所以不能用于queue) mystl::MyStack<int, std::vector<int>> vec_stack; // 使用list作为队列的底层容器 mystl::MyQueue<int, std::list<int>> list_queue;
注意:如果指定的容器不满足接口要求,编译时会报错(例如vector用于queue时缺少pop_front)。
5.2 线程安全问题探讨
STL容器(包括stack和queue)的成员函数不是线程安全的。
多线程只读:多个线程同时调用
const成员函数(如top()、empty()、size())通常是安全的,前提是没有线程在修改。多线程读写:如果有任何线程在修改容器(如
push()、pop()),则必须使用外部同步机制(如互斥锁)。
常见实践:
cpp
std::stack<int> s; std::mutex mtx; void thread_safe_push(int val) { std::lock_guard<std::mutex> lock(mtx); s.push(val); }C++标准库提供了std::stack和std::queue,但没有提供线程安全版本。
5.3 实际应用场景
stack的应用:
函数调用栈(递归实现)
表达式求值(中缀转后缀)
括号匹配
深度优先搜索(DFS)
queue的应用:
任务调度队列
广度优先搜索(BFS)
消息队列(生产者-消费者模式)
缓冲池
6. 总结
本文深入探讨了STL中stack和queue的设计与实现,主要内容包括:
容器适配器概念:理解
stack和queue如何通过封装底层容器来提供特定的接口。模拟实现:手写了
MyStack和MyQueue,展示了模板编程、异常安全、接口设计等要点。底层容器解析:详细剖析了
deque的分段连续存储结构、中央控制器map、迭代器实现,以及它为何成为默认底层容器。性能对比:对比了
deque与vector、list在栈和队列场景下的优劣。扩展知识:讨论了指定底层容器、线程安全、实际应用等话题。