栈的定义与顺序实现:栈的概念与特性(LIFO)

说到栈,我脑子里第一个蹦出来的画面就是食堂里那摞餐盘。你想想看,后来放上去的盘子总是最先被拿走,最早放进去的反倒压在最底下。这就是栈最核心的特性——后进先出(LIFO, Last In First Out)

我在做嵌入式协议栈的时候,经常用栈来保存函数调用的返回地址。每次调用子函数,就把返回地址压进去;函数返回时,再从栈顶弹出。这个机制要是搞错了,程序跑飞那是分分钟的事。

栈的抽象数据类型

栈说白了就是一种操作受限的线性表。它只允许在一端进行插入和删除操作,这一端我们叫它栈顶(Top),另一端叫栈底(Bottom)

一个标准的栈应该提供以下操作:

操作 描述
InitStack 初始化一个空栈
Push 将元素压入栈顶
Pop 弹出栈顶元素
Peek / Top 获取栈顶元素但不弹出
IsEmpty 判断栈是否为空
IsFull 判断栈是否已满(顺序栈特有)
Size 获取栈中元素个数

核心要点:栈的所有操作都围绕栈顶展开。你没法直接访问栈底的元素,必须先把上面的元素全部弹出才行。这就是LIFO的约束力。

顺序栈的实现

顺序栈就是用数组来模拟栈。我习惯用一个结构体来封装栈的数据和状态,这样代码更清晰,也方便移植。

#define MAX_SIZE 100

typedef struct {
    int data[MAX_SIZE];  // 存放栈元素的数组
    int top;             // 栈顶指针,指向当前栈顶元素的位置
} SeqStack;

这里有个细节要注意:top 的初始值怎么设?我个人习惯设成 -1,表示空栈。当压入第一个元素时,top 变成 0,元素放在 data[0]。这样 top 始终指向栈顶元素的位置。

入栈操作(Push)

入栈就是把新元素放到栈顶。逻辑很简单:先检查栈是否满了,没满就把 top 加 1,然后把元素放进去。

int Push(SeqStack *stack, int value) {
    if (stack->top >= MAX_SIZE - 1) {
        printf("栈已满,无法入栈\n");
        return -1;  // 返回错误码
    }
    stack->data[++stack->top] = value;
    return 0;  // 成功
}

你看,++stack->top 这个写法,先自增再赋值。嗯,这里要注意:如果你写成 stack->data[stack->top++] = value,那就变成先赋值再自增,逻辑就全乱了。我曾经在调试一个通信协议栈时,就因为这种细节没注意,导致数据错位,排查了整整一个下午。

出栈操作(Pop)

出栈是从栈顶取出元素。同样先检查栈是否为空,不为空就把 top 减 1,返回原来的栈顶元素。

int Pop(SeqStack *stack, int *value) {
    if (stack->top == -1) {
        printf("栈已空,无法出栈\n");
        return -1;
    }
    *value = stack->data[stack->top--];
    return 0;
}

这里我用的是传指针的方式把弹出的值带出来。为什么不用返回值?因为返回值我已经用来表示操作状态了(0 成功,-1 失败)。这是C语言里很常见的做法——用返回值表示状态,用参数传递数据。

小技巧:出栈时其实不需要真的把数据从数组里清掉,只要把 top 减 1 就行。下次入栈时,新数据会直接覆盖掉旧数据。省掉一次赋值操作,在嵌入式环境里能省一点是一点。

辅助操作

除了入栈出栈,还有几个辅助函数也很常用:

// 获取栈顶元素,不弹出
int Peek(SeqStack *stack, int *value) {
    if (stack->top == -1) {
        return -1;
    }
    *value = stack->data[stack->top];
    return 0;
}

// 判断栈是否为空
int IsEmpty(SeqStack *stack) {
    return stack->top == -1;
}

// 判断栈是否已满
int IsFull(SeqStack *stack) {
    return stack->top >= MAX_SIZE - 1;
}

// 获取栈中元素个数
int Size(SeqStack *stack) {
    return stack->top + 1;
}

避坑指南:我曾经在一个项目里,因为忘记在初始化时把 top 设成 -1,导致第一次入栈时数据放到了 data[0]top 还是 0,第二次入栈时直接覆盖了第一次的数据。这种 bug 很难查,因为代码逻辑看起来都对,就是初始化漏了一行。所以,初始化一定要做彻底

顺序栈的完整示例

下面是一个完整的顺序栈使用示例:

#include <stdio.h>

#define MAX_SIZE 100

typedef struct {
    int data[MAX_SIZE];
    int top;
} SeqStack;

void InitStack(SeqStack *stack) {
    stack->top = -1;
}

int Push(SeqStack *stack, int value) {
    if (stack->top >= MAX_SIZE - 1) return -1;
    stack->data[++stack->top] = value;
    return 0;
}

int Pop(SeqStack *stack, int *value) {
    if (stack->top == -1) return -1;
    *value = stack->data[stack->top--];
    return 0;
}

int main() {
    SeqStack stack;
    InitStack(&stack);
    
    Push(&stack, 10);
    Push(&stack, 20);
    Push(&stack, 30);
    
    int val;
    while (Pop(&stack, &val) == 0) {
        printf("弹出: %d\n", val);
    }
    // 输出顺序: 30, 20, 10
    return 0;
}

运行结果会依次打印 30、20、10。你看,后进去的 30 最先出来,这就是 LIFO 的直观体现。

知识体系图

下面这张图帮你理清顺序栈的核心脉络:

顺序栈知识体系 概念与特性 • LIFO 后进先出 • 只允许栈顶操作 • 栈顶指针 top • 栈底固定不变 数据结构 • 数组 + top 指针 • top = -1 表示空栈 • 固定容量 MAX_SIZE • 下标从 0 开始 核心操作 • Push: top++ 后赋值 • Pop: 取值后 top-- • Peek: 只取值不移动 • 判空/判满/求大小 常见陷阱 1. 初始化忘记设 top = -1 → 数据错位 2. Push 时先赋值再自增 → 覆盖旧数据 3. Pop 时不判空 → 访问非法内存 4. 栈满继续 Push → 数组越界

顺序栈的实现思路很直白,但越简单的东西越容易在细节上翻车。你写代码时多留个心眼,把边界条件都考虑清楚,基本就稳了。


公众号:蓝海资料掘金营,微信deep3321