数据结构 C语言 顺序栈

一、整体设计思路

  1. 模块化拆分 :将接口定义、逻辑实现、功能测试分离,降低代码耦合度,便于后续修改和复用;
  2. 核心规则 :基于数组实现,通过栈顶指针 top 标识栈状态(空栈top=-1、满栈top=MAX_SIZE-1),仅允许栈顶压栈 / 弹栈;
  3. 健壮性设计:增加入参 NULL 检查、内存分配校验、栈空 / 栈满边界判断,避免程序崩溃和逻辑误判;
  4. 可扩展性:通过统一数据类型定义,一键修改栈的存储类型(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__

关键代码解析

  1. 头文件保护宏#ifndef __SEQSTACK_H__ + #define __SEQSTACK_H__ + #endif,防止头文件被多个源文件重复包含,导致结构体 / 函数重定义编译错误;
  2. Datatype 类型定义 :将栈存储类型统一定义为 int,后续若要存储 char / 结构体,仅需修改此处,无需改动所有函数,符合高内聚低耦合
  3. 栈顶指针 top :采用top=-1作为空栈标识(工业级常用方案),栈顶元素直接对应data[top],压栈 / 弹栈操作更直观;
  4. 函数声明规范 :明确返回值(状态码 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.hseqstack.cmain.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. 所有正常操作均执行成功,-1 正常压栈 / 弹栈,证明语义冲突 bug已修复;
  2. 栈满时压入 11 失败,栈空时弹栈失败,均打印警告而非崩溃,证明边界防护有效;
  3. 重复销毁栈时打印错误提示,证明入参检查防重复操作有效;
  4. 全程无崩溃、无乱码,证明内存管理数组操作规范。

四、三文件模块化开发的优势

为什么不把所有代码写在一个main.c文件中,而是要拆分为三个文件?这是 C 语言开发的工业级规范,核心优势有 3 点:

  1. 代码复用seqstack.h+seqstack.c可作为独立的顺序栈模块,在其他项目中直接引入,无需重复编写代码;
  2. 易维护性 :修改顺序栈逻辑时,仅需改动seqstack.c,无需改动测试文件或其他调用文件;修改存储类型时,仅需改动seqstack.hDatatype
  3. 低耦合度 :调用方(main.c)仅需通过头文件的接口函数操作栈,无需关心底层实现(数组 / 链表),即使后续将顺序栈改为链式栈,调用方代码也无需改动;
  4. 适合团队开发 :一人负责模块实现(seqstack.c),一人负责测试(main.c),分工明确,避免代码冲突。

五、新手必避的 4 个核心坑

结合本文代码和顺序栈的实现经验,总结 4 个新手最易踩的坑,避坑即提升代码质量:

  1. 栈顶指针操作顺序颠倒 :压栈先存数后移指针、弹栈先移指针再取数,会导致数据覆盖或丢失,牢记压栈先移后存,弹栈先取后移
  2. 解引用空指针 :所有指针入参必做NULL检查,尤其是函数的入口参数,这是 C 语言程序崩溃的最常见原因;
  3. 弹栈返回值语义冲突 :不要用业务数据的范围作为错误码(如 int 返回 - 1),优先采用状态码 + 输出参数的设计,这是工业级通用方案;
  4. 忽略内存管理malloc后不校验、free后不置空,会导致空指针、野指针和内存泄漏,牢记malloc 必检查,free 必置空
相关推荐
heartbeat..2 小时前
Redis 性能优化全指南:从基础配置到架构升级
java·redis·性能优化·架构
m0_748233172 小时前
C#与C语言:5大核心语法共性
java·jvm·算法
JavaGuide2 小时前
推荐一个基于 Spring Boot 4.0 + Java 21 + Spring AI 2.0 的大模型项目!
java·spring boot·spring
Maynor9962 小时前
Clawdbot安装教程:从零开始到接入飞书
java·数据库·飞书
小北方城市网2 小时前
Spring Boot 多数据源与事务管理实战:主从分离、动态切换与事务一致性
java·开发语言·jvm·数据库·mysql·oracle·mybatis
日拱一卒——功不唐捐2 小时前
循环队列+双端队列(C语言)
c语言·数据结构
进击的小头2 小时前
设计模式组合应用:嵌入式通信协议栈
c语言·设计模式·策略模式
养军博客2 小时前
C语言五天速成(可用于蓝桥杯备考 难度中等偏下)
c语言·算法·蓝桥杯
roman_日积跬步-终至千里2 小时前
【Java 并发-面试】从线程基础到企业级开发的知识点概况
java·开发语言