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/list的swap()均为 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()会删除元素并调用其析构函数,但底层容器的内存(如vector的capacity)不会释放; - 若需彻底释放内存,可通过"临时对象交换":
- 调用
cpp
stack<int>().swap(stk); // 用空栈交换,原栈的底层容器被销毁,内存释放
📚六、总结
stack 是 STL 中设计简洁、用途明确的适配器容器,核心价值在于"封装底层容器,提供严格的先进后出接口"。其底层默认依赖 deque,兼顾效率与内存灵活性,也可根据场景切换为 vector 或 list。
使用时需注意:避免栈空时操作、理解 pop() 不返回元素的设计初衷。