☰
C 语言通用自增长栈(Generic Self-growing Stack)源码级解析:基于 data_structures/stack 模块
2026/10/1 9:44:35 网站建设 项目流程
  • 示例工程

【免费下载链接】C

Collection of various algorithms in mathematics, machine learning, computer science, physics, etc implemented in C for educational purposes.

项目地址:https://gitcode.com/gh_mirrors/c/C
点击查看免费下载

本指南以仓库 data_structures/stack/README.md 为骨架,深入讲解其中实现的模块化、泛型、自增长栈:它通过void *指针数组容纳任意类型的数据,容量不足时自动扩容,并向调用方隐藏全部内部状态(数据隐藏)。读完本文,你将掌握该栈的完整公共接口、底层实现机制(扩容、偏移量、计数器)、两种编译测试方式,以及基于链表的对照实现,可直接在自己的 C 项目中复用这套数据结构。

模块概览:一个文件即可引入

data_structures/stack目录下包含以下组成部分:

文件作用
stack.h公共接口头文件,使用方只需#include "stack.h"
stack.c基于动态数组的栈实现(含自增长逻辑)
main.c面向数组栈的交互式测试框架程序
stack_linked_list/另一种基于链表的栈实现(含 stack.h、stack.c、main.c、Makefile)

如 README 所述,使用方只需引入stack.h一个头文件即可获得全部能力:头文件只暴露函数原型,具体的内部数据结构(指针数组、容量、计数器等)全部隐藏在stack.c中,体现了良好的封装与数据隐藏原则。

公共接口:五个核心函数

README 定义的公共接口如下,头文件 stack.h 中一一对应声明(此外还额外声明了top()函数):

void initStack(); void push(void *object); void *pop(); int size(); int isEmpty();
函数签名行为说明
initStackvoid initStack()将栈初始化为容量为10 个元素的动态数组
pushvoid push(void *object)将任意指针压入栈顶
popvoid *pop()弹出并返回栈顶元素,前置条件:栈非空(违反会触发断言)
sizeint size()返回当前栈内元素个数
isEmptyint isEmpty()栈空返回1,否则返回0

由于栈元素类型为void *,这套接口可以存放任何类型的指针(整数、结构体、字符串等),这正是"泛型(generic)"的含义。同时接口里还隐含了头文件中额外声明的 top():与pop()不同,它只查看栈顶元素而不移除。

源码级实现原理:数据隐藏 + 自动扩容

内部状态与初始化

stack.c 通过文件级全局变量维护栈状态,调用方完全不可见:

void **array; /* 指向实际存储元素的 void* 指针数组 */ int max = 10; /* 当前容量 */ int counter = 0;/* 元素计数器 */ int offset = -1;/* 指向栈顶元素的偏移地址 */

initStack()在 stack.c 中只做一件事——为max(10)个void *指针分配内存,并用assert(array)确保分配成功:

void initStack() { array = malloc(sizeof(void *) * max); assert(array); /* tests whether pointer is assigned to memory. */ }

注意:初始化后counter = 0、offset = -1,表示栈为空、栈顶尚不存在任何元素。

push 与自动扩容机制

push()位于 stack.c,是"自增长"特性的核心。其逻辑为:

  • 先用assert(object)拒绝空指针入栈;
  • 若counter < max(未满):offset++指向新栈顶,*(array + offset) = object写入元素,counter++;
  • 若栈已满:调用内部工具函数grow()扩容,然后递归调用自身完成入栈。

扩容函数 grow() 不在公共接口中,属于实现细节:

void grow() { max += 10; /* 容量每次增加 10 */ void **tmp = malloc(sizeof(void *) * max); for (i = 0; i < max - 10; i++) /* 拷贝旧数组元素 */ *(tmp + i) = *(array + i); free(array); /* 释放旧数组 */ array = tmp; }

从源码结构可以看到三个明确结论:

  • 扩容步长固定为 10 个元素,避免频繁调用malloc;
  • 每次扩容都会重新分配整块内存并整体拷贝,属于"搬家式"扩容(与按需倍增的实现相比,摊还开销略高,但实现直观、便于教学理解);
  • push通过递归重试实现"满则先扩容再入栈"的闭环,代码简洁。

pop / size / isEmpty / top

pop()位于 stack.c:

void *pop() { void *top = *(array + offset); assert(top); assert(!isEmpty()); /* 前置条件:栈非空 */ offset--; counter--; return top; }

它先断言栈非空(与 README 中"assumes: stack not empty"的约定一致),取出栈顶指针后下移offset、递减counter。注意它返回的是元素指针本身,不释放元素内存——谁压入,谁负责释放,这是使用本栈时需要牢记的内存约定。

其余函数实现极为精简(stack.c):

int size() { return counter; } int isEmpty() { return counter == 0; } void *top() { return array[offset]; }

其中size()直接返回内部计数器,isEmpty()等价于判断counter == 0,top()则按offset直接读取栈顶而不修改任何状态。

编译与测试:两种验证方式

方式一:链表栈(带 Makefile,可直接构建)

进入 stack_linked_list 目录,按 Makefile 执行:

cd data_structures/stack/stack_linked_list make ./main

main.c 依次压入 1~4 四个元素,打印栈大小与内容,再连续两次Stack_pop并打印,可直观验证 LIFO(后进先出)行为:

Size: 4 Stack [Top --- Bottom]: 0x4 0x3 0x2 0x1 Stack after popping: Stack [Top --- Bottom]: 0x3 0x2 0x1 Stack after popping: Stack [Top --- Bottom]: 0x2 0x1

方式二:数组版交互式测试程序

根目录下的 main.c 是一个交互式菜单程序,提供 Push、Pop、Peek、Update、Display 五个操作,可作为理解栈语义的参考测试框架:

gcc main.c -o stack_menu ./stack_menu

运行后按菜单输入选择即可完成压栈、弹栈、查看栈顶、按位置更新元素以及从栈顶到栈底打印全部元素等操作;选择0或按Ctrl-C退出。

直接集成泛型栈到自有项目

若要在自己的项目中复用 stack.c 与 stack.h,只需:

gcc -c stack.c -o stack.o gcc your_main.c stack.o -o your_program

并在your_main.c中#include "stack.h",随后依次调用initStack()→push()/pop()/top()即可。注意每个逻辑上独立的栈共用同一组全局状态,如需多个互不干扰的栈实例,更适合选用下方的链表实现。

对照实现:基于链表的栈(stack_linked_list)

README 明确列出了第二种实现 stack_linked_list。其头文件 stack.h 采用经典的typedef 隐式指针风格封装句柄:

#define T Stack_T typedef struct T *T; /* 对外只暴露不透明句柄 */ extern T Stack_init(void); extern int Stack_size(T stack); extern int Stack_empty(T stack); extern void Stack_push(T stack, void *val); extern void *Stack_pop(T stack); extern void Stack_print(T stack);

实现 stack.c 中,每个节点为elem_t { void *val; struct elem *next; },栈结构体只维护count与head指针:

  • Stack_init分配栈句柄并置空;
  • Stack_push每次在表头插入新节点(t->next = stack->head; stack->head = t;),O(1);
  • Stack_pop从表头摘除节点并free(t),返回保存的值;
  • Stack_print从栈顶向栈底打印各元素的指针值。

与数组版相比,链表版天然无容量上限、无需扩容逻辑,且每个栈实例独立(句柄封装),但每个元素多一个指针节点的内存开销,且需要显式Stack_init初始化句柄。两种实现恰好形成"数组式自动扩容"与"链表式动态增长"两种典型栈方案的对照。

使用注意事项小结

  1. 必须先initStack()再执行任何入栈/出栈操作,否则array为未初始化指针;
  2. pop()的前置条件是栈非空,空栈弹栈会触发assert失败(发布构建需自行移除断言或先检查isEmpty());
  3. push(NULL)会被断言拦截,不可入栈空指针;
  4. 栈内保存的是指针本身,栈退出/元素弹出后,由调用方负责释放指向的动态内存;
  5. 数组版为全局单例状态,适合单栈场景;多栈并发或长期运行场景建议使用链表版句柄封装。

综上所述,data_structures/stack以极简的公共接口(initStack/push/pop/size/isEmpty)配合"容量满 10 增 10"的自增长机制,为 C 语言学习者提供了一个兼顾封装性、泛型性与可读性的栈参考实现,其相邻的链表版实现则展示了同一抽象在不同存储策略下的工程取舍。

  • 示例工程

【免费下载链接】C

Collection of various algorithms in mathematics, machine learning, computer science, physics, etc implemented in C for educational purposes.

项目地址:https://gitcode.com/gh_mirrors/c/C
点击查看免费下载
上一篇:Instabot故事功能完全指南:下载、上传和监控用户故事
下一篇:Git-it技术架构揭秘:Electron框架下的Git教学工具

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询