1. 理解stack与queue的基础概念
在C++标准模板库(STL)中,stack和queue是两种最常用的容器适配器。它们虽然底层基于其他容器实现,但提供了独特的接口和行为特性,专门为特定场景设计。
stack(栈)遵循LIFO(Last In First Out)原则,就像餐厅里叠放的盘子——你总是取用最上面那个,新盘子也放在最上面。它的核心操作只有三个:
- push:将元素压入栈顶
- pop:移除栈顶元素
- top:访问栈顶元素
queue(队列)则遵循FIFO(First In First Out)原则,如同超市排队结账——先来的人先服务。其基本操作包括:
- push:元素加入队尾
- pop:移除队首元素
- front:访问队首元素
- back:访问队尾元素
这两种数据结构在算法和系统设计中无处不在。比如函数调用栈、浏览器历史记录、消息队列等场景都能看到它们的身影。理解它们的实现原理,能帮助我们在合适的地方选择合适的数据结构。
2. STL中stack与queue的标准用法
2.1 基本声明与初始化
使用STL的stack和queue需要包含相应头文件:
#include <stack> #include <queue>声明一个整型栈和字符串队列:
std::stack<int> myStack; std::queue<std::string> messageQueue;它们也可以基于其他容器构建:
std::stack<int, std::vector<int>> vecStack; // 基于vector的栈 std::queue<char, std::list<char>> listQueue; // 基于list的队列2.2 常用成员函数对比
stack的典型操作:
myStack.push(42); // 压入元素 int top = myStack.top(); // 获取栈顶元素 myStack.pop(); // 弹出栈顶(无返回值) bool isEmpty = myStack.empty(); // 判断是否为空 size_t size = myStack.size(); // 元素数量queue的典型操作:
messageQueue.push("Hello"); // 入队 std::string first = messageQueue.front(); // 队首元素 std::string last = messageQueue.back(); // 队尾元素 messageQueue.pop(); // 出队注意:直接对空容器调用top()/front()/pop()会导致未定义行为,操作前务必检查empty()
2.3 实际应用示例
浏览器历史记录实现:
std::stack<std::string> history; void visitPage(const std::string& url) { history.push(url); cout << "当前页面:" << url << endl; } void goBack() { if (history.size() > 1) { history.pop(); cout << "返回至:" << history.top() << endl; } }打印机任务队列:
std::queue<std::string> printQueue; void addPrintJob(const std::string& document) { printQueue.push(document); cout << "已添加:" << document << endl; } void processPrintJob() { if (!printQueue.empty()) { string current = printQueue.front(); printQueue.pop(); cout << "正在打印:" << current << endl; } }3. 从零实现stack与queue
3.1 基于数组的简易stack实现
template <typename T, size_t Capacity> class ArrayStack { private: T data[Capacity]; size_t topIndex; public: ArrayStack() : topIndex(0) {} void push(const T& value) { if (topIndex >= Capacity) throw std::overflow_error("Stack overflow"); data[topIndex++] = value; } void pop() { if (topIndex == 0) throw std::underflow_error("Stack underflow"); --topIndex; } T& top() { if (topIndex == 0) throw std::out_of_range("Stack is empty"); return data[topIndex - 1]; } bool empty() const { return topIndex == 0; } size_t size() const { return topIndex; } };3.2 基于链表的queue实现
template <typename T> class LinkedListQueue { private: struct Node { T data; Node* next; Node(const T& val) : data(val), next(nullptr) {} }; Node* frontNode; Node* rearNode; size_t count; public: LinkedListQueue() : frontNode(nullptr), rearNode(nullptr), count(0) {} ~LinkedListQueue() { while (!empty()) pop(); } void push(const T& value) { Node* newNode = new Node(value); if (rearNode) rearNode->next = newNode; rearNode = newNode; if (!frontNode) frontNode = newNode; ++count; } void pop() { if (empty()) throw std::underflow_error("Queue is empty"); Node* temp = frontNode; frontNode = frontNode->next; if (!frontNode) rearNode = nullptr; delete temp; --count; } T& front() { if (empty()) throw std::out_of_range("Queue is empty"); return frontNode->data; } bool empty() const { return count == 0; } size_t size() const { return count; } };3.3 性能优化技巧
- 内存预分配:对于已知最大容量的stack,使用静态数组比动态分配更高效
- 批量操作:实现push_range和pop_multiple等方法减少频繁调用开销
- 移动语义:为push实现右值引用版本,避免不必要的拷贝
- 内存池:频繁创建销毁节点时,使用对象池管理内存
4. 高级应用与常见问题
4.1 使用stack实现算法
括号匹配检查器:
bool isBalanced(const std::string& expr) { std::stack<char> s; for (char c : expr) { if (c == '(' || c == '[' || c == '{') { s.push(c); } else if (c == ')' || c == ']' || c == '}') { if (s.empty()) return false; char top = s.top(); s.pop(); if ((c == ')' && top != '(') || (c == ']' && top != '[') || (c == '}' && top != '{')) { return false; } } } return s.empty(); }4.2 使用queue实现BFS
void BFS(const std::vector<std::vector<int>>& graph, int start) { std::vector<bool> visited(graph.size(), false); std::queue<int> q; q.push(start); visited[start] = true; while (!q.empty()) { int current = q.front(); q.pop(); cout << "访问节点:" << current << endl; for (int neighbor : graph[current]) { if (!visited[neighbor]) { visited[neighbor] = true; q.push(neighbor); } } } }4.3 常见问题排查
迭代器失效:
- stack和queue不提供完整的迭代器支持
- 如果需要遍历,考虑先复制到其他容器
性能瓶颈:
- 默认基于deque实现,特定场景可更换底层容器
- 频繁插入删除小对象时,list可能更高效
线程安全:
- STL容器非线程安全
- 多线程环境需要自行加锁或使用并发容器
内存泄漏:
- 自定义实现时确保析构函数释放所有资源
- 使用RAII管理动态分配的内存
5. 工程实践中的经验分享
在实际项目中,stack和queue的使用有几个值得注意的细节:
异常安全:自定义实现时,push操作应该先分配资源再修改状态,确保异常时不会破坏容器一致性
容量管理:对于性能敏感场景,提前reserve足够空间避免频繁扩容:
std::vector<int> underlyingVec; underlyingVec.reserve(1000); std::stack<int, std::vector<int>> perfStack(underlyingVec);自定义比较器:priority_queue(虽然不属于本文范围)允许自定义优先级规则,这是queue的一个重要变体
调试技巧:在调试复杂算法时,可以添加打印语句跟踪stack/queue状态:
template<typename T> void debugPrint(std::stack<T> s) { // 传值复制以避免修改原stack while (!s.empty()) { std::cout << s.top() << " "; s.pop(); } std::cout << std::endl; }容器选择:当需要频繁访问中间元素时,stack/queue可能不是最佳选择,考虑使用deque或list
在实现自定义stack/queue时,我习惯先定义清晰的接口契约,然后编写单元测试验证各种边界条件(空容器操作、满容量操作等)。这种测试驱动开发的方式能有效避免许多常见错误。