恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

链栈C语言实现详解:从结构设计到完整代码与避坑指南

  • 首页
  • 资讯中心
  • /
  • 链栈C语言实现详解:从结构设计到完整代码与避坑指南

相关资讯

STM32库函数避障小车实战:从工程编译到实地调通 2026/10/15 18:11:34
毕业论文一站式神器|真心安利 Okbiye,写论文的同学一定要收好[特殊字符] 2026/10/15 18:11:34
FastLED 3.x 版本演进全解析:从 FastLED2 到 3.3.2 的芯片支持、平台移植与性能特性(附 HoloCubic 实战用法) 2026/10/15 18:11:34

最新资讯

MYSQL连接设置
FPGA实战:VGA时序、I2C配置与SD卡帧缓存全链路
OceanStor 5300V5 Quorum Server 部署与仲裁服务详解
3分钟跑通CORAL:从安装到启动你的第一个多智能体实验完全指南
Pixhawk V4 Linux驱动配置:从ttyACM0权限到udev规则全流程
Python入门必做:用Pygame实现见缝插针小游戏

今日推荐

AI视频生成的时间一致性难题与时空建模演进
基于bbox的PDF文档解析:多栏重排与水印过滤实战
探矿数据进RAG前的清洗全攻略:TXT、Word、PDF、网页四类文档处理实践

本周热门

UE动画修改实战:从资产编辑到重定向与蒙太奇驱动
统计随机数生成器攻击下的KLJN安全密钥交换协议Matlab仿真
政务API安全治理:资产测绘、低代码编排与行标对标实践

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

链栈C语言实现详解:从结构设计到完整代码与避坑指南

发布时间:2026/10/15 18:16:34
链栈C语言实现详解:从结构设计到完整代码与避坑指南 1. 链栈到底解决了什么问题栈这个结构学过数据结构的人都不陌生先进后出像一摞盘子你只能从最上面拿。但真到写代码的时候很多人第一反应是用数组去模拟栈也就是顺序栈。顺序栈确实简单一个数组加一个栈顶指针就完事了但它有个绕不开的硬伤容量是固定的。你开100个元素的数组存到101个就溢出你开10000个大部分时候又浪费内存。这个矛盾在嵌入式或者资源受限的场景里特别扎眼。链栈就是冲着这个痛点来的。它用链表来组织栈元素栈顶就是链表的头节点每次入栈相当于在头部插入一个节点出栈就是把头部节点摘掉。这样一来栈的容量只受限于系统可用内存理论上可以一直增长不需要预先估计规模。而且链栈的入栈和出栈都只操作头节点时间复杂度稳定在O(1)不存在顺序栈扩容时整体搬移数据的开销。这篇文章我打算把链栈的完整C语言实现从头到尾捋一遍包括初始化、判栈空、入栈、出栈、取栈顶元素、求栈长这六个基本操作。代码是可直接编译运行的完整版本不是伪代码。适合正在学数据结构的学生、准备面试的开发者以及需要手写一个轻量级栈结构的嵌入式工程师。我会把每个操作背后的指针变化画清楚把容易踩的坑标出来最后给一份可以直接抄的完整代码。2. 链栈的结构设计与核心思路拆解2.1 为什么选带头节点的单链表链栈的实现方式其实有好几种可以用带头节点的单链表也可以不用头节点甚至可以用双向链表。我选的是带头节点的单链表栈顶固定在头节点之后。这个选择不是随便定的背后有几个实际考量。第一带头节点之后入栈和出栈的操作逻辑变得统一。不管栈里有没有元素入栈永远是“在头节点后面插入新节点”出栈永远是“删除头节点后面的节点”。不需要单独判断“栈为空时入栈”和“栈非空时入栈”两种情况代码分支少了一半。第二头节点本身不存数据它只是一个锚点。这样栈顶指针永远指向头节点不会因为出栈到空栈就变成野指针。你想想如果不带头节点出栈最后一个元素之后栈顶指针就变成NULL了下次入栈又得重新处理NULL的情况代码里到处是if判断容易出错。第三求栈长的时候从头节点开始遍历逻辑清晰。虽然求栈长是O(n)的操作但链栈本来就不适合频繁求长度这个后面会讲优化方案。typedef int ElemType; // 元素类型按需修改 typedef struct StackNode { ElemType data; // 数据域 struct StackNode *next; // 指针域 } StackNode, *LinkStackPtr; typedef struct { LinkStackPtr top; // 栈顶指针始终指向头节点 int count; // 栈元素个数可选用于O(1)求栈长 } LinkStack;这里我多加了一个count字段。严格来说链栈求栈长可以遍历链表来数但那是O(n)。加一个计数器入栈加一出栈减一求栈长就是O(1)。多占4个字节换一个高频操作的性能提升这笔账划算。当然如果你在极端省内存的场景可以把count去掉用遍历的方式求长度。2.2 栈顶到底放在链表的哪一头这个问题初学者容易搞混。链栈的栈顶应该放在链表的头部不是尾部。原因很简单单链表在头部插入和删除都是O(1)在尾部插入虽然是O(1)如果有尾指针但删除尾部节点需要找到前驱是O(n)。栈的核心操作是入栈和出栈都发生在栈顶所以栈顶必须放在操作最快的那一端也就是头部。用生活化的类比链表就像一列火车头部是车头。栈顶放在车头位置每次加车厢或者摘车厢都在车头操作你一眼就能看到动手也快。如果栈顶放在车尾每次摘最后一节车厢你还得从头走到尾找到倒数第二节费时费力。2.3 内存管理策略谁分配谁释放链栈的每个节点都是动态分配的用malloc申请用free释放。这里有个原则入栈时分配节点出栈时释放节点。初始化的时候只分配头节点头节点在销毁栈的时候统一释放。我见过不少人在出栈时只把栈顶指针往下移不free被摘掉的节点结果内存泄漏。也见过有人在入栈时复用已释放的节点导致野指针。这两个极端都要避免。正确的做法是出栈时先用一个临时指针保存要删除的节点把栈顶指针移到下一个节点然后free临时指针指向的节点。注意free之后要把指针置NULL虽然在这个场景里临时指针马上就不用了但养成习惯能避免很多悬空指针的问题。3. 六个基本操作的完整实现与细节剖析3.1 初始化头节点从哪来初始化链栈就做一件事创建一个头节点让栈顶指针指向它并把next置为NULLcount置为0。int InitStack(LinkStack *S) { S-top (LinkStackPtr)malloc(sizeof(StackNode)); if (S-top NULL) { return 0; // 内存分配失败 } S-top-next NULL; S-count 0; return 1; // 初始化成功 }这里返回int而不是void是为了让调用者知道初始化是否成功。malloc在内存不足时会返回NULL虽然现代系统上很少发生但在嵌入式环境里这是必须处理的。我试过在一个内存只有几十KB的板子上跑malloc失败是家常便饭不检查返回值程序直接就崩了。头节点的data域我们不关心不初始化也没关系但有些静态分析工具会报“未初始化变量”的警告所以稳妥起见可以给data赋个0。不过这不是必须的看你的编译选项严格程度。3.2 判栈空为什么不能只看top是否为NULL判栈空的逻辑很简单头节点的next是否为NULL。如果next是NULL说明头节点后面没有元素栈为空。int IsEmpty(LinkStack *S) { return (S-top-next NULL); }这里要特别强调不能判断S-top是否为NULL。因为我们的设计是带头节点的S-top永远指向头节点初始化之后就不是NULL了。如果你判断S-top NULL那永远返回假栈空判断就失效了。这是带头节点链表最容易犯的错误没有之一。那什么时候S-top会是NULL呢只有一种情况初始化失败malloc返回了NULL。这种情况下整个栈都是无效的判栈空也没有意义。所以判栈空的前提是栈已经成功初始化。3.3 入栈头插法的标准姿势入栈就是在头节点后面插入一个新节点。步骤分三步申请新节点、填入数据、调整指针。int Push(LinkStack *S, ElemType e) { LinkStackPtr newNode (LinkStackPtr)malloc(sizeof(StackNode)); if (newNode NULL) { return 0; // 内存分配失败入栈失败 } newNode-data e; newNode-next S-top-next; // 新节点指向原来的栈顶元素 S-top-next newNode; // 头节点指向新节点 S-count; return 1; }指针调整的顺序很关键。必须先把新节点的next指向原来的栈顶元素再让头节点的next指向新节点。如果顺序反了先执行S-top-next newNode那原来的栈顶元素就丢了链断了。这个顺序问题在纸上画一下指针图就清楚了但写代码的时候一着急就容易写反。我用一个具体的例子走一遍。假设栈里已经有元素3和7栈顶是3。头节点-3-7-NULL。现在入栈5申请新节点data5。newNode-next S-top-nextnewNode-next指向3。此时链表是头节点-3-7-NULLnewNode-5-3-7-NULLnewNode还没接入。S-top-next newNode头节点-5-3-7-NULL。入栈完成。实操心得入栈失败时要不要返回错误码我的建议是要。虽然大多数时候malloc不会失败但一旦失败调用者需要知道这次入栈没成功。如果Push返回void调用者以为入栈成功了后续逻辑全乱。返回int0表示失败1表示成功调用者可以决定是重试还是报错。3.4 出栈别忘了free出栈是把头节点后面的第一个节点摘掉取出它的数据然后释放内存。int Pop(LinkStack *S, ElemType *e) { if (IsEmpty(S)) { return 0; // 栈空出栈失败 } LinkStackPtr temp S-top-next; // 临时保存要删除的节点 *e temp-data; // 取出数据 S-top-next temp-next; // 头节点跳过temp free(temp); // 释放节点内存 temp NULL; // 避免悬空指针 S-count--; return 1; }出栈有三个关键点。第一出栈前必须判空空栈出栈是非法操作直接返回失败。第二用temp保存要删除的节点因为调整指针之后你就找不到它了没法free。第三free之后把temp置NULL虽然temp是局部变量马上销毁但这是一个好习惯能防止后续误用。出栈的参数用ElemType *e而不是返回值是因为返回值要用来表示成功或失败。C语言没有异常机制只能通过返回码加输出参数的方式来传递数据和状态。这是C语言接口设计的常见模式你会在很多标准库和开源项目里看到。3.5 取栈顶元素只读不删取栈顶元素和出栈的区别在于取栈顶元素只读取数据不删除节点栈的结构不变。int GetTop(LinkStack *S, ElemType *e) { if (IsEmpty(S)) { return 0; // 栈空取栈顶失败 } *e S-top-next-data; return 1; }这个操作很简单但要注意判空。空栈取栈顶元素是没有意义的必须返回失败。有些实现会返回一个默认值比如0或者-1但这样调用者无法区分“栈顶元素就是0”和“栈空返回的0”。用返回码加输出参数的方式语义最清晰。3.6 求栈长遍历还是计数器求栈长有两种实现方式。第一种是遍历链表数节点int StackLength(LinkStack *S) { int len 0; LinkStackPtr p S-top-next; while (p ! NULL) { len; p p-next; } return len; }第二种是直接返回count字段int StackLength(LinkStack *S) { return S-count; }两种方式结果一样但性能差很多。遍历是O(n)计数器是O(1)。如果你的程序频繁求栈长比如在循环里判断栈是否达到某个阈值那必须用计数器。如果只是偶尔求一次遍历也够用还能省4个字节。我个人的选择是保留count字段。原因很简单链栈的入栈和出栈本来就是O(1)加一个计数器不增加时间复杂度只是常数级开销。而求栈长从O(n)降到O(1)在需要频繁判断栈大小的场景里提升明显。这4个字节花得值。注意如果你选择用count字段那入栈和出栈时必须同步更新count不能漏。漏更新一次后续所有求栈长的结果都是错的而且这种bug很难查因为栈的入栈出栈逻辑本身是对的只是长度不对。4. 完整代码与编译运行实录4.1 完整代码清单把上面的片段拼起来加上测试用的main函数就是一份可以直接编译运行的完整代码。#include stdio.h #include stdlib.h typedef int ElemType; typedef struct StackNode { ElemType data; struct StackNode *next; } StackNode, *LinkStackPtr; typedef struct { LinkStackPtr top; int count; } LinkStack; /* 初始化 */ int InitStack(LinkStack *S) { S-top (LinkStackPtr)malloc(sizeof(StackNode)); if (S-top NULL) { return 0; } S-top-next NULL; S-count 0; return 1; } /* 判栈空 */ int IsEmpty(LinkStack *S) { return (S-top-next NULL); } /* 入栈 */ int Push(LinkStack *S, ElemType e) { LinkStackPtr newNode (LinkStackPtr)malloc(sizeof(StackNode)); if (newNode NULL) { return 0; } newNode-data e; newNode-next S-top-next; S-top-next newNode; S-count; return 1; } /* 出栈 */ int Pop(LinkStack *S, ElemType *e) { if (IsEmpty(S)) { return 0; } LinkStackPtr temp S-top-next; *e temp-data; S-top-next temp-next; free(temp); temp NULL; S-count--; return 1; } /* 取栈顶元素 */ int GetTop(LinkStack *S, ElemType *e) { if (IsEmpty(S)) { return 0; } *e S-top-next-data; return 1; } /* 求栈长 */ int StackLength(LinkStack *S) { return S-count; } /* 销毁栈 */ void DestroyStack(LinkStack *S) { LinkStackPtr p S-top; LinkStackPtr temp; while (p ! NULL) { temp p; p p-next; free(temp); } S-top NULL; S-count 0; } /* 测试 */ int main() { LinkStack S; ElemType e; int i; if (!InitStack(S)) { printf(初始化失败\n); return 1; } printf(初始化后栈长: %d\n, StackLength(S)); printf(栈是否为空: %s\n, IsEmpty(S) ? 是 : 否); for (i 1; i 5; i) { Push(S, i * 10); printf(入栈 %d, 当前栈长: %d\n, i * 10, StackLength(S)); } if (GetTop(S, e)) { printf(当前栈顶元素: %d\n, e); } while (!IsEmpty(S)) { Pop(S, e); printf(出栈 %d, 剩余栈长: %d\n, e, StackLength(S)); } printf(全部出栈后是否为空: %s\n, IsEmpty(S) ? 是 : 否); DestroyStack(S); return 0; }4.2 编译与运行结果用gcc编译命令如下gcc -o linkstack linkstack.c -Wall -g-Wall打开所有警告-g保留调试信息。编译如果没有警告说明代码质量基本过关。运行结果初始化后栈长: 0 栈是否为空: 是 入栈 10, 当前栈长: 1 入栈 20, 当前栈长: 2 入栈 30, 当前栈长: 3 入栈 40, 当前栈长: 4 入栈 50, 当前栈长: 5 当前栈顶元素: 50 出栈 50, 剩余栈长: 4 出栈 40, 剩余栈长: 3 出栈 30, 剩余栈长: 2 出栈 20, 剩余栈长: 1 出栈 10, 剩余栈长: 0 全部出栈后是否为空: 是结果符合预期入栈顺序是10、20、30、40、50出栈顺序是50、40、30、20、10典型的后进先出。栈长在入栈时递增出栈时递减取栈顶元素返回的是最后入栈的50。4.3 内存泄漏检测在Linux下可以用valgrind检测内存泄漏valgrind --leak-checkfull ./linkstack如果代码正确valgrind会报告“All heap blocks were freed -- no leaks are possible”。这说明所有malloc的节点都被free了没有内存泄漏。我强烈建议每次写完涉及动态内存的代码都跑一遍valgrind很多隐藏的内存问题只有它能发现。实操心得valgrind在嵌入式环境里跑不了但你可以用计数器法。在全局维护一个malloc_count每次malloc加一每次free减一程序结束时如果malloc_count不为0说明有泄漏。这个方法简单粗暴但很有效。5. 常见问题与排查技巧实录5.1 段错误最常见也最好查段错误是链栈代码最常见的崩溃原因没有之一。我总结了几种典型场景和排查方法。场景一初始化失败后继续操作。如果InitStack返回0说明malloc失败S-top是NULL。这时候你调用Push执行S-top-next直接访问NULL指针段错误。解决办法是检查InitStack的返回值失败就退出。场景二出栈时没判空。空栈时S-top-next是NULL你执行temp-datatemp是NULL段错误。解决办法是出栈前必须调用IsEmpty判断。场景三free之后继续使用。出栈时free了temp指向的节点但后面又通过其他指针访问这个节点段错误。解决办法是free之后把指针置NULL并且确保没有其他指针指向这个节点。排查段错误最有效的工具是gdb。编译时加-g运行gdb ./linkstack然后run崩溃后输入bt查看调用栈能直接定位到出错的行号。如果是在嵌入式环境可以用printf大法在关键位置打印指针地址看哪个指针是NULL或者非法值。5.2 栈空判断失效带头节点的陷阱前面提过带头节点的链栈判栈空必须判断S-top-next NULL不能判断S-top NULL。我见过有人写成int IsEmpty(LinkStack *S) { return (S-top NULL); // 错误 }这样写的话初始化之后S-top指向头节点不是NULL所以IsEmpty永远返回0假栈空判断完全失效。后果是出栈时不判空直接操作空栈段错误。这个bug的隐蔽性在于初始化之后程序运行正常入栈出栈都没问题只有出栈到空栈再出栈时才崩溃。如果你测试的时候没有测“空栈出栈”这个边界情况就发现不了。5.3 内存泄漏出栈忘记free出栈时只调整指针不free是内存泄漏的经典原因。代码长这样int Pop(LinkStack *S, ElemType *e) { if (IsEmpty(S)) return 0; *e S-top-next-data; S-top-next S-top-next-next; // 只调整指针没free S-count--; return 1; }这样写逻辑上没问题出栈的数据也对但被摘掉的节点内存永远回收不了。如果程序长时间运行反复入栈出栈内存会持续增长最终耗尽。用valgrind一跑就能发现报告里会显示“definitely lost”的字节数等于节点大小乘以出栈次数。5.4 常见问题速查表问题现象可能原因排查方法解决方案段错误崩溃在Push栈未初始化或初始化失败检查InitStack返回值初始化失败时不要继续操作段错误崩溃在Pop空栈出栈在Pop开头加IsEmpty判断出栈前必须判空栈空判断永远为假判断了S-top NULL打印S-top和S-top-next改为判断S-top-next NULL内存持续增长出栈未free节点用valgrind检测出栈时free被摘节点栈长不正确count未同步更新检查Push和Pop中的count操作入栈count出栈count--取栈顶元素返回错误值空栈取栈顶检查GetTop是否判空空栈时返回失败码5.5 独家避坑技巧第一个技巧画图再写代码。链栈的指针操作在纸上画三个节点标出头节点、栈顶节点、新节点把指针箭头画清楚再写代码。我刚开始学的时候觉得自己脑子能记住结果指针顺序写反了好几次后来老老实实画图一次就对了。第二个技巧边界测试必做。空栈出栈、空栈取栈顶、连续入栈出栈、入栈到很大数量再全部出栈这四个场景必须测。很多bug只在边界情况出现正常流程测不出来。第三个技巧用assert辅助调试。在Push和Pop里加assert(S ! NULL S-top ! NULL)能提前发现非法调用。发布版本可以用#define NDEBUG关掉assert不影响性能。第四个技巧count字段用unsigned int。栈长不可能是负数用unsigned int更语义化。但要注意如果出栈时count已经是0还减会变成很大的正数。所以出栈前判空是必须的判空之后count一定大于0减一是安全的。6. 链栈与顺序栈的选型对比6.1 性能与内存的权衡链栈和顺序栈没有绝对的优劣关键看场景。我把两者的核心差异整理成表格对比维度链栈顺序栈容量限制仅受内存限制固定容量需预分配入栈时间复杂度O(1)平均O(1)扩容时O(n)出栈时间复杂度O(1)O(1)取栈顶O(1)O(1)求栈长O(1)带count或O(n)O(1)内存开销每节点多一个指针域无额外开销但可能浪费预分配空间缓存友好性差节点分散在堆上好连续内存实现复杂度稍复杂涉及动态内存简单数组加指针顺序栈的缓存友好性是它最大的优势。数组是连续内存CPU缓存命中率高实际运行速度往往比链栈快尽管时间复杂度一样。链栈的节点是malloc出来的散落在堆的各个位置缓存命中率低每次访问都可能触发缓存未命中。但链栈的容量弹性是顺序栈比不了的。如果你不确定栈的最大深度或者栈的深度波动很大链栈更合适。顺序栈要么开小了溢出要么开大了浪费。6.2 什么场景选链栈根据我的经验以下几种场景优先选链栈场景一栈深度不可预测。比如解析表达式时括号嵌套深度取决于输入可能很深也可能很浅。用链栈不用担心溢出。场景二内存碎片化的环境。嵌入式系统运行久了内存碎片化严重大块连续内存很难申请。链栈每次只申请一个节点对连续内存要求低更容易分配成功。场景三需要频繁入栈出栈且深度变化大。链栈的入栈出栈不涉及数据搬移深度变化时没有额外开销。顺序栈在扩容时要搬移所有元素深度频繁变化时开销明显。反过来如果栈深度已知且固定比如函数调用栈那顺序栈更合适简单高效。如果对性能要求极高且深度可控顺序栈的缓存优势能带来实际的速度提升。6.3 一个折中方案动态数组栈如果你既想要顺序栈的缓存友好性又想要链栈的弹性容量可以考虑动态数组栈。底层还是数组但容量不够时用realloc扩容通常是翻倍扩容。这样入栈平均O(1)缓存友好性也保留了大半。typedef struct { ElemType *data; int top; int capacity; } DynArrayStack; int Push(DynArrayStack *S, ElemType e) { if (S-top S-capacity) { int newCap S-capacity * 2; ElemType *newData (ElemType *)realloc(S-data, newCap * sizeof(ElemType)); if (newData NULL) return 0; S-data newData; S-capacity newCap; } S-data[S-top] e; return 1; }这个方案在大多数场景下比纯链栈更实用。但它的缺点是扩容时需要搬移数据而且realloc可能失败。链栈的每个节点独立分配不存在整体搬移的问题。选哪个看你的具体需求。7. 从链栈延伸出去的知识点7.1 链栈与递归的关系栈和递归是天然绑定的。函数调用本身就是用栈来实现的每次调用压入一个栈帧返回时弹出。你写递归函数的时候系统在背后维护了一个调用栈。如果递归深度太大系统栈溢出程序崩溃。链栈可以用来模拟递归。比如二叉树的非递归遍历就是用显式的栈来替代系统的调用栈。你把原本递归函数里的局部变量和返回地址打包成一个结构体压入链栈循环处理。这样递归深度不受系统栈限制只受堆内存限制。我试过把一个深度上万层的递归改成链栈模拟系统栈版本直接段错误链栈版本跑得好好的。当然改成非递归之后代码复杂度上升不少可读性下降这是代价。7.2 链栈在表达式求值中的应用表达式求值是栈的经典应用。中缀表达式转后缀表达式以及后缀表达式的计算都需要栈。链栈在这里的优势是容量弹性因为表达式的嵌套深度取决于输入可能很深。以中缀转后缀为例算法需要两个栈一个存操作数一个存运算符。遇到操作数直接输出遇到运算符根据优先级决定是压栈还是弹出。括号的处理也需要栈。用链栈实现不用担心表达式太长导致栈溢出。7.3 链栈的线程安全问题标准链栈不是线程安全的。多个线程同时入栈或出栈指针操作会竞争导致链表断裂或数据丢失。如果需要在多线程环境使用链栈必须加锁。最简单的方案是用互斥锁保护整个栈pthread_mutex_t lock PTHREAD_MUTEX_INITIALIZER; int Push(LinkStack *S, ElemType e) { pthread_mutex_lock(lock); // ... 入栈逻辑 ... pthread_mutex_unlock(lock); return 1; }加锁的代价是性能下降每次入栈出栈都要加锁解锁。如果并发量不大这个开销可以接受。如果并发量很大可以考虑无锁栈用CAS原子操作实现。但无锁编程复杂度高容易出bug除非性能要求极高否则不建议自己写。注意加锁的粒度要控制好。锁的范围太大并发度低锁的范围太小可能保护不全。对于链栈整个入栈或出栈操作应该是一个原子操作锁的范围要覆盖指针调整的全过程。7.4 链栈的持久化有时候你需要把栈的内容保存到文件下次启动时恢复。链栈的持久化很简单遍历链表把每个节点的data写入文件。恢复时按顺序读取依次入栈。但要注意顺序。写入时是从栈顶到栈底恢复时如果按写入顺序入栈栈顶就变成栈底了。正确的做法是写入时从栈底到栈顶或者恢复时反向读取。我建议写入时从栈底到栈顶这样恢复时顺序入栈栈顶还是原来的栈顶。void SaveStack(LinkStack *S, const char *filename) { FILE *fp fopen(filename, wb); if (fp NULL) return; // 先写入栈长 fwrite(S-count, sizeof(int), 1, fp); // 从栈底到栈顶写入需要先反转或者用递归 // 简单做法用临时数组保存反向写入 ElemType *temp (ElemType *)malloc(S-count * sizeof(ElemType)); LinkStackPtr p S-top-next; int i S-count - 1; while (p ! NULL) { temp[i--] p-data; p p-next; } fwrite(temp, sizeof(ElemType), S-count, fp); free(temp); fclose(fp); }这段代码先把栈元素倒序存入临时数组再写入文件。恢复时顺序读取依次入栈栈顶就是原来的栈顶。临时数组的额外内存开销是O(n)对于大栈可能是个问题但持久化本来就是低频操作可以接受。8. 我个人的实操体会链栈这个结构代码量不大但指针操作密集是练习指针和动态内存的好素材。我刚开始学的时候觉得带头节点的设计多此一举直接用一个top指针指向栈顶元素不就行了。后来写多了才发现带头节点省去了大量边界判断代码更简洁出错概率更低。那一个头节点的内存开销换来的是代码的健壮性非常划算。另一个体会是count字段的价值。教科书上的链栈求栈长通常是遍历因为教科书关注的是算法本身不考虑工程优化。但实际项目中求栈长往往是高频操作O(n)的遍历会成为性能瓶颈。加一个count字段O(1)求长度这是工程思维和学术思维的区别。最后说说出栈时的free。我见过太多人忘记free包括我自己早期也犯过这个错。内存泄漏在短时间运行的程序里看不出来但长时间运行的服务里内存会一点点被吃掉最终OOM。养成“谁分配谁释放”的习惯每个malloc都要有对应的free这是C语言程序员的基本功。如果你把这份代码敲一遍编译运行再用gdb和valgrind过一遍链栈的指针操作和内存管理就算入门了。接下来可以试试用链栈实现表达式求值或者把链栈改成线程安全版本这些都是很好的进阶练习。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号