欢迎光临
我们一直在努力

数据结构 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 <stdio.h>
    #include <stdlib.h>
    #include <string.h>

    // 宏常量:顺序栈最大容量,可按需修改
    #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\\n", MAX_SIZE);
    return pstack;
    }

    // 判断栈空:空返回1,非空返回0,入参错误返回-1
    int IsEmptySeqStack(seqstack_t *pstack)
    {
    if(pstack == NULL) // 所有指针入参必做NULL检查,避免解引用崩溃
    {
    printf("error:入参pstack为NULL(IsEmptySeqStack)\\n");
    return -1;
    }
    return pstack->top == -1 ? 1 : 0;
    }

    // 判断栈满:满返回1,非满返回0,入参错误返回-1
    int IsFullSeqStack(seqstack_t *pstack)
    {
    if(pstack == NULL)
    {
    printf("error:入参pstack为NULL(IsFullSeqStack)\\n");
    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)\\n");
    return -1;
    }
    if(IsFullSeqStack(pstack) == 1)
    {
    printf("warning:栈满,无法压入数据%d\\n", data);
    return -2;
    }
    pstack->top++; // 栈顶指针上移
    pstack->data[pstack->top] = data; // 存入新数据
    printf("数据%d压栈成功\\n", 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)\\n");
    return -1;
    }
    if(IsEmptySeqStack(pstack) == 1)
    {
    printf("warning:栈空,无法弹栈\\n");
    return -2;
    }
    *pdata = pstack->data[pstack->top]; // 取出栈顶数据
    pstack->top–; // 栈顶指针下移
    printf("数据%d弹栈成功\\n", *pdata);
    return 0;
    }

    // 遍历打印:成功0,入参错误-1,栈空-2 | 从栈顶到栈底打印
    int ShowSeqStack(seqstack_t *pstack)
    {
    if(pstack == NULL)
    {
    printf("error:入参pstack为NULL(ShowSeqStack)\\n");
    return -1;
    }
    if(IsEmptySeqStack(pstack) == 1)
    {
    printf("warning:栈空,无元素可打印\\n");
    return -2;
    }
    printf("栈内元素(栈顶→栈底):");
    for(int i = pstack->top; i >= 0; i–)
    {
    printf("%d ", pstack->data[i]);
    }
    printf("\\n");
    return 0;
    }

    // 销毁栈:成功0,入参错误-1 | 二级指针修改外部指针,避免野指针
    int DestroySeqStack(seqstack_t **pstack)
    {
    if(pstack == NULL || *pstack == NULL) // 避免重复销毁
    {
    printf("error:入参pstack为NULL或栈已销毁(DestroySeqStack)\\n");
    return -1;
    }
    free(*pstack); // 释放栈结构体内存
    *pstack = NULL; // 外部指针置空,彻底避免野指针
    printf("顺序栈销毁成功,栈指针已置空\\n");
    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("顺序栈创建失败,程序退出\\n");
    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 必置空。
  • 赞(0)
    未经允许不得转载:171主机测评 » 数据结构 C语言 顺序栈
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址