栈是一种非常经典的 ** 后进先出(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指向新节点p,p就变成了新的栈顶。
③举个直观例子
假设当前栈里有节点 A(栈顶),头结点 s 指向 A:s → A → NULL
现在要压入新节点 B:
p是新节点B,p->data = B。p->next = s->next→p->next = A(B 指向 A)。s->next = p→s->next = B(头结点指向 B)。- 新栈结构:
s → B → A → NULL,B成为新栈顶。
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:遍历释放所有节点内存(包括头结点),是链式结构工程化实现的必要步骤,防止内存泄漏。
九、核心易错点与避坑指南
- 入栈指针操作顺序 :若先执行
s->next = p再执行p->next = s->next,会导致链表成环、原栈顶节点丢失,是新手最易犯的错误; - 内存管理 :链式栈所有动态分配的节点(头结点、数据节点)必须手动释放,
free操作不可遗漏; - 空栈判断 :始终以头结点
next是否为NULL作为判空依据,而非节点数据域。