【数据结构实战】C语言实现栈的链式存储:从初始化到销毁,手把手教你写可运行代码

栈是一种非常经典的 ** 后进先出(LIFO)** 线性数据结构。在 C 语言中,栈有两种常见实现方式:顺序栈(数组实现)链式栈(链表实现)

本文带你从零实现链式栈,包含初始化、入栈、出栈、取栈顶、遍历、销毁等完整操作,代码可直接编译运行,非常适合数据结构入门学习。

一、链式栈简介

链式栈使用单链表 实现,不需要预先分配固定大小空间,支持动态扩容,没有栈满溢出问题(除非内存耗尽)。

特点:

  • 用链表节点存储数据
  • 栈顶在链表头部,入栈、出栈都是 O (1)
  • 带头结点,操作更统一、更安全
  • 手动管理内存,避免内存泄漏

二、 链式栈基础定义

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

// 定义栈中存储的元素类型,可根据需求修改为char、float等
typedef int ElemType;
 
// 链式栈节点结构体定义
typedef struct stack {
    // 数据域:存储节点的元素值
    ElemType data;
    // 指针域:指向后继节点(下一个节点)
    struct stack *next;
} Stack;

三、 栈的链式结构实现------初始化

cpp 复制代码
// 初始化链式栈(创建头结点,返回栈指针)
Stack* initStack()
{
    // 动态分配头结点内存(头结点不存储有效数据,仅作为链表入口)
    Stack *s = (Stack*)malloc(sizeof(Stack));
    // 头结点数据域可设为0(仅占位,无实际意义)
    s->data = 0;
    // 空栈时,头结点next指针指向NULL
    s->next = NULL;
    // 返回初始化后的栈指针
    return s;
}

四、栈的链式结构实现------栈是否为空

cpp 复制代码
// 判断栈是否为空,空则返回1,否则返回0
int isEmpty(Stack *s)
{
    // 空栈标志:头结点的next指针为NULL(无有效节点)
    if (s->next == NULL)
    {
        printf("空的\n");
        return 1;
    }
    else
    {
        return 0;
    }
}

五、 栈的链式结构实现-进栈/压栈

cpp 复制代码
// 入栈操作:将元素e压入栈顶
int push(Stack *s, ElemType e)
{
    // 1. 动态分配新节点内存
    Stack *p = (Stack*)malloc(sizeof(Stack));
    // 2. 为新节点赋值数据
    p->data = e;
    // 3. 新节点next指向原栈顶节点(防止链表断裂)
    p->next = s->next;
    // 4. 头结点next指向新节点,新节点成为新栈顶
    s->next = p;
    // 返回1表示入栈成功
    return 1;
}

1.关键逻辑解释
① 链式栈的结构

链式栈通常用带头结点的单链表实现:

  • s:头结点指针,本身不存数据,只用来管理栈。
  • s->next:永远指向栈顶节点(第一个有效数据节点)。
  • 每个节点包含 data(数据域)和 next(指针域,指向下一个节点)。
②为什么要这样插入?

栈的规则是后进先出,所以新元素必须放在链表最前面(栈顶):

  • p->next = s->next:让新节点 p 先 "接住" 原来的栈顶节点,保证链表不断。
  • s->next = p:再让头结点 s 指向新节点 pp 就变成了新的栈顶。
③举个直观例子

假设当前栈里有节点 A(栈顶),头结点 s 指向 As → A → NULL

现在要压入新节点 B

  1. p 是新节点 Bp->data = B
  2. p->next = s->nextp->next = A(B 指向 A)。
  3. s->next = ps->next = B(头结点指向 B)。
  4. 新栈结构:s → B → A → NULLB 成为新栈顶。

2.为什么要先 p->next = s->next,再 s->next = p

如果顺序写反:

cpp 复制代码
s->next = p;  // 先让头结点指向p
p->next = s->next;  // 此时s->next已经是p,p->next=p,形成环,链表断裂!

这会导致原来的栈顶节点丢失,链表变成环,所以必须先让新节点指向旧栈顶,再更新头结点指针。


3.整体作用总结

  • malloc:为新元素申请一个节点空间。
  • p->data = e:把要入栈的数据存到新节点里。
  • p->next = s->next:新节点连接到原栈顶,保证链表不断。
  • s->next = p:头结点指向新节点,新节点成为栈顶。
  • return 1:表示入栈成功(链式栈一般不会满,所以很少失败)。

六、栈的链式结构实现-出栈

cpp 复制代码
// 出栈操作:弹出栈顶元素,通过*e带回值
int pop(Stack *s, ElemType *e)
{
    // 先判断栈是否为空
    if (s->next == NULL)
    {
        printf("空的\n");
        return 0;
    }
    // 1. 保存栈顶节点数据到*e
    *e = s->next->data;
    // 2. 用临时指针q指向待删除的栈顶节点
    Stack *q = s->next;
    // 3. 头结点next指向原栈顶的下一个节点(更新栈顶)
    s->next = q->next;
    // 4. 释放原栈顶节点内存(避免内存泄漏)
    free(q);
    // 返回1表示出栈成功
    return 1;
}

七、链式栈完整可运行代码

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

// 定义栈元素类型
typedef int ElemType;

// 链式栈节点结构体
typedef struct stack {
    ElemType data;      // 数据域
    struct stack *next; // 指针域,指向后继节点
} Stack;

// -------------------------- 初始化栈 --------------------------
// 功能:创建链式栈的头结点,初始化空栈
// 返回值:指向头结点的栈指针
Stack* initStack()
{
    // 分配头结点内存(头结点不存储有效数据)
    Stack *s = (Stack*)malloc(sizeof(Stack));
    if (s == NULL) { // 内存分配失败检查(健壮性处理)
        printf("内存分配失败!\n");
        exit(1);
    }
    s->data = 0;       // 头结点数据域无意义,赋值为0
    s->next = NULL;    // 空栈:头结点next指向NULL
    return s;
}

// -------------------------- 判断栈是否为空 --------------------------
// 功能:检查栈中是否存在有效节点
// 参数:s - 栈指针(指向头结点)
// 返回值:1=空栈,0=非空栈
int isEmpty(Stack *s)
{
    if (s->next == NULL)
    {
        printf("空的\n");
        return 1;
    }
    else
    {
        return 0;
    }
}

// -------------------------- 入栈(压栈) --------------------------
// 功能:将元素e插入栈顶(链表头部插入)
// 参数:s - 栈指针,e - 待入栈元素
// 返回值:1=成功,0=失败(内存不足时)
int push(Stack *s, ElemType e)
{
    // 分配新节点内存
    Stack *p = (Stack*)malloc(sizeof(Stack));
    if (p == NULL) {
        printf("内存分配失败,入栈失败!\n");
        return 0;
    }
    p->data = e;        // 新节点赋值
    p->next = s->next;  // 新节点先连接原栈顶(防止断链)
    s->next = p;        // 头结点指向新节点,新节点成为栈顶
    return 1;
}

// -------------------------- 出栈(弹栈) --------------------------
// 功能:删除栈顶节点,并将其值通过指针e返回
// 参数:s - 栈指针,e - 存储出栈元素的指针
// 返回值:1=成功,0=失败(栈空)
int pop(Stack *s, ElemType *e)
{
    if (isEmpty(s)) {
        return 0;
    }
    *e = s->next->data; // 保存栈顶节点数据
    Stack *q = s->next; // 临时指针指向待删除节点
    s->next = q->next;  // 头结点跳过待删除节点,更新栈顶
    free(q);            // 释放节点内存(必须操作,避免内存泄漏)
    return 1;
}

// -------------------------- 获取栈顶元素 --------------------------
// 功能:读取栈顶元素值,不删除节点
// 参数:s - 栈指针,e - 存储栈顶元素的指针
// 返回值:1=成功,0=失败(栈空)
int getTop(Stack *s, ElemType *e)
{
    if (isEmpty(s)) {
        return 0;
    }
    *e = s->next->data; // 仅读取,不修改栈结构
    return 1;
}

// -------------------------- 遍历打印栈 --------------------------
// 功能:从栈顶到栈底打印所有元素
// 参数:s - 栈指针
void printStack(Stack *s)
{
    if (isEmpty(s)) {
        return;
    }
    printf("栈元素(栈顶→栈底):");
    Stack *p = s->next; // p从栈顶开始遍历
    while (p != NULL)
    {
        printf("%d ", p->data);
        p = p->next; // 移动到下一个节点
    }
    printf("\n");
}

// -------------------------- 销毁栈 --------------------------
// 功能:释放所有节点内存,避免内存泄漏
// 参数:s - 栈指针的指针(需要修改原栈指针)
void destroyStack(Stack **s)
{
    Stack *p = *s;
    while (p != NULL)
    {
        *s = p->next; // 先保存下一个节点
        free(p);       // 释放当前节点
        p = *s;        // 移动到下一个节点
    }
}

// -------------------------- 主函数测试 --------------------------
int main()
{
    // 1. 初始化链式栈
    Stack *s = initStack();
    
    // 2. 入栈测试
    push(s, 10);
    push(s, 20);
    push(s, 30);
    printStack(s); // 预期输出:30 20 10
    
    // 3. 出栈测试
    ElemType e;
    pop(s, &e);
    printf("出栈元素:%d\n", e); // 预期输出:30
    printStack(s);             // 预期输出:20 10
    
    // 4. 获取栈顶测试
    getTop(s, &e);
    printf("当前栈顶元素:%d\n", e); // 预期输出:20
    
    // 5. 销毁栈(释放内存)
    destroyStack(&s);
    return 0;
}

运行结果截图

八、总结核心要点

1. 初始化(initStack)

  • 动态分配头结点内存,检查内存分配是否成功(健壮性处理);
  • 头结点next置为NULL标志空栈,数据域赋值为 0 仅作占位,无实际意义。

2. 判空(isEmpty)

  • 核心判断条件:头结点next == NULL,直接反映栈内无有效节点,是所有操作的前置检查条件。

3. 入栈(push)

  • 采用头插法 实现 O (1) 时间复杂度的入栈,核心步骤不可颠倒:① 先让新节点p->next = s->next(连接原栈顶,避免链表断裂);② 再让头结点s->next = p(更新栈顶为新节点);
  • 必须检查malloc返回值,防止内存分配失败导致程序异常。

4. 出栈(pop)

  • 前置检查栈是否为空,避免空栈操作;
  • 核心步骤:保存栈顶数据→用临时指针指向栈顶节点→头结点跳过该节点→释放节点内存(关键!避免内存泄漏);
  • 通过指针*e带回出栈元素,区分 "操作状态" 和 "元素值",消除返回值歧义。

5. 补充操作(getTop/printStack/destroyStack)

  • getTop:仅读取栈顶数据,不修改栈结构,逻辑与出栈一致但不释放节点;
  • printStack:从栈顶到栈底遍历,直观验证栈的存储状态;
  • destroyStack:遍历释放所有节点内存(包括头结点),是链式结构工程化实现的必要步骤,防止内存泄漏。

九、核心易错点与避坑指南

  1. 入栈指针操作顺序 :若先执行s->next = p再执行p->next = s->next,会导致链表成环、原栈顶节点丢失,是新手最易犯的错误;
  2. 内存管理 :链式栈所有动态分配的节点(头结点、数据节点)必须手动释放,free操作不可遗漏;
  3. 空栈判断 :始终以头结点next是否为NULL作为判空依据,而非节点数据域。
相关推荐
m0_488633321 小时前
C语言变量命名规则、入门自学、运算符优先级及数据结构介绍
c语言·数据结构·运算符优先级·变量命名·入门自学
左左右右左右摇晃1 小时前
数据结构——栈
数据结构·笔记
左左右右左右摇晃2 小时前
数据结构——树
数据结构·笔记
Book思议-2 小时前
【数据结构实战】川剧 “扯脸” 与栈的 LIFO 特性 :用 C 语言实现 3 种栈结构
c语言·数据结构·算法·
3GPP仿真实验室2 小时前
【MATLAB源码】感知:CFAR 检测算法库
算法·matlab·目标跟踪
fengenrong2 小时前
20260324
c++·算法
qq_416018722 小时前
设计模式在C++中的实现
开发语言·c++·算法
倾心琴心2 小时前
【agent辅助pcb routing coding学习】实践9 CU GR 代码 算法学习
算法·agent·pcb·eda·routing
数据智能老司机2 小时前
谷歌 TurboQuant 深度拆解:LLM 内存压缩 6 倍、推理加速 8 倍、零精度损失,它是怎么做到的?
算法