在 C++ STL 标准库中,stack(栈)是一种经典的容器适配器,它基于“后进先出(LIFO, Last In First Out)”的核心原则,为开发者提供了简洁高效的元素存取接口。与可随机访问的 vector 或双向遍历的 list 不同,stack 刻意隐藏了复杂的底层操作,仅暴露栈顶相关的核心接口,完美适配需要严格遵循“后进先出”逻辑的场景——从表达式求值到深度优先搜索(DFS),都能看到它的身影。

本文将从 stack 的核心特性入手,详解其底层设计逻辑,手把手拆解接口实现过程,并结合实战案例说明使用规范,帮助开发者真正理解“容器适配器”的设计思想,而非仅停留在API调用层面。

一、Stack 核心特性:不止于“后进先出”

要掌握 stack,首先需要明确其作为“容器适配器”的本质特性,这是区别于普通容器的关键。

1.1 本质:基于底层容器的适配器

stack 本身不直接存储数据,而是封装了一个底层容器(默认是 deque<T> 双端队列),通过限制底层容器的操作接口来实现“后进先出”的逻辑。简单来说:

  • 底层容器提供实际的存储能力(如 deque 的动态数组结构);
  • stack 对外暴露的接口仅允许从“一端”(栈顶)进行元素的插入、删除和访问,屏蔽了底层容器的其他操作(如 deque 的头部插入/删除)。

这种设计的优势在于复用性——无需重新实现存储逻辑,直接借助现有容器的高效实现;同时保证了接口纯粹性,避免开发者因误用接口破坏“后进先出”的约束。

1.2 关键特性与使用注意事项

特性说明
数据访问限制仅支持访问栈顶元素,不支持随机访问,无迭代器接口,无法遍历内部元素
操作效率所有核心操作(push/pop/top/size/empty)的时间复杂度均为 O(1)
底层容器灵活性可指定自定义底层容器(如 vectorlist),需支持 push_back/pop_back/back 等接口

使用时必须警惕的“坑点”:

  1. 空栈操作风险:调用 top()(获取栈顶元素)或 pop()(删除栈顶元素)前,必须通过 empty() 判断栈是否非空,否则会引发未定义行为(如访问非法内存)。
  2. 无原生清空接口stack 未提供 clear() 函数,如需清空栈,需循环调用 pop() 直至栈为空。
  3. 底层容器依赖:自定义底层容器时,必须确保其支持 push_back(尾部插入)、pop_back(尾部删除)、back(获取尾部元素)、size(元素个数)和 empty(判空)这5个核心接口。

二、Stack 常用接口:一张表理清核心操作

stack 的接口设计极为简洁,仅保留实现“后进先出”逻辑必需的操作,下表完整列出了其常用接口及功能:

函数接口功能描述参数与返回值说明
void push(const T& x)向栈顶插入一个元素参数:待插入的 T 类型元素;无返回值
void pop()删除栈顶元素(不返回被删除元素无参数;无返回值
T& top()返回栈顶元素的引用(可修改栈顶元素)无参数;返回 T& 类型的栈顶元素引用
const T& top() const重载版本,返回栈顶元素的常引用(不可修改,用于const对象)无参数;返回 const T& 类型的栈顶元素引用
size_t size() const返回栈中元素的个数无参数;返回 size_t 类型的元素个数
bool empty() const判断栈是否为空,为空返回 true,非空返回 false无参数;返回 bool 类型的判空结果

接口使用示例

#include <iostream>
#include <stack>  // 标准库stack头文件

using namespace std;

int main() {
    stack<int> st;  // 定义存储int类型的stack,底层默认使用deque<int>

    // 1. 入栈操作
    st.push(10);
    st.push(20);
    st.push(30);

    // 2. 访问栈顶元素与元素个数
    cout << "栈顶元素:" << st.top() << endl;    // 输出:30
    cout << "元素个数:" << st.size() << endl;    // 输出:3

    // 3. 出栈操作(需先判空)
    while (!st.empty()) {
        cout << "出栈元素:" << st.top() << endl;  // 先获取栈顶再出栈
        st.pop();
    }

    // 4. 空栈判断
    cout << "栈是否为空:" << (st.empty() ? "是" : "否") << endl;  // 输出:是

    return 0;
}

运行结果:

栈顶元素:30
元素个数:3
出栈元素:30
出栈元素:20
出栈元素:10
栈是否为空:是

三、手写 Stack:从模板设计到接口实现

理解 stack 的最佳方式是亲手实现它。下面我们基于 C++ 模板,从零构建一个功能完整的 stack 容器适配器,深入拆解每一行代码的设计逻辑。

3.1 模板参数设计:兼顾易用性与灵活性

stack 的模板参数需要同时满足“用户无需关心底层实现”和“支持自定义底层容器”的需求,因此设计为双模板参数:

#include <vector>
#include <deque>  // 默认底层容器所需头文件

namespace my_stl {  // 自定义命名空间,避免与标准库冲突
    // 模板参数:T-元素类型;Container-底层容器类型,默认值为deque<T>
    template <class T, class Container = std::deque<T>>
    class stack {
        // 接口实现见下文
    };
}
模板参数设计的深层逻辑:
  • 第一个参数 T:明确栈存储的元素类型(如 intstring),这是用户最关心的“数据契约”,保证栈内元素类型统一。
  • 第二个参数 Container:指定底层存储容器,默认使用 deque<T>。这样设计的原因是:
    1. 易用性:用户可直接声明 my_stl::stack<int>,无需手动指定底层容器;
    2. 灵活性:若需特殊需求(如追求更高的缓存命中率),可指定 vector<T> 作为底层容器(如 my_stl::stack<int, std::vector<int>>)。

为什么默认选 deque 而非 vector
deque 兼顾了 vector 的随机访问效率和 list 的头部/尾部操作效率,且避免了 vector 扩容时的大量数据拷贝,是实现 stack 的最优平衡选择。

3.2 私有成员:复用底层容器的存储能力

stack 的核心是“适配”底层容器,因此仅需一个私有成员变量存储底层容器对象,所有操作都通过该对象间接实现:

private:
    Container _con;  // 底层容器对象,负责实际存储元素

3.3 核心接口实现:映射到底层容器的操作

stack 的所有接口本质上是对底层容器对应接口的“封装”,仅暴露栈顶相关的操作:

1. 入栈操作 push

向栈顶插入元素,对应底层容器的“尾部插入”(push_back):

public:
    // 插入元素到栈顶(复用底层容器的push_back)
    void push(const T& x) {
        _con.push_back(x);
    }
2. 出栈操作 pop

删除栈顶元素,对应底层容器的“尾部删除”(pop_back):

    // 删除栈顶元素(复用底层容器的pop_back,不返回被删除元素)
    void pop() {
        // 注意:此处可添加断言判断栈非空,增强代码健壮性
        assert(!empty() && "stack is empty, cannot pop!");
        _con.pop_back();
    }
3. 获取栈顶元素 top

返回栈顶元素的引用,对应底层容器的“获取尾部元素”(back):

    // 返回栈顶元素的引用(可修改)
    T& top() {
        assert(!empty() && "stack is empty, cannot get top!");
        return _con.back();
    }

    // 重载版本:支持const对象调用,返回常引用(不可修改)
    const T& top() const {
        assert(!empty() && "stack is empty, cannot get top!");
        return _con.back();
    }
4. 元素个数 size

返回栈中元素的个数,直接调用底层容器的 size 接口:

    // 返回栈中元素的个数
    size_t size() const {
        return _con.size();
    }
5. 判空操作 empty

判断栈是否为空,直接调用底层容器的 empty 接口:

    // 判断栈是否为空
    bool empty() const {
        return _con.empty();
    }

3.4 完整实现代码

#pragma once  // 防止头文件重复包含
#include <vector>
#include <deque>
#include <cassert>  // 用于断言

namespace my_stl {
    // 模板参数:T-元素类型;Container-底层容器(默认deque<T>)
    template <class T, class Container = std::deque<T>>
    class stack {
    public:
        // 1. 入栈:向栈顶插入元素
        void push(const T& x) {
            _con.push_back(x);
        }

        // 2. 出栈:删除栈顶元素(需确保栈非空)
        void pop() {
            assert(!empty() && "stack::pop() failed: stack is empty!");
            _con.pop_back();
        }

        // 3. 获取栈顶元素(可修改)
        T& top() {
            assert(!empty() && "stack::top() failed: stack is empty!");
            return _con.back();
        }

        // 4. 获取栈顶元素(const版本,不可修改)
        const T& top() const {
            assert(!empty() && "stack::top() const failed: stack is empty!");
            return _con.back();
        }

        // 5. 获取元素个数
        size_t size() const {
            return _con.size();
        }

        // 6. 判空
        bool empty() const {
            return _con.empty();
        }

    private:
        Container _con;  // 底层容器,负责实际存储
    };
}

四、实战进阶:自定义底层容器与场景适配

stack 的灵活性体现在支持自定义底层容器。只要满足“尾部操作接口”的容器,都可以作为 stack 的底层实现。下面通过两个案例展示其适配能力。

4.1 用 vector 作为底层容器

vector 支持 push_back/pop_back/back 等接口,可直接作为 stack 的底层容器,适合对内存连续性要求较高的场景:

#include <iostream>
#include "my_stack.h"  // 引入自定义stack头文件

using namespace std;
using namespace my_stl;

int main() {
    // 指定vector<int>作为底层容器
    stack<int, vector<int>> st;

    st.push(100);
    st.push(200);
    st.push(300);

    cout << "栈顶元素:" << st.top() << endl;  // 输出:300
    cout << "元素个数:" << st.size() << endl;  // 输出:3

    st.pop();
    cout << "出栈后栈顶:" << st.top() << endl;  // 输出:200

    return 0;
}

4.2 用 list 作为底层容器

list 是双向链表,尾部操作同样高效,适合频繁插入删除且无需随机访问的场景:

#include <iostream>
#include <list>  // 引入list头文件
#include "my_stack.h"

using namespace std;
using namespace my_stl;

int main() {
    // 指定list<string>作为底层容器
    stack<string, list<string>> st;

    st.push("C++");
    st.push("STL");
    st.push("stack");

    while (!st.empty()) {
        cout << st.top() << " ";  // 输出:stack STL C++
        st.pop();
    }

    return 0;
}

五、常见问题与避坑指南

5.1 空栈调用 top()/pop() 怎么办?

标准库 stack 对空栈操作不做检查,会引发未定义行为。解决方式有两种:

  1. 手动判空:调用前必须用 empty() 检查,这是最推荐的做法;
  2. 添加断言:自定义 stack 中可像上文实现那样添加 assert,在调试阶段快速定位问题。

5.2 如何高效清空栈?

由于 stackclear() 接口,清空栈需循环 pop()

// 清空栈的通用函数
template <class T, class Container>
void clear_stack(my_stl::stack<T, Container>& st) {
    while (!st.empty()) {
        st.pop();
    }
}

5.3 stack 为什么没有迭代器?

stack 的设计目标是严格遵循“后进先出”,迭代器会允许遍历内部元素,破坏其接口纯粹性。如果需要遍历元素,应先将 stack 的元素转移到 vectorlist 中再遍历。

六、总结

stack 作为 C++ STL 中经典的容器适配器,其设计精髓在于“封装与复用”:

  • 封装底层容器的复杂操作,对外提供纯粹的“后进先出”接口;
  • 复用现有容器的高效实现,同时支持自定义底层容器,兼顾易用性与灵活性。

掌握 stack 不仅要会用其接口,更要理解:

  1. 它不是“容器”,而是“容器适配器”,依赖底层容器实现存储;
  2. 所有操作的高效性(O(1) 时间复杂度)源于底层容器的尾部操作特性;
  3. 接口设计的“限制性”是为了保证数据访问的逻辑正确性。

在实际开发中,无论是表达式求值、括号匹配、函数调用栈模拟,还是深度优先搜索,stack 都是契合场景的最优选择。合理使用它,能让代码更简洁、高效且符合逻辑直觉。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐