1. 项目概述与学习思路
1.1 为什么学习C++要从栈和队列开始
我记得第一次接触C++的时候,被指针、引用、内存管理这些东西折磨得不轻。等到自以为把语法啃得差不多了,写出来的程序却总感觉哪里不对——逻辑不清晰、代码杂乱、遇到复杂问题就不知道从哪里下手。后来才明白,缺的那块拼图正是数据结构。
栈(Stack)和队列(Queue)是数据结构里最基础也最实用的两种线性结构。说它们基础,是因为只需要掌握几个核心操作就能完全理解;说它们实用,是因为在操作系统、编译器、网络通信、图形界面开发等几乎所有软件系统中,都能看到它们的身影。整个C++标准库的设计里也处处渗透着这两种结构的思维。
这篇文章我会从零开始,把栈和队列的本质讲透,带着你用C++从底层亲手实现一遍,再看它们在实际项目中到底是怎么用的。无论你是刚学完C++基础语法的小白,还是准备面试的求职者,或者只是想把数据结构的坑补上,这篇文章都能给你一条清晰的学习路径。
1.2 一个类比帮你建立直觉
先抛开枯燥的定义,用生活场景建立直觉。
栈就像一摞盘子。你在饭店后厨看到的那种弹簧托盘架——你放上去一个盘子,它落在最上面;你要拿盘子,只能从最上面拿。后放上去的盘子总是先被拿走。这个特点叫“后进先出”(Last In First Out,LIFO)。
队列就像排队买奶茶。新来的人排在队尾,队头的人先买到奶茶离开。先来的人先被服务。这个特点叫“先进先出”(First In First Out,FIFO)。
就是这么简单的两个概念。但简单不意味着浅薄——正是这两种最简单的规则,构成了无数复杂算法和系统设计的基石。你把这两个直觉记在脑子里,后面所有的代码、场景、代码层面的细节,都不过是这两个直觉的具象化。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心概念与底层原理拆解
2.1 栈的定义与核心操作
栈是只允许在一端进行插入和删除操作的线性表。这一端被称为栈顶(Top),另一端被称为栈底(Bottom)。当栈里没有元素时,称为空栈。
栈的核心操作只有四个:
push(x):将元素 x 压入栈顶pop():弹出栈顶元素top():获取栈顶元素的值(不弹出)empty():判断栈是否为空
很多教材还会提一个 size() 操作,返回栈中元素个数,其实是用一个计数变量就能搞定的事。
这里有一个新手容易犯的认知偏差:栈的操作看似简单,但“只能在栈顶操作”这个限制恰恰是它的灵魂所在。这个限制意味着你无法随意访问中间的元素——想要访问栈底的元素,必须先把上面的所有元素都弹出去。这种受限访问换来的好处是操作的时间复杂度全部是 O(1),而且不会出现元素位置错乱的问题。
2.2 队列的定义与核心操作
队列是只允许在一端进行插入、在另一端进行删除操作的线性表。允许插入的一端叫队尾(Rear),允许删除的一端叫队头(Front)。同样,当队列中没有元素时称为空队列。
队列的核心操作:
enqueue(x):将元素 x 插入队尾dequeue():删除队头元素front():获取队头元素的值(不删除)empty():判断队列是否为空
初学者最容易混淆的一点是:队列和栈一样,也不允许从中间操作元素。但栈的插入删除在同一端,队列在两段。这个区别直接决定了两种结构适用的场景完全不同——栈适合做“需要回溯”的场景,队列适合做“需要公平排队”的场景。
2.3 底层存储方案:数组 vs 链表
实现栈和队列,主要有两条路线:数组(顺序存储)和链表(链式存储)。这两种方案没有绝对的好坏,取决于你的使用场景。
数组实现的核心思路是:预先分配一块连续的内存,用下标来追踪栈顶或队头队尾的位置。优点是随机访问快、缓存命中率高、实现简单;缺点是容量固定,需要扩容时就比较麻烦,而且扩容涉及数据的整体搬移。
链表实现的核心思路是:每个节点保存数据和指向下一个节点的指针,用头指针和尾指针维护结构。优点是不需要预先分配大块内存,元素个数可以动态增长;缺点是每个节点需要额外的指针空间,而且节点的内存不连续,对缓存的利用不如数组友好。
一个经常被问到的问题是:既然链表也能实现,为什么很多场景仍然优先用数组?我的看法是,现代计算机的体系结构下,顺序内存访问的速度优势非常明显,数组的连续内存天然有利于CPU缓存预取。所以在大多数应用场景里,数组实现的栈和队列性能更好。链表的价值更多体现在“不知道数据量上限”和“需要频繁插入删除”的场景中。
3. 手把手实现:C++代码实战
3.1 基于数组的栈实现
从最直观的数组栈开始。我用一个动态数组来模拟栈的伸缩,这样可以避开“固定容量”的尴尬。
cpp复制#include <iostream>
#include <stdexcept>
template <typename T>
class ArrayStack {
private:
T* data; // 底层数组
int capacity; // 当前容量
int topIndex; // 栈顶下标,-1表示空栈
void resize(int newCapacity) {
T* newData = new T[newCapacity];
for (int i = 0; i <= topIndex; i++) {
newData[i] = data[i];
}
delete[] data;
data = newData;
capacity = newCapacity;
}
public:
ArrayStack() : capacity(8), topIndex(-1) {
data = new T[capacity];
}
~ArrayStack() {
delete[] data;
}
void push(const T& value) {
if (topIndex + 1 >= capacity) {
resize(capacity * 2); // 容量翻倍
}
data[++topIndex] = value;
}
void pop() {
if (empty()) {
throw std::underflow_error("Stack underflow: pop on empty stack");
