C++ stack 全面解析与实战指南

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 使用注意事项

  1. 栈为空时禁止调用pop()和top():stack的pop()和top()接口在栈为空时调用会导致未定义行为(程序崩溃或异常)。因此,在调用这两个接口前,必须通过empty()判断栈是否为空。

  2. stack无迭代器,无法遍历:stack的设计初衷是严格遵循LIFO规则,因此未暴露迭代器接口,无法直接遍历栈中的所有元素。若需遍历,需通过pop()将元素依次弹出并记录(但会清空栈),或自定义基于基础容器的栈结构。

  3. stack无clear()接口,清空需手动循环pop():与vector、deque等容器不同,stack未提供clear()接口。若需清空栈,需通过while循环调用pop(),直到栈为空。

  4. 选择合适的基础容器

    • 默认的deque基础容器适用于大多数场景,兼顾效率和灵活性;

    • 若栈元素数量稳定,无需频繁扩容,可选择vector作为基础容器(内存连续,缓存命中率高);

    • 若需频繁插入/删除且元素数量波动大,可选择list作为基础容器(无扩容开销)。

  5. top()返回的是引用,可修改栈顶元素:stack的top()返回栈顶元素的非const引用,因此可以通过top()修改栈顶元素的值。若需禁止修改,可使用const stack。

  6. 元素类型需支持拷贝/移动:stack的push()接口会拷贝/移动元素到基础容器中,因此元素类型必须支持拷贝构造或移动构造。若元素为自定义类型,需确保正确实现拷贝/移动语义。

  7. 线程安全性:与所有STL容器一致,stack不保证线程安全。多线程环境下并发读写栈时,需手动加锁(如使用std::mutex)保护栈的操作。

五、总结

stack是C++ STL中基于基础容器封装的"后进先出"容器适配器,核心优势是接口简洁、操作高效(压栈、出栈、访问栈顶均为O(1))。它不独立管理内存,而是复用deque、vector、list等基础容器的功能,严格屏蔽了非LIFO相关的接口,确保数据操作的规范性。

stack的经典应用场景包括括号匹配、表达式求值、函数调用栈模拟等,掌握其核心接口和使用注意事项,能帮助我们快速解决这类"后进先出"相关的问题。使用时需注意:调用pop()和top()前先判空、无迭代器无法遍历、清空需手动循环pop()等细节,避免出现未定义行为。

希望本文能帮助大家彻底理解并掌握stack的使用,如果有疑问或补充,欢迎在评论区留言!

相关推荐
AI视觉网奇2 小时前
ue 设置骨骼网格体
c++·ue5
宋情写2 小时前
JavaAI06-SpringAI
java·人工智能
冉冰学姐2 小时前
SSM校园学习空间预约系统w314l(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面
数据库·学习·ssm 框架·校园学习空间预约系统·师生双角色
Hello.Reader2 小时前
Flink Avro Format Java / PyFlink 读写、Schema 细节与坑点总结
java·python·flink
人道领域2 小时前
【零基础学java】(反射)
java·开发语言
C雨后彩虹2 小时前
书籍叠放问题
java·数据结构·算法·华为·面试
小当家.1052 小时前
从零构建项目认知:如何画出一张合格的系统架构图(以供应链系统为例)
java·spring boot·学习·架构·系统架构·供应链·实习
悟能不能悟2 小时前
springboot如何通过url地址获得这个地址的文件
java·spring boot·后端
问今域中2 小时前
Spring Security + JWT
java·后端·spring