往期内容回顾
C++ 容器适配器自实现(Stack & Queue)
前言
在掌握了 string、vector(连续内存)、list(节点容器)之后,我们再看 栈(Stack) 与 队列(Queue) 会更容易:它们在 STL 中并不是"容器",而是 容器适配器(Container Adapter) ------基于一个可用作底层的顺序容器,只暴露特定的接口与语义。
Stack(栈):后进先出(LIFO)
典型操作:push、pop、top
Queue(队列):先进先出(FIFO)
典型操作:push、pop(从头弹)、front、ba
标准库默认用 deque 作为底层,因为它的双端插删性能均衡。本文将从零实现 my_stack 与 my_queue,并解释设计取舍、异常安全、迭代器失效与可替换底层容器的约束。
主要内容
-
1、适配器思想与底层容器约束
-
2、my_stack:接口、约束、实现与测试
-
3、my_queue:接口、约束、实现与测试
-
4、复杂度、异常安全、迭代器/引用有效性
-
5、可替换底层容器:deque vs vector vs list
-
6、常见坑位与工程化建议
-
总结:为何"用组合而非继承"
一、适配器思想与底层容器约束
适配器 的核心:不自己管理存储,只组合一个"底层容器",并将接口受限为"栈语义/队列语义"。
这要求底层容器满足最小能力集合(接口约束):
-
Stack 底层容器需要:
back() / push_back() / pop_back() / empty() / size()
(典型可用:deque<T>、vector<T>、list<T>)
-
Queue 底层容器需要:
front() / back() / push_back() / pop_front() / empty() / size()
(典型可用:deque<T>、list<T>;vector<T> 不可用 ------ 没有 pop_front())
标准库 std::stack/std::queue 默认 deque<T>,也允许自定义容器,只要满足上述接口。
二、my_stack 的实现
2.1 设计要点
-
按标准库风格提供:top / push / emplace / pop / empty / size / swap
-
不提供迭代器与遍历接口(保持"栈语义"的束缚)
-
接口时间复杂度与异常安全以底层容器为准
-
采用组合(一个成员 Container _con;)
2.2 代码(C++14+ 版本,适配
deque/vector/list)
cpp#include <deque> #include <utility> #include <type_traits> template<class T, class Container = std::deque<T>> class my_stack { public: using value_type = T; using container_type = Container; using size_type = typename Container::size_type; using reference = typename Container::reference; using const_reference = typename Container::const_reference; my_stack() = default; explicit my_stack(const Container& cont) : c_(cont) {} explicit my_stack(Container&& cont) : c_(std::move(cont)) {} bool empty() const noexcept { return c_.empty(); } size_type size() const noexcept { return c_.size(); } reference top() { return c_.back(); } const_reference top() const { return c_.back(); } void push(const value_type& v) { c_.push_back(v); } void push(value_type&& v) { c_.push_back(std::move(v)); } template<class... Args> decltype(auto) emplace(Args&&... args) { return c_.emplace_back(std::forward<Args>(args)...); } void pop() { c_.pop_back(); } void swap(my_stack& other) noexcept(noexcept(std::swap(c_, other.c_))) { using std::swap; swap(c_, other.c_); } private: Container c_; };
2.3 快速测试
cpp#include <vector> #include <list> #include <iostream> int main() { my_stack<int> s1; // 默认 deque s1.push(1); s1.push(2); s1.push(3); std::cout << s1.top() << "\n"; // 3 s1.pop(); std::cout << s1.top() << "\n"; // 2 my_stack<int, std::vector<int>> s2; // vector 也可作 Stack 底层 s2.emplace(42); std::cout << s2.top() << "\n"; // 42 my_stack<int, std::list<int>> s3; // list 也可作 Stack 底层 s3.push(7); std::cout << s3.top() << "\n"; // 7 }
三、my_queue的实现
3.1 设计要点
-
按标准库风格提供:front / back / push / emplace / pop / empty / size / swap
-
不提供迭代器与遍历接口(保持"队列语义"的束缚)
-
底层容器必须支持 push_back / pop_front,这决定了 vector 不可用
3.2 代码(C++14+ 版本,默认 deque,也支持 list)
cpp#include <deque> #include <utility> template<class T, class Container = std::deque<T>> class my_queue { public: using value_type = T; using container_type = Container; using size_type = typename Container::size_type; using reference = typename Container::reference; using const_reference = typename Container::const_reference; my_queue() = default; explicit my_queue(const Container& cont) : c_(cont) {} explicit my_queue(Container&& cont) : c_(std::move(cont)) {} bool empty() const noexcept { return c_.empty(); } size_type size() const noexcept { return c_.size(); } reference front() { return c_.front(); } const_reference front() const { return c_.front(); } reference back() { return c_.back(); } const_reference back() const { return c_.back(); } void push(const value_type& v) { c_.push_back(v); } void push(value_type&& v) { c_.push_back(std::move(v)); } template<class... Args> decltype(auto) emplace(Args&&... args) { return c_.emplace_back(std::forward<Args>(args)...); } void pop() { c_.pop_front(); } void swap(my_queue& other) noexcept(noexcept(std::swap(c_, other.c_))) { using std::swap; swap(c_, other.c_); } private: Container c_; };
3.3 快速测试
cpp#include <list> #include <iostream> int main() { my_queue<int> q; // 默认 deque q.push(10); q.push(20); q.emplace(30); std::cout << q.front() << "," << q.back() << "\n"; // 10,30 q.pop(); std::cout << q.front() << "\n"; // 20 my_queue<int, std::list<int>> ql; // list 可用作 Queue 底层 ql.push(1); ql.push(2); ql.pop(); std::cout << ql.front() << "\n"; // 2 }
四、复杂度、异常安全、迭代器/引用有效性
4.1 时间复杂度(以默认 deque为参考)
-
Stack:push/pop/top/empty/size → O(1)
-
Queue:push/back/front/pop/empty/size → O(1)
极少数情况下 deque 内部重分配可能引起常数因子的波动,但外部语义仍视作常数时间。
4.2 异常安全
-
push/emplace 可能在分配或构造时抛异常;容器保持自洽,要么成功插入,要么回滚到插入前状态(依赖底层容器保障)。
-
pop 不抛异常(但对空容器调用是未定义行为,建议在 Debug 下断言/检查)。
4.3 迭代器/指针/引用的有效性(适配器本身不暴露迭代器)
-
top()/front()/back() 返回的引用在元素被 pop 后立即失效。
-
push 是否使已有引用失效取决于底层容器:
-
deque:在极少数扩容/重组时,引用/指针可能失效;
list:节点稳定,绝大多数情况下引用有效(被删节点除外)。
结论:尽快使用 top()/front() 返回的引用,不要长期持有。
五、可替换底层容器:该怎么选?
适配器 | 推荐默认 | 备选 | 说明 |
---|---|---|---|
Stack | deque<T> | vector<T>/list<T> | 三者都能满足 Stack 的需求 |
Queue | deque<T> | list<T> | 不要用 vector,缺 pop_front() |
-
deque :两端操作均衡、实现成熟、整体吞吐好 → 首选
-
vector(仅 Stack):push_back/pop_back/top 很快;但随机插入/头删不适合
-
list:指针/引用稳定、频繁中间插删友好,但节点开销大、局部性差
六、常见坑位与工程化建议
-
不要继承底层容器
适配器语义是"约束接口",用组合而非继承;继承会把不该暴露的接口也暴露出来,破坏 LIFO/FIFO 语义边界。
-
不提供 clear() 是否合适?
标准 std::stack/queue 不提供 clear();
需要清空时可:while(!s.empty()) s.pop(); 或 my_stack<T>().swap(s);。
自定义适配器可以"加一个 clear()"作为工程便捷,但要清楚与标准差异。
-
空容器上的 top()/front()/back()/pop()
与标准一致:未定义行为。工程里可在 Debug 宏下 assert(!empty()) 防御。
-
emplace 的价值
复杂对象构造时,emplace 原地构造避免一次拷贝/移动,异常安全更自然。
-
线程安全
适配器/底层容器都不是线程安全;并发场景须加锁或使用并发队列。
-
C++20 Concepts(可选)
你可以用 Concepts 对底层容器做编译期约束,让错误更早暴露(见下)。
七、(可选)用 C++20 Concepts 约束底层容器
cpp#if __cpp_concepts #include <concepts> template<class C, class T> concept StackContainer = requires(C c, T v) { { c.back() } -> std::same_as<typename C::reference>; { c.push_back(v) }; { c.pop_back() }; { c.empty() } -> std::same_as<bool>; { c.size() }; }; template<class C, class T> concept QueueContainer = requires(C c, T v) { { c.front() } -> std::same_as<typename C::reference>; { c.back() } -> std::same_as<typename C::reference>; { c.push_back(v) }; { c.pop_front() }; { c.empty() } -> std::same_as<bool>; { c.size() }; }; // 用法:template<class T, StackContainer<T> Container = std::deque<T>> class my_stack { ... }; #endif
这能让「把 vector 错误地拿去做 queue 底层」直接编译失败,错误更易懂。
总结
-
Stack/Queue 是"适配器",不是容器 :通过组合 底层容器并收窄接口来提供 LIFO/FIFO 语义。
-
关键在接口约束:
-
Stack 需 back/push_back/pop_back;
-
Queue 需 front/back/push_back/pop_front;
因此 Queue 不能用 vector。
-
-
复杂度与异常安全 取决于底层容器;默认 deque 综合表现最好。
-
迭代器/引用有效性:适配器不提供迭代器;front()/top() 的引用在 pop 后失效,push 的影响以底层容器为准。
-
工程建议:用组合而非继承;可在 Debug 下断言空栈/空队列的错误;必要时使用 Concepts 约束。
-
学习意义 :通过自实现适配器,你会更透彻地理解 STL 的"以语义约束能力"的设计思想,以及组合优于继承的工程实践。