C++ stack 全面解析与实战指南
在C++标准模板库(STL)中,stack(栈)是一种遵循"后进先出"(LIFO, Last In First Out)规则的容器适配器。它并非独立的容器,而是基于其他基础容器(如deque、vector、list)封装实现,屏蔽了部分基础容器的接口,仅暴露符合栈逻辑的操作。stack在日常开发中应用广泛,例如表达式求值、函数调用栈模拟、括号匹配等场景。本文将从stack的底层实现出发,详细讲解其核心特性、常用接口,结合实战案例演示具体用法,并梳理使用注意事项,帮助大家彻底掌握这一基础容器适配器。
一、stack 核心原理与特性
要理解stack的行为逻辑,首先需要明确其"容器适配器"的本质------它不直接管理内存,而是复用基础容器的内存管理和核心操作,仅对外提供统一的栈操作接口。
1.1 底层实现:基于基础容器的适配
stack的底层默认依赖deque容器实现(C++标准推荐,大部分编译器默认如此)。这是因为deque支持高效的尾部插入/删除操作(push_back、pop_back),且内存分配灵活,能很好地匹配栈的核心需求。同时,C++也支持指定其他符合要求的基础容器(需支持push_back、pop_back、back、empty、size这5个核心接口),例如vector、list等。
stack的适配逻辑非常简单:将栈的"压栈"对应基础容器的push_back(尾部插入),"出栈"对应基础容器的pop_back(尾部删除),"栈顶元素访问"对应基础容器的back(访问尾部元素)。通过这种适配,屏蔽了基础容器的头部操作、随机访问等接口,严格保证"后进先出"的规则。
1.2 核心特性总结
-
后进先出(LIFO):最后插入的元素最先被访问,仅允许在栈顶进行插入和删除操作;
-
容器适配器:不独立管理内存,依赖基础容器(默认deque)实现核心功能;
-
接口受限:仅暴露栈相关接口(压栈、出栈、访问栈顶、判空、获取大小),不支持迭代器遍历,无法直接访问栈中间的元素;
-
高效操作:压栈(push)、出栈(pop)、访问栈顶(top)的时间复杂度均为O(1)(依赖基础容器的尾部操作效率);
-
无默认初始化元素:创建空stack时无默认元素,需手动压栈添加元素。
1.3 支持的基础容器
stack可指定的基础容器需满足"支持尾部插入/删除、访问尾部元素、判空、获取大小"这5个核心接口,STL中符合要求的容器有3个:
| 基础容器 | 适配优势 | 适用场景 |
|---|---|---|
| deque(默认) | 尾部插入/删除效率高,内存分配灵活,无vector扩容时的大量数据拷贝开销 | 大多数通用场景,推荐默认使用 |
| vector | 内存连续,缓存命中率高,尾部操作效率稳定 | 栈元素数量稳定,无需频繁扩容的场景 |
| list | 尾部插入/删除效率高,无扩容开销,元素插入不会导致迭代器失效(但stack不支持迭代器) | 栈元素数量波动大,需频繁插入/删除的场景 |
| 指定基础容器的语法示例: |
cpp
#include <stack>
#include <vector>
#include <list>
// 基于vector的stack
std::stack<int, std::vector<int>> stack_vec;
// 基于list的stack
std::stack<int, std::list<int>> stack_list;
// 基于默认deque的stack(最常用)
std::stack<int> stack_deque;
}
二、C++ stack 常用接口详解
stack的接口设计简洁直观,仅包含与栈逻辑相关的核心操作。使用stack前,需包含头文件 <stack>,并使用std命名空间(或显式指定std::stack)。
2.1 构造与析构
| 接口原型 | 功能说明 | 示例 |
|---|---|---|
| stack(); | 默认构造函数,创建空stack(基础容器也为空) | std::stack s; |
| explicit stack(const Container& cont); | 用已有的基础容器对象cont初始化stack,stack的元素与cont一致 | std::deque d{1,2,3}; std::stack s(d); |
| stack(const stack& other); | 拷贝构造函数,创建一个与other内容完全相同的stack | std::stack s1; s1.push(1); std::stack s2(s1); |
| ~stack(); | 析构函数,释放基础容器的资源 | - |
2.2 核心操作(压栈、出栈、访问栈顶)
这是stack最常用的接口,直接对应栈的核心逻辑:
| 接口 | 功能说明 | 注意事项 | 时间复杂度 |
|---|---|---|---|
| push(const value_type& val) | 将val压入栈顶(调用基础容器的push_back) | val会被拷贝/移动到容器中 | O(1) |
| emplace(Args&&... args) | 在栈顶直接构造元素(调用基础容器的emplace_back) | 避免拷贝,效率高于push | O(1) |
| pop() | 删除栈顶元素(调用基础容器的pop_back) | 不返回被删除的元素;栈为空时调用会导致未定义行为 | O(1) |
| top() | 返回栈顶元素的引用(调用基础容器的back) | 栈为空时调用会导致未定义行为;可通过top()修改栈顶元素(若元素非const) | O(1) |
2.3 容量相关
| 接口 | 功能说明 | 示例 |
|---|---|---|
| empty() | 判断栈是否为空(调用基础容器的empty),空返回true,否则返回false | if (s.empty()) { ... } |
| size() | 返回栈中元素的个数(调用基础容器的size) | cout << "栈大小:" << s.size(); |
2.4 赋值操作
| 接口原型 | 功能说明 | 示例 |
|---|---|---|
| stack& operator=(const stack& other); | 拷贝赋值,将other的内容赋值给当前stack,覆盖原有内容 | s1 = s2; // s1的内容变为s2的内容 |
| stack& operator=(stack&& other) noexcept; | 移动赋值,将other的内容移动到当前stack,other变为空 | s1 = std::move(s2); // 高效转移资源 |
2.5 接口综合示例
cpp
#include <stack>
#include <iostream>
using namespace std;
int main() {
// 1. 构造空stack(默认deque为基础容器)
stack<int> s;
// 2. 压栈操作
s.push(10);
s.push(20);
s.emplace(30); // 直接构造,效率更高
cout << "栈大小:" << s.size() << endl; // 输出:3
cout << "栈顶元素:" << s.top() << endl; // 输出:30(最后压入的元素)
// 3. 修改栈顶元素(非const情况下)
s.top() = 35;
cout << "修改后栈顶元素:" << s.top() << endl; // 输出:35
// 4. 出栈操作
s.pop();
cout << "出栈后栈顶元素:" << s.top() << endl; // 输出:20
cout << "出栈后栈大小:" << s.size() << endl; // 输出:2
// 5. 判空与清空(stack无clear接口,需通过pop循环清空)
while (!s.empty()) {
cout << "出栈元素:" << s.top() << endl;
s.pop();
}
cout << "清空后栈是否为空:" << (s.empty() ? "是" : "否") << endl; // 输出:是
// 6. 拷贝构造与赋值
stack<int> s1;
s1.push(1);
s1.push(2);
stack<int> s2(s1); // 拷贝构造
cout << "s2栈顶元素:" << s2.top() << endl; // 输出:2
stack<int> s3;
s3 = s1; // 拷贝赋值
cout << "s3栈大小:" << s3.size() << endl; // 输出:2
return 0;
}
输出结果:
text
栈大小:3
栈顶元素:30
修改后栈顶元素:35
出栈后栈顶元素:20
出栈后栈大小:2
出栈元素:20
出栈元素:10
清空后栈是否为空:是
s2栈顶元素:2
s3栈大小:2
三、stack 实战案例
stack的"后进先出"特性使其在多个经典场景中不可或缺,以下通过3个实战案例演示其实际应用:
3.1 场景1:括号匹配验证
需求:给定一个只包含括号('('、')'、'{'、'}'、'['、']')的字符串,判断字符串中的括号是否完全匹配(左右括号类型一致、顺序正确、无多余括号)。
思路:
-
遍历字符串,遇到左括号('('、'{'、'[')时,将其压入栈中;
-
遇到右括号时,判断栈是否为空(为空则无匹配的左括号,返回false),或栈顶左括号与当前右括号类型不匹配(返回false);若匹配,则弹出栈顶左括号;
-
遍历结束后,栈需为空(否则存在未匹配的左括号,返回false),否则返回true。
cpp
#include <stack>
#include <iostream>
#include <string>
#include <unordered_map>
using namespace std;
bool isValid(string s) {
// 定义右括号到左括号的映射
unordered_map<char, char> bracketMap = {
{')', '('},
{'}', '{'},
{']', '['}
};
stack<char> st;
for (char c : s) {
// 遇到右括号
if (bracketMap.count(c)) {
// 栈为空或栈顶左括号不匹配
if (st.empty() || st.top() != bracketMap[c]) {
return false;
}
// 匹配成功,弹出栈顶左括号
st.pop();
} else {
// 遇到左括号,压入栈中
st.push(c);
}
}
// 遍历结束后栈需为空(无未匹配的左括号)
return st.empty();
}
int main() {
string s1 = "()[]{}";
string s2 = "(]";
string s3 = "([)]";
string s4 = "{[]}";
cout << s1 << " 是否有效:" << (isValid(s1) ? "是" : "否") << endl; // 是
cout << s2 << " 是否有效:" << (isValid(s2) ? "是" : "否") << endl; // 否
cout << s3 << " 是否有效:" << (isValid(s3) ? "是" : "否") << endl; // 否
cout << s4 << " 是否有效:" << (isValid(s4) ? "是" : "否") << endl; // 是
return 0;
}
3.2 场景2:逆波兰表达式求值
需求:逆波兰表达式(后缀表达式)是一种不含括号的表达式,运算符位于两个操作数之后,计算规则简单。给定一个逆波兰表达式的字符串数组,求其结果(假设表达式合法,仅包含数字和+、-、*、/四种运算符,除法向下取整)。
思路:
-
遍历字符串数组,遇到数字时,将其转换为整数压入栈中;
-
遇到运算符时,弹出栈顶两个元素(注意:先弹出的是右操作数,后弹出的是左操作数);
-
计算两个操作数与运算符的结果,将结果压入栈中;
-
遍历结束后,栈中仅剩一个元素,即为表达式的结果。
cpp
#include <stack>
#include <iostream>
#include <vector>
#include <string>
using namespace std;
int evalRPN(vector<string>& tokens) {
stack<int> st;
for (const string& token : tokens) {
// 遇到运算符
if (token == "+" || token == "-" || token == "*" || token == "/") {
// 弹出两个操作数(注意顺序:先弹右操作数,后弹左操作数)
int right = st.top();
st.pop();
int left = st.top();
st.pop();
// 计算结果并压栈
if (token == "+") {
st.push(left + right);
} else if (token == "-") {
st.push(left - right);
} else if (token == "*") {
st.push(left * right);
} else if (token == "/") {
// 除法向下取整(C++中负数除法需注意,此处按题目要求处理)
st.push(left / right);
}
} else {
// 遇到数字,转换为整数压栈
st.push(stoi(token));
}
}
// 栈中仅剩结果
return st.top();
}
int main() {
vector<string> tokens1 = {"2","1","+","3","*"}; // 等价于 (2+1)*3 = 9
vector<string> tokens2 = {"4","13","5","/","+"}; // 等价于 4 + (13/5) = 6
vector<string> tokens3 = {"10","6","9","3","+","-11","*","/","*","17","+","5","+"}; // 等价于 ((10*(6/(9+3*-11)))+17)+5 = 22
cout << "表达式1结果:" << evalRPN(tokens1) << endl; // 9
cout << "表达式2结果:" << evalRPN(tokens2) << endl; // 6
cout << "表达式3结果:" << evalRPN(tokens3) << endl; // 22
return 0;
}
3.3 场景3:模拟函数调用栈
需求:模拟程序的函数调用过程,记录函数的调用顺序和返回顺序(函数调用时压栈,函数返回时出栈)。
cpp
#include <stack>
#include <iostream>
#include <string>
using namespace std;
// 模拟函数调用(压栈)
void callFunction(stack<string>& callStack, const string& funcName) {
callStack.push(funcName);
cout << "调用函数:" << funcName << endl;
}
// 模拟函数返回(出栈)
void returnFunction(stack<string>& callStack) {
if (callStack.empty()) {
cout << "无正在执行的函数,无法返回!" << endl;
return;
}
string funcName = callStack.top();
callStack.pop();
cout << "返回函数:" << funcName << endl;
}
int main() {
stack<string> callStack;
// 模拟函数调用流程
callFunction(callStack, "main()");
callFunction(callStack, "funcA()");
callFunction(callStack, "funcB()");
returnFunction(callStack); // funcB返回
callFunction(callStack, "funcC()");
returnFunction(callStack); // funcC返回
returnFunction(callStack); // funcA返回
returnFunction(callStack); // main返回
returnFunction(callStack); // 无函数可返回
return 0;
}
输出结果:
text
调用函数:main()
调用函数:funcA()
调用函数:funcB()
返回函数:funcB()
调用函数:funcC()
返回函数:funcC()
返回函数:funcA()
返回函数:main()
无正在执行的函数,无法返回!
四、stack 使用注意事项
-
栈为空时禁止调用pop()和top():stack的pop()和top()接口在栈为空时调用会导致未定义行为(程序崩溃或异常)。因此,在调用这两个接口前,必须通过empty()判断栈是否为空。
-
stack无迭代器,无法遍历:stack的设计初衷是严格遵循LIFO规则,因此未暴露迭代器接口,无法直接遍历栈中的所有元素。若需遍历,需通过pop()将元素依次弹出并记录(但会清空栈),或自定义基于基础容器的栈结构。
-
stack无clear()接口,清空需手动循环pop():与vector、deque等容器不同,stack未提供clear()接口。若需清空栈,需通过while循环调用pop(),直到栈为空。
-
选择合适的基础容器:
-
默认的deque基础容器适用于大多数场景,兼顾效率和灵活性;
-
若栈元素数量稳定,无需频繁扩容,可选择vector作为基础容器(内存连续,缓存命中率高);
-
若需频繁插入/删除且元素数量波动大,可选择list作为基础容器(无扩容开销)。
-
-
top()返回的是引用,可修改栈顶元素:stack的top()返回栈顶元素的非const引用,因此可以通过top()修改栈顶元素的值。若需禁止修改,可使用const stack。
-
元素类型需支持拷贝/移动:stack的push()接口会拷贝/移动元素到基础容器中,因此元素类型必须支持拷贝构造或移动构造。若元素为自定义类型,需确保正确实现拷贝/移动语义。
-
线程安全性:与所有STL容器一致,stack不保证线程安全。多线程环境下并发读写栈时,需手动加锁(如使用std::mutex)保护栈的操作。
五、总结
stack是C++ STL中基于基础容器封装的"后进先出"容器适配器,核心优势是接口简洁、操作高效(压栈、出栈、访问栈顶均为O(1))。它不独立管理内存,而是复用deque、vector、list等基础容器的功能,严格屏蔽了非LIFO相关的接口,确保数据操作的规范性。
stack的经典应用场景包括括号匹配、表达式求值、函数调用栈模拟等,掌握其核心接口和使用注意事项,能帮助我们快速解决这类"后进先出"相关的问题。使用时需注意:调用pop()和top()前先判空、无迭代器无法遍历、清空需手动循环pop()等细节,避免出现未定义行为。
希望本文能帮助大家彻底理解并掌握stack的使用,如果有疑问或补充,欢迎在评论区留言!