大家好,这里是彩妙呀~

今天彩妙带着大家来聊聊STL里的stack容器。

目录

stack的介绍 --- 参考文档

stack 容器的快速上手

stack的头文件与容器的声明

stack中的核心接口

empty():判空接口

size():获取stack的大小

top():获取栈顶元素

push():向栈中增加元素(栈顶加入元素)

emplace():高级点的push() --- C++11新增

pop():删除栈顶元素

使用stack时的常见误区

空栈直接调用 top () / pop ()

忽视 top () 返回引用的修改风险

stack 没有 clear () 函数

试图用迭代器遍历 stack

小结


stack的介绍 --- 参考文档

如果你刷过 C++ 相关的面试题,那对括号匹配、表达式求值这类题目肯定不陌生。在这些高频面试题里,stack 往往是解题的关键。

stack底层逻辑是一个叫做的数据结构,遵循“后进先出(LIFO)”的原则。而stack的底层实现是依靠容器适配器来实现的。具体可以看下面的博客:

C++:吃透容器适配器https://blog.csdn.net/weixin_66776566/article/details/157982117?spm=1001.2014.3001.5502总的来看,由于stack是依靠容器适配器来实现(简单的说,stack本质上是基于其他容器来实现的),通过对stack这个类的接口进行封装,从而来表现出这个数据结构的特性。

有关stack支持不同传参的可以看下面的博客:

c++:详解模版 从初阶到进阶的掌握https://blog.csdn.net/weixin_66776566/article/details/156945543?spm=1001.2014.3001.5502学会模版时理解stack底层的关键。

stack 容器的快速上手

官方文档中为stack的解释与定义

在官方文档中,stack是这么定义的:

template <class T, class Container = deque<T> > class stack;

显而易见,stack在官方定义中,使用类模版来方便泛型编程(也就是支持不同种类的传参,生成对应的类),而官方是选择deque来作为stack的容器适配器。

在官方中,他为stack的描述:

LIFO(后进先出)栈
栈是一种容器适配器,专门设计用于后进先出(LIFO)的操作场景,在这种场景下,元素只能从容器的同一端进行插入和提取

栈作为容器适配器实现,容器适配器是使用特定容器类的封装对象作为其底层容器的类,并提供一组特定的成员函数来访问其元素。元素从特定容器的"尾部"被压入或弹出,这一端被称为栈顶。

底层容器可以是任何标准容器类模板,或其他专门设计的容器类。该容器需要支持以下操作:

  • empty(判空)

  • size(大小)

  • back(访问尾部元素)

  • push_back(尾部插入)

  • pop_back(尾部删除)

标准容器类 vector、deque 和 list 均满足这些要求。默认情况下,如果实例化栈时未指定容器类,则使用标准容器 deque

stack的头文件与容器的声明

要使用 stack,首先需要包含它的头文件:

#include <stack>

注意:stack是一个容器适配器,所以声明一个stack类时需要指定他的元素类型,也可以指定他的底层容器(不设置默认为deque):

//stack命名的格式
std::stack<T, Container> stk;

// 默认使用 deque 存储 int
std::stack<int> st1;               

// 使用 vector 作为底层容器
std::stack<double, std::vector<double>> st2; 

// 使用 list 作为底层容器
std::stack<char, std::list<char>> st3;        
  • T:存储在栈中的元素类型(如 intstring 等)。

  • Container:底层容器的类型,可以是 std::deque<T>(默认)、std::vector<T>std::list<T>只要该容器支持 back()push_back()pop_back() 等操作即可

  • 注意:在指定底层容器是,最好要与他指定的元素类型适配,不然会出现编译错误。

  • 当stack中指定元素类型是一个类时(例如string是容器,但底层是类),如果这个类是我们自己写的类,后面的底层逻辑也要匹配适合的容器才行

stack中的核心接口

上面就是stack中所有的接口,第一个就是构造函数,没啥可以说的,从第二个以后来讲:

empty():判空接口

官方定义:

bool empty() const;

官方解释:

  • 测试容器是否为空
  • 返回栈是否为空:即其大小是否为零。
  • 此成员函数实际上调用了底层容器对象的成员函数 empty
// stack::empty --- 官方代码示例
#include <iostream>       // std::cout
#include <stack>          // std::stack

int main ()
{
  std::stack<int> mystack;
  int sum (0);

  for (int i=1;i<=10;i++) mystack.push(i);

  while (!mystack.empty())
  {
     sum += mystack.top();
     mystack.pop();
  }

  std::cout << "total: " << sum << '\n';

  return 0;
}

size():获取stack的大小

官方定义:

size_type size() const;

官方解释:

  • 返回大小
  • 返回栈中的元素数量。
  • 此成员函数实际上调用了底层容器对象的成员 size 函数

官方代码示例:

// stack::size
#include <iostream>       // std::cout
#include <stack>          // std::stack

int main ()
{
  std::stack<int> myints;
  std::cout << "0. size: " << myints.size() << '\n';

  for (int i=0; i<5; i++) myints.push(i);
  std::cout << "1. size: " << myints.size() << '\n';

  myints.pop();
  std::cout << "2. size: " << myints.size() << '\n';

  return 0;
}

top():获取栈顶元素

官方定义:

value_type& top();
const value_type& top() const;

官方解释:

  • 返回栈顶元素的引用。
  • 由于栈是后进先出的容器,所以栈顶元素是最后插入栈中的元素。
  • 此成员函数实际上调用了底层容器对象的成员回调函数(以vector来解释,就是返回最后一个值的引用)。

官方代码定义:

// stack::top
#include <iostream>       // std::cout
#include <stack>          // std::stack

int main ()
{
  std::stack<int> mystack;

  mystack.push(10);
  mystack.push(20);

  mystack.top() -= 5;

  std::cout << "mystack.top() is now " << mystack.top() << '\n';

  return 0;
}

push():向栈中增加元素(栈顶加入元素)

官方定义:

void push (const value_type& val);

官方解释:

  • 插入元素
  • 将一个新元素插入到栈的顶部,位于当前栈顶元素之上。
  • 此新元素的内容会被初始化为 val 的副本。
  • 此成员函数实际上调用了底层容器对象的成员函数“push_back”。

官方代码定义:

// stack::push/pop
#include <iostream>       // std::cout
#include <stack>          // std::stack

int main ()
{
  std::stack<int> mystack;

  for (int i=0; i<5; ++i) mystack.push(i);

  std::cout << "Popping out elements...";
  while (!mystack.empty())
  {
     std::cout << ' ' << mystack.top();
     mystack.pop();
  }
  std::cout << '\n';

  return 0;
}

emplace():高级点的push() --- C++11新增

官方定义:

template <class... Args> void emplace (Args&&... args);

官方解释:

  • 构造和插入元素
  • 在堆栈的顶部添加一个新元素,位于其当前顶部元素的上方。
  • 这个新元素是通过传递参数作为其构造函数的参数而就地构造的。
  • 这个成员函数有效地调用了底层容器的成员函数emplace_back,转发参数。

官方代码定义:

// stack::emplace
#include <iostream>       // std::cin, std::cout
#include <stack>          // std::stack
#include <string>         // std::string, std::getline(string)

int main ()
{
  std::stack<std::string> mystack;

  mystack.emplace ("First sentence");
  mystack.emplace ("Second sentence");

  std::cout << "mystack contains:\n";
  while (!mystack.empty())
  {
    std::cout << mystack.top() << '\n';
    mystack.pop();
  }

  return 0;
}

这里emplace与push一样的作用,区别在于前者在stack中插入类类型的元素时效率比较高,除此之外没啥别的特点。

pop():删除栈顶元素

官方定义:

void pop();

官方解释:

  • 移除顶部元素
  • 移除栈顶的元素,相当于使栈的大小减少一个单元。
  • 被移除的元素是刚刚插入到栈中的最后一个元素,其值可以通过调用“stack::top”成员函数来获取。
  • 这会调用被移除元素的析构函数(如果元素是类元素)。
  • 此成员函数实际上调用了底层容器对象的成员函数“pop_back”。

官方代码定义:

// stack::push/pop
#include <iostream>       // std::cout
#include <stack>          // std::stack

int main ()
{
  std::stack<int> mystack;

  for (int i=0; i<5; ++i) mystack.push(i);

  std::cout << "Popping out elements...";
  while (!mystack.empty())
  {
     std::cout << ' ' << mystack.top();
     mystack.pop();
  }
  std::cout << '\n';

  return 0;
}

使用stack时的常见误区

空栈直接调用 top () / pop ()

在使用stack时,最常见的错误之一就是在空栈上调用top()或pop()函数 。

这两个函数在栈为空时被调用,会导致程序崩溃,因为这属于未定义行为 。例如:

std::stack<int> stk;
int topElement = stk.top(); // 错误:空栈调用top(),未定义行为
stk.pop(); // 错误:空栈调用pop(),未定义行为

正确的做法是在调用top()和pop()之前,先使用empty()函数检查栈是否为空。

std::stack<int> stk;
if (!stk.empty()) {
    int topElement = stk.top();
    stk.pop();
}

忽视 top () 返回引用的修改风险

top()函数返回的是栈顶元素的引用,这意味着通过这个引用对栈顶元素进行赋值操作,会直接修改栈内的数据 :

std::stack<int> stk;
stk.push(10);
stk.top() = 20; // 直接修改栈顶元素,此时栈顶元素变为20

如果我们只想读取栈顶元素而非修改时,可以加const加以修饰:

const int topElement = stk.top(); // 此时topElement为只读,无法修改栈顶元素

也可以使用转接对象(就是加一个临时值来代替top):

int topElementCopy = stk.top(); // 拷贝栈顶元素的值,不会影响栈内数据

stack 没有 clear () 函数

stack并没有内置的clear()成员函数,这是很多初学者容易混淆的地方 。

当需要清空stack时,常见的做法是循环调用pop()函数,直到栈为空 。例如:

std::stack<int> stk;
stk.push(1);
stk.push(2);
stk.push(3);
while (!stk.empty()) {
    stk.pop();
}

另一种方式是利用stack的作用域生命周期 。如果stack是局部变量,当它离开作用域时,会自动调用析构函数,从而清空栈中的所有元素(stack的析构函数是调用其适配器的析构函数) :

{
    std::stack<int> stk;
    stk.push(1);
    stk.push(2);
    stk.push(3);
} // stk离开作用域,自动析构,栈被清空

当然,我们也可以自己写一个stack,自己创建clear(),但本质上自己创建clear()这个接口底层也是逐一pop()值,只是封装起来从而实现代码复用。

试图用迭代器遍历 stack

stack不支持迭代器,这是由它的设计逻辑决定的 。

stack的核心特性是后进先出,为了严格保证这一特性,它不允许对元素进行随机访问和遍历 。如果尝试使用迭代器来遍历stack,会发现根本无法实现,因为stack类并没有提供相关的接口 (自己实现的stack除外)。

当你想要遍历stack时,可以考虑把stack的值逐个赋予到另一个匹配的容器。

但你这么做,显然要评估自己是否选择了合适的数据结构,这种情况主在人为。

小结

在 C++ STL 中,stack 作为一种独特的容器适配器,闪耀着“后进先出”的简洁光芒。它并非独立容器,而是对底层容器(如 deque、vector 或 list)的巧妙封装,通过限制接口(仅从一端操作)呈现出栈的行为。这种适配器模式不仅体现了 STL 的高度复用性,也让我们能按需选择底层容器以优化性能。

stack 的接口极为精炼:push(压入栈顶)、pop(弹出栈顶)、top(访问栈顶)、empty(判空)和 size(获取大小)。所有操作均为 O(1) 时间复杂度,确保了高效处理。设计上,stack 秉持“极简接口 + 严格行为约束”的原则,像一位严谨的管家,确保数据的有序性和一致性。

正是这种专注,让 stack 在括号匹配、表达式求值、函数调用栈等“后进先出”场景中大放异彩——它总能精准地按照压入顺序反向处理,成为算法实现中不可或缺的基础工具。


本篇到这里就结束了,喜欢文章的小伙伴可以关注一下彩妙,我们下一篇再见~

更多推荐