认识STLstack容器

stack(栈)是 C++ STL 中的容器适配器(adaptor) ,而非独立的基础容器------它是基于现有的序列式容器(默认 deque)封装实现的,提供"先进后出(LIFO,Last In First Out)"的操作接口。


🔎一、stack 的本质:容器适配器的设计思想

1. 什么是"容器适配器"?

适配器(adaptor)是 STL 的核心设计模式之一,其作用是对现有容器的接口进行封装和改造 ,屏蔽原有容器的部分功能,只暴露符合特定使用场景的接口。

stack 就是典型的适配器:它并不直接管理内存,而是"复用"deque(或 vector/list)的底层内存结构和操作,仅提供栈所需要的"入栈、出栈、访问栈顶"等核心接口,隐藏了序列式容器的随机访问、中间插入/删除等无关功能。

2. stack 的底层依赖(默认与可选容器)

stack 的底层容器并非固定,可通过模板参数指定,但必须满足以下要求:

  • 支持 push_back()(尾部插入,对应栈的"入栈")
  • 支持 pop_back()(尾部删除,对应栈的"出栈")
  • 支持 back()(访问尾部元素,对应栈的"栈顶")
  • 支持 empty()/size()(判断空、获取大小)

STL 中默认选择 deque 作为底层容器,原因如下:

  • deque 尾部增删(push_back/pop_back)是 O(1) 复杂度,且无 vector 扩容时的整体拷贝开销;
  • deque 头部增删同样高效,但 stack 用不到,不影响适配;
  • 避免 vector 存储大量元素时,扩容导致的性能波动。

也可手动指定其他符合要求的底层容器:

cpp 复制代码
// 以 vector 为底层容器的 stack
stack<int, vector<int>> stk_vec;
// 以 list 为底层容器的 stack
stack<int, list<int>> stk_list;

💡二、stack 的底层实现

stack 的实现极其简洁,核心是"委托"底层容器完成所有操作。以下是 GCC 标准库中 stack 的简化源码(<stack> 头文件):

cpp 复制代码
template <typename T, typename Container = deque<T>>
class stack {
protected:
    Container c;  // 核心:底层容器对象,所有操作都委托给 c

public:
    // 1. 入栈:调用底层容器的 push_back()
    void push(const T& x) { c.push_back(x); }
    // C++11 移动语义支持
    void push(T&& x) { c.push_back(std::move(x)); }

    // 2. 出栈:调用底层容器的 pop_back()(注意:不返回栈顶元素)
    void pop() { c.pop_back(); }

    // 3. 访问栈顶元素:调用底层容器的 back()
    T& top() { return c.back(); }
    const T& top() const { return c.back(); }

    // 4. 状态查询:委托底层容器的接口
    bool empty() const { return c.empty(); }
    size_t size() const { return c.size(); }

    // 交换两个 stack:委托底层容器的 swap()
    void swap(stack& other) noexcept(noexcept(c.swap(other.c))) {
        c.swap(other.c);
    }
};
关键结论:
  • stack 本身不存储任何数据 ,所有数据都存储在底层容器 c
  • stack 的所有接口都是对底层容器对应接口的"一对一映射",无额外性能开销
  • 底层容器的特性直接决定 stack 的性能(如用 vector 则扩容时可能有拷贝,用 list 则内存碎片更多)

📝三、stack 核心操作与底层逻辑

stack 的接口非常精简,仅提供 5 个核心操作(入栈、出栈、栈顶访问、状态查询、交换),完全贴合"先进后出"的栈模型。

1. 入栈:push()emplace()
(1)push():拷贝/移动元素入栈
cpp 复制代码
stack<int> stk;
stk.push(10);  // 拷贝入栈(字面量隐式构造)
stk.push(int(20));  // 移动入栈(临时对象)
  • 底层逻辑:调用 c.push_back(x),将元素添加到底层容器的尾部(栈顶)
  • C++11 后支持移动语义:若元素是可移动类型(如 string),则直接移动元素,避免拷贝
(2)emplace():直接构造元素入栈(C++11)
cpp 复制代码
// 直接在栈顶构造元素,无需先创建临时对象
stk.emplace(30); 
// 构造自定义类型(如 pair),传递构造参数即可
stack<pair<int, string>> stk_pair;
stk_pair.emplace(1, "hello");  // 直接构造 pair(1, "hello")
  • 底层逻辑:调用 c.emplace_back(Args...),在底层容器尾部直接构造元素
  • 优势:比 push() 更高效,尤其对于自定义类型/大对象,避免临时对象的拷贝/移动开销

2. 出栈:pop()
cpp 复制代码
stk.pop();  // 删除栈顶元素(不返回该元素)
  • 底层逻辑:调用 c.pop_back(),删除底层容器的尾部元素
  • 注意事项:
    • pop() 不返回栈顶元素,若需获取栈顶并删除,需先 top()pop()
    • 若栈为空(empty() == true),调用 pop() 会触发未定义行为(崩溃),所以删除之前一定要判断栈是否为空

3. 访问栈顶:top()
cpp 复制代码
cout << stk.top();  // 返回栈顶元素的引用(底层容器的 back())
  • 底层逻辑:返回 c.back(),即底层容器尾部元素的引用
  • 注意事项:
    • 栈为空时调用 top() 会触发未定义行为
    • 非 const 栈返回非 const 引用,可直接修改栈顶元素
cpp 复制代码
stk.top() = 100;  // 合法,栈顶元素被改为 100

4. 状态查询:empty()size()
cpp 复制代码
if (stk.empty()) {  // 判断栈是否为空(c.empty())
    cout << "栈为空" << endl;
}
cout << "栈中元素个数:" << stk.size();  // 获取元素个数(c.size())

5. 交换:swap()
cpp 复制代码
stack<int> stk1, stk2;
stk1.push(10);
stk2.push(20);
stk1.swap(stk2);  // 交换两个栈的内容
  • 底层逻辑:调用 c.swap(other.c),交换底层容器的内容
  • 特性:时间复杂度 O(1)(底层容器 deque/vector/listswap() 均为 O(1))

🔧四、stack 的底层容器对比(选择指南)

stack 的性能完全由底层容器决定,不同底层容器的特性差异会直接影响 stack 的使用场景。以下是三种常用底层容器的对比:

|-------------|----------------------|---------------------------|----------------------------------|
| 底层容器 | 入栈/出栈效率 | 内存特性 | 适用场景 |
| deque(默认) | O(1)(无扩容时),扩容时无需整体拷贝 | 分段连续内存,无内存浪费(相对 vector) | 大多数场景(平衡性能与内存) |
| vector | O(1)(无扩容时),扩容时需整体拷贝 | 单一连续内存,缓存友好,可能有预留空间浪费 | 栈元素数量可预估(避免频繁扩容),追求极致访问速度 |
| list | O(1)(无扩容概念) | 完全离散节点,内存碎片多,缓存友好性差 | 需频繁入栈/出栈且元素体积大(避免 vector 扩容拷贝) |

选择建议:
  • 无特殊需求时,直接使用默认的 deque 底层实现(STL 推荐)
  • 若栈元素数量固定或可预估,且需要最快的栈顶访问速度,选 vector
  • 若元素是大对象(如自定义类实例),且频繁入栈出栈,选 list(避免拷贝开销)

⚠️五、stack 使用中的注意事项与陷阱

1. 栈为空时的操作风险
  • 禁止在 empty() == true 时调用 top()pop(),会触发未定义行为(程序崩溃);
  • 正确做法:操作前先判断栈是否为空:
cpp 复制代码
if (!stk.empty()) {
    cout << stk.top();
    stk.pop();
}

2. pop() 不返回元素的设计原因

stack 的 pop() 不返回栈顶元素,是为了异常安全

  • pop() 返回元素,当元素拷贝/移动过程中抛出异常时,元素会被从栈中删除但未成功返回,导致数据丢失;
  • 分开调用 top()pop():若 top() 抛出异常(如栈空),栈状态未变;若 pop() 抛出异常(极少情况),元素已被安全删除,无数据丢失。

3. 不支持迭代器与随机访问

stack 是"严格的栈模型",刻意屏蔽了迭代器接口,不支持遍历、随机访问等操作------这是设计意图:强制用户遵守"先进后出"的使用规则,避免破坏栈的逻辑结构。


4. 自定义类型的栈使用要点
  • 自定义类型需提供默认构造函数、拷贝构造函数(或移动构造函数),否则无法入栈;
  • 若自定义类型有资源管理(如堆内存),需正确实现析构函数,避免内存泄漏(底层容器会自动调用元素的析构函数)。

5. 内存释放问题
  • stack 本身不管理内存,内存释放依赖底层容器:
    • 调用 pop() 会删除元素并调用其析构函数,但底层容器的内存(如 vectorcapacity)不会释放;
    • 若需彻底释放内存,可通过"临时对象交换":
cpp 复制代码
stack<int>().swap(stk);  // 用空栈交换,原栈的底层容器被销毁,内存释放

📚六、总结

stack 是 STL 中设计简洁、用途明确的适配器容器,核心价值在于"封装底层容器,提供严格的先进后出接口"。其底层默认依赖 deque,兼顾效率与内存灵活性,也可根据场景切换为 vectorlist

使用时需注意:避免栈空时操作、理解 pop() 不返回元素的设计初衷。

相关推荐
繁华似锦respect40 分钟前
C++ 设计模式之观察者模式详细介绍
linux·开发语言·c++·windows·观察者模式·设计模式·visual studio
威桑41 分钟前
一个 CMake 项目是否只能有一个 install 目录?
linux·c++·cmake
爪哇部落算法小助手44 分钟前
每日两题day61
数据结构·c++·算法
曼巴UE51 小时前
UE5 C++ 多播绑定执行演示
c++·ue5
繁华似锦respect1 小时前
C++ 自定义 String 类
服务器·开发语言·c++·哈希算法·visual studio
phdsky1 小时前
【设计模式】工厂方法模式
c++·设计模式·工厂方法模式
2301_807997381 小时前
代码随想录-day54
数据结构·c++·算法
curry____3031 小时前
study in pta + 豆包(求区间和)(前缀和算法)(如何处理zhan栈溢出和超出时间复杂度问题)(2025.12.2)
数据结构·c++·算法
BestOrNothing_20151 小时前
【C++基础】Day 6:前置++ VS 后置++(语法底层 + STL规范 + 面试高频)
c++·运算符重载·面试八股·前置++·后置++·stl迭代器