数据结构 C语言 顺序栈
一、整体设计思路
- 模块化拆分:将接口定义、逻辑实现、功能测试分离,降低代码耦合度,便于后续修改和复用;
- 核心规则:基于数组实现,通过栈顶指针 top标识栈状态(空栈
top=-1、满栈top=MAX_SIZE-1),仅允许栈顶压栈 / 弹栈; - 健壮性设计:增加入参 NULL 检查、内存分配校验、栈空 / 栈满边界判断,避免程序崩溃和逻辑误判;
- 可扩展性:通过统一数据类型定义,一键修改栈的存储类型(int/char/ 结构体等)。
三文件核心分工:
| 文件名称 | 核心角色 | 核心作用 |
|---|---|---|
seqstack.h | 接口定义层 | 定义结构体、数据类型、函数声明,作为模块对外暴露的唯一接口 |
seqstack.c | 核心实现层 | 实现头文件中声明的所有函数,封装顺序栈的创建、压栈、弹栈等核心逻辑 |
main.c | 功能测试层 | 编写测试用例,调用接口函数,验证顺序栈所有功能的正确性,模拟实际使用场景 |
二、核心文件详细解析
2.1 头文件:seqstack.h(接口定义层)
核心作用:定规矩、做声明 —— 定义顺序栈的结构体、存储数据类型、宏常量,声明所有对外提供的函数接口,同时通过头文件保护机制避免重复包含导致的编译错误。
头文件是模块的 “说明书”,其他文件只需包含头文件,无需关心底层实现,符合面向接口编程思想。
#ifndef __SEQSTACK_H__
#define __SEQSTACK_H__
#include
#include
#include
// 宏常量:顺序栈最大容量,可按需修改
#define MAX_SIZE 10
// 统一数据类型定义:修改此处可适配char/float/结构体,实现一键扩展
typedef int Datatype;
// 顺序栈核心结构体:存储栈的状态和数据
typedef struct seqstack
{
int top; // 栈顶指针:空栈=-1,栈顶元素下标=top,满栈=MAX_SIZE-1
int max_size; // 栈的最大容量,与MAX_SIZE一致
Datatype data[MAX_SIZE]; // 静态数组:存储栈的实际数据
}seqstack_t;
// 函数声明:所有接口对外暴露,参数/返回值明确,方便调用
extern seqstack_t *CreateEmptySeqStack(void); // 创建空栈
extern int IsEmptySeqStack(seqstack_t *pstack); // 判断栈空
extern int IsFullSeqStack(seqstack_t *pstack); // 判断栈满
extern int PushSeqStack(seqstack_t *pstack, Datatype data); // 压栈
extern int PopSeqStack(seqstack_t *pstack, Datatype *pdata); // 弹栈
extern int ShowSeqStack(seqstack_t *pstack); // 遍历打印栈元素
extern int DestroySeqStack(seqstack_t **pstack); // 销毁栈
#endif // __SEQSTACK_H__
关键代码解析:
- 头文件保护宏:
#ifndef __SEQSTACK_H__+#define __SEQSTACK_H__+#endif,防止头文件被多个源文件重复包含,导致结构体 / 函数重定义编译错误; - Datatype 类型定义:将栈存储类型统一定义为 int,后续若要存储 char / 结构体,仅需修改此处,无需改动所有函数,符合高内聚低耦合;
- 栈顶指针 top:采用
top=-1作为空栈标识(工业级常用方案),栈顶元素直接对应data[top],压栈 / 弹栈操作更直观; - 函数声明规范:明确返回值(状态码 0 成功 /-1 失败)和参数,弹栈采用状态码 + 输出参数设计,解决传统返回值语义冲突问题。
2.2 实现文件:seqstack.c(核心逻辑层)
核心作用:实现头文件中声明的所有函数,是顺序栈核心功能的载体,封装了所有底层逻辑,对外隐藏实现细节。
该文件仅需包含
seqstack.h,无需关心其他调用文件,修改内部逻辑不会影响外部调用,大幅提升可维护性。
#include "seqstack.h"
// 创建空栈:成功返回栈指针,失败返回NULL(内存分配失败)
seqstack_t *CreateEmptySeqStack(void)
{
seqstack_t *pstack = (seqstack_t *)malloc(sizeof(seqstack_t));
if(pstack == NULL)
{
perror("malloc seqstack fail"); // 校验内存分配结果,避免空指针
return NULL;
}
// 初始化空栈核心状态
pstack->top = -1; // 空栈标识
pstack->max_size = MAX_SIZE; // 初始化最大容量
memset(pstack->data, 0, sizeof(pstack->data)); // 初始化数组,避免脏数据
printf("顺序栈创建成功,最大容量:%d
", MAX_SIZE);
return pstack;
}
// 判断栈空:空返回1,非空返回0,入参错误返回-1
int IsEmptySeqStack(seqstack_t *pstack)
{
if(pstack == NULL) // 所有指针入参必做NULL检查,避免解引用崩溃
{
printf("error:入参pstack为NULL(IsEmptySeqStack)
");
return -1;
}
return pstack->top == -1 ? 1 : 0;
}
// 判断栈满:满返回1,非满返回0,入参错误返回-1
int IsFullSeqStack(seqstack_t *pstack)
{
if(pstack == NULL)
{
printf("error:入参pstack为NULL(IsFullSeqStack)
");
return -1;
}
return pstack->top == pstack->max_size - 1 ? 1 : 0;
}
// 压栈:成功0,入参错误-1,栈满-2 | 核心规则:先移指针,再存数据
int PushSeqStack(seqstack_t *pstack, Datatype data)
{
if(pstack == NULL)
{
printf("error:入参pstack为NULL(PushSeqStack)
");
return -1;
}
if(IsFullSeqStack(pstack) == 1)
{
printf("warning:栈满,无法压入数据%d
", data);
return -2;
}
pstack->top++; // 栈顶指针上移
pstack->data[pstack->top] = data; // 存入新数据
printf("数据%d压栈成功
", data);
return 0;
}
// 弹栈:成功0,入参错误-1,栈空-2 | 核心规则:先取数据,再移指针
// 输出参数pdata接收弹栈数据,解决-1语义冲突bug
int PopSeqStack(seqstack_t *pstack, Datatype *pdata)
{
if(pstack == NULL || pdata == NULL) // 双指针入参都要检查
{
printf("error:入参pstack或pdata为NULL(PopSeqStack)
");
return -1;
}
if(IsEmptySeqStack(pstack) == 1)
{
printf("warning:栈空,无法弹栈
");
return -2;
}
*pdata = pstack->data[pstack->top]; // 取出栈顶数据
pstack->top--; // 栈顶指针下移
printf("数据%d弹栈成功
", *pdata);
return 0;
}
// 遍历打印:成功0,入参错误-1,栈空-2 | 从栈顶到栈底打印
int ShowSeqStack(seqstack_t *pstack)
{
if(pstack == NULL)
{
printf("error:入参pstack为NULL(ShowSeqStack)
");
return -1;
}
if(IsEmptySeqStack(pstack) == 1)
{
printf("warning:栈空,无元素可打印
");
return -2;
}
printf("栈内元素(栈顶→栈底):");
for(int i = pstack->top; i >= 0; i--)
{
printf("%d ", pstack->data[i]);
}
printf("
");
return 0;
}
// 销毁栈:成功0,入参错误-1 | 二级指针修改外部指针,避免野指针
int DestroySeqStack(seqstack_t **pstack)
{
if(pstack == NULL || *pstack == NULL) // 避免重复销毁
{
printf("error:入参pstack为NULL或栈已销毁(DestroySeqStack)
");
return -1;
}
free(*pstack); // 释放栈结构体内存
*pstack = NULL; // 外部指针置空,彻底避免野指针
printf("顺序栈销毁成功,栈指针已置空
");
return 0;
}
2.3 测试文件:main.c(功能测试层)
核心作用:作为程序入口,调用seqstack.h声明的接口函数,编写完整测试用例,验证顺序栈的所有功能是否正常,同时模拟实际业务场景的使用方式。
测试文件是代码的 “校验器”,不仅要测试正常流程,还要测试边界场景(栈空弹栈、栈满压栈、重复销毁)和异常场景(传入 NULL 指针),确保代码健壮性。
#include "seqstack.h"
int main(void)
{
seqstack_t *pstack = NULL;
Datatype pop_data; // 临时变量,接收弹栈数据(给输出参数用)
// 1. 创建空栈:增加失败判断,避免后续操作空指针
pstack = CreateEmptySeqStack();
if(pstack == NULL)
{
printf("顺序栈创建失败,程序退出
");
return -1;
}
// 2. 正常压栈测试:1-5 + 测试数据-1(验证语义冲突bug修复)
PushSeqStack(pstack, 1);
PushSeqStack(pstack, 2);
PushSeqStack(pstack, 3);
PushSeqStack(pstack, 4);
PushSeqStack(pstack, 5);
PushSeqStack(pstack, -1); // 测试传统弹栈的-1语义冲突问题
// 3. 遍历打印栈内元素
ShowSeqStack(pstack);
// 4. 正常弹栈测试
PopSeqStack(pstack, &pop_data);
ShowSeqStack(pstack);
// 5. 边界场景1:栈满压栈(MAX_SIZE=10,继续压6-11)
for(int i = 6; i <= 11; i++)
{
PushSeqStack(pstack, i);
}
// 6. 边界场景2:循环弹栈至空
while(IsEmptySeqStack(pstack) == 0)
{
PopSeqStack(pstack, &pop_data);
}
// 7. 边界场景3:栈空弹栈(验证异常处理)
PopSeqStack(pstack, &pop_data);
// 8. 边界场景4:重复销毁栈(验证异常处理)
DestroySeqStack(&pstack);
DestroySeqStack(&pstack);
return 0;
}
三、编译运行步骤(跨平台通用)
这套代码是跨平台的,可在 Linux/macOS(GCC)、Windows(MinGW/Dev-C++/VS)下编译运行,核心编译命令一致。
3.1 编译环境
- Linux/macOS:自带 GCC 编译器,直接在终端执行命令;
- Windows:安装 MinGW(轻量)或 Dev-C++/VS(集成环境),配置环境变量后即可使用 GCC 命令。
3.2 编译运行命令
将seqstack.h、seqstack.c、main.c放在同一目录,打开终端 / 命令行,执行以下命令:
# 编译:将seqstack.c和main.c编译为可执行文件seqstack,-g添加调试信息(可选)
gcc seqstack.c main.c -o seqstack -g
# 运行:Linux/macOS
./seqstack
# 运行:Windows
seqstack.exe
3.3 预期运行结果
顺序栈创建成功,最大容量:10
数据1压栈成功
数据2压栈成功
数据3压栈成功
数据4压栈成功
数据5压栈成功
数据-1压栈成功
栈内元素(栈顶→栈底):-1 5 4 3 2 1
数据-1弹栈成功
栈内元素(栈顶→栈底):5 4 3 2 1
数据6压栈成功
数据7压栈成功
数据8压栈成功
数据9压栈成功
数据10压栈成功
warning:栈满,无法压入数据11
数据5弹栈成功
数据4弹栈成功
数据3弹栈成功
数据2弹栈成功
数据1弹栈成功
数据6弹栈成功
数据7弹栈成功
数据8弹栈成功
数据9弹栈成功
数据10弹栈成功
warning:栈空,无法弹栈
顺序栈销毁成功,栈指针已置空
error:入参pstack为NULL或栈已销毁(DestroySeqStack)
结果解析:
- 所有正常操作均执行成功,-1 正常压栈 / 弹栈,证明语义冲突 bug已修复;
- 栈满时压入 11 失败,栈空时弹栈失败,均打印警告而非崩溃,证明边界防护有效;
- 重复销毁栈时打印错误提示,证明入参检查和防重复操作有效;
- 全程无崩溃、无乱码,证明内存管理和数组操作规范。
四、三文件模块化开发的优势
为什么不把所有代码写在一个main.c文件中,而是要拆分为三个文件?这是 C 语言开发的工业级规范,核心优势有 3 点:
- 代码复用:
seqstack.h+seqstack.c可作为独立的顺序栈模块,在其他项目中直接引入,无需重复编写代码; - 易维护性:修改顺序栈逻辑时,仅需改动
seqstack.c,无需改动测试文件或其他调用文件;修改存储类型时,仅需改动seqstack.h的Datatype; - 低耦合度:调用方(
main.c)仅需通过头文件的接口函数操作栈,无需关心底层实现(数组 / 链表),即使后续将顺序栈改为链式栈,调用方代码也无需改动; - 适合团队开发:一人负责模块实现(
seqstack.c),一人负责测试(main.c),分工明确,避免代码冲突。
五、新手必避的 4 个核心坑
结合本文代码和顺序栈的实现经验,总结 4 个新手最易踩的坑,避坑即提升代码质量:
- 栈顶指针操作顺序颠倒:压栈先存数后移指针、弹栈先移指针再取数,会导致数据覆盖或丢失,牢记压栈先移后存,弹栈先取后移;
- 解引用空指针:所有指针入参必做
NULL检查,尤其是函数的入口参数,这是 C 语言程序崩溃的最常见原因; - 弹栈返回值语义冲突:不要用业务数据的范围作为错误码(如 int 返回 - 1),优先采用状态码 + 输出参数的设计,这是工业级通用方案;
- 忽略内存管理:
malloc后不校验、free后不置空,会导致空指针、野指针和内存泄漏,牢记malloc 必检查,free 必置空。







