#include <stdbool.h>引入布尔类型支持。在本例使用的 C99/C11 写法中,bool 表示布尔类型,true 和 false 表示成功与失败。预处理器会在编译前处理 #include;它不是运行时执行的函数。
Stack · Last In, First Out
像叠盘子一样存放数据:只能从栈顶放入和取出。观察操作代码、栈顶指针和底层数组如何同步变化。
示例未开始;启动示例会清空当前栈。
这里展示的是栈(Stack),也常被称为堆栈,不是堆排序中的堆。栈遵循 LIFO:Last In, First Out,后进先出。
入栈把值放在顶部;出栈移除顶部元素并返回;查看栈顶只读不删除。空栈出栈称为下溢,达到演示容量后再入栈称为上溢。
C 示例用 Stack 结构体保存字符串数组、top 和 capacity;push/pop/peek 返回 bool 表示是否成功。左侧 C 代码用于教学展示,浏览器由 JavaScript 模型模拟相同操作并同步高亮,不编译执行 C。单步执行一个栈操作。
常见用途包括函数调用栈、撤销操作、括号匹配和深度优先搜索。紫色边框表示栈顶,绿色表示入栈,粉色表示出栈或错误。
C Code · Line by Line
以下行号与上方“栈操作 · C”代码面板一一对应。第 9 行是空行,仅用于分隔类型定义和函数,不会执行任何操作。
#include <stdbool.h>引入布尔类型支持。在本例使用的 C99/C11 写法中,bool 表示布尔类型,true 和 false 表示成功与失败。预处理器会在编译前处理 #include;它不是运行时执行的函数。
#include <stdio.h>引入标准输入输出相关函数声明。本程序使用其中的 snprintf,把输入字符串写入指定长度的缓冲区;这里并不是用它向屏幕打印。
#include <string.h>引入字符串处理函数声明。本程序使用 strcpy,将栈顶字符串复制到调用者提供的输出缓冲区。
#define MAX_CAPACITY 8定义预处理宏 MAX_CAPACITY,其值为 8。编译前,相应宏名会被替换成 8。它决定结构体内部实际分配的槽位数;没有结尾分号,也不是可以在运行时修改的变量。
typedef struct {开始定义一个匿名结构体,并通过 typedef 为它起类型别名。结构体把数据数组、栈顶位置和容量放在一起,方便作为一个完整的栈对象传递。左花括号开始成员声明区域。
char data[MAX_CAPACITY][13];定义一个二维字符数组,共 8 行,每行 13 个 char。每一行保存一个栈元素:最多 12 字节的字符串内容,加上 1 字节字符串结束符 \0。data[0] 是栈底槽位,data[top] 是当前栈顶字符串。数组直接包含在结构体内,不需要额外 malloc。
int top, capacity;声明两个整数成员。top 是栈顶下标,不是元素数量:空栈为 -1,一个元素时为 0,元素数等于 top + 1。capacity 是本次允许使用的有效容量,须为 1–8;即使页面选容量 4,结构体仍实际包含 8 个槽位,只允许使用前 4 个。
} Stack;结束结构体定义,并把该结构体类型命名为 Stack。以后可以写 Stack s; 来声明一个栈对象。这里的分号是类型声明的一部分,不能省略。
bool push(Stack *s, const char *value) {定义入栈函数,返回 bool。Stack *s 接收栈对象的地址,所以函数可以修改调用者的原始栈,而不是修改一个副本。const char *value 指向待复制的字符串;const 表示函数不会通过这个指针修改输入字符,不代表它会永久保存这个地址。左花括号开始函数体。
if (s->top + 1 >= s->capacity) return false;先判断栈是否已满。s->top 通过指针访问成员,等价于 (*s).top;top + 1 就是当前元素数。若数量达到或超过有效容量,立即返回 false,不再执行后面的入栈语句,也不改变栈。必须先检查再递增,否则可能写出数组边界。
++s->top;将栈顶下标加 1,为新元素选中一个槽位。空栈的 top 从 -1 变为 0;若原来 top=1,则新元素放入下标 2。这里的前缀 ++ 是递增操作;此行是独立语句,因此没有利用它的表达式返回值。
snprintf(s->data[s->top], 13, "%s", value);把 value 中的字符串复制到新的栈顶槽位。第一个参数是目标字符数组,第二个参数 13 是包含结束符在内的目标容量,第三个参数 "%s" 表示按字符串格式写入,第四个参数是输入。目标容量大于 0 时,正常成功写入会以 \0 结束;最多保留 12 字节内容,超长内容会被截断。这里只复制字符,不保存输入指针。固定使用 "%s" 也避免把输入中的百分号当作格式指令。
return true;返回 true,告诉调用者入栈流程已完成。它不是返回入栈的字符串,也不是返回新的栈顶下标。注意本教学代码未检查 snprintf 的返回值,因此 true 不保证长输入完整保存,也未单独处理格式化失败。
}结束 push 函数体。此处不是结构体类型声明,所以右花括号后不需要分号。
bool pop(Stack *s, char out[13]) {定义出栈函数。s 指向要修改的栈,out 是调用者提供的结果缓冲区,成功与否通过 bool 返回,取出的字符串通过 out 输出。特别注意:函数形参中的 char out[13] 会调整为 char *,数字 13 不会让编译器自动验证调用者确实分配了 13 字节。
if (s->top < 0) return false;如果 top 小于 0,说明栈为空,不能访问 data[top]。函数立即返回 false,避免访问 data[-1],这称为防止栈下溢。失败时没有复制字符串,out 的原有内容也不会由这一行清空。
strcpy(out, s->data[s->top]);先将当前栈顶字符串复制到 out,包括末尾的 \0。strcpy 不接收目标容量,也不会自动检查目标大小,因此 out 必须足够大,源字符串必须正确结束。这里先复制再移动 top,才能得到要被移除的那个元素。
--s->top;将栈顶下标减 1,逻辑上删除刚才的栈顶元素。例如 top 从 2 变为 1,原下标 2 不再属于有效栈范围;若最后一个元素被取出,top 从 0 变为 -1。此行并不擦除 data 中的旧字符,旧槽位以后入栈时会被覆盖。
return true;表示出栈成功。真正取出的字符串已经写入 out。调用者应先检查 bool 返回值,再使用 out;不能把 false 情况下遗留在 out 的旧内容当作新的出栈结果。
}结束 pop 函数体。出栈只处理一个栈顶元素,因此后入栈的元素先被取出,这就是 LIFO。
bool peek(const Stack *s, char out[13]) {定义只读的栈顶查看函数。const Stack *s 表示不能通过 s 修改栈的成员;例如在这个函数里写 --s->top 会违反 const 限制。out 仍是可写的输出缓冲区,const 只作用于 s 指向的栈对象。
if (s->top < 0) return false;先检查空栈。若不存在栈顶元素,则立即返回 false,既不读取 data[-1],也不修改 out。这个检查与 pop 的空栈保护相同。
strcpy(out, s->data[s->top]);将当前栈顶字符串复制到调用者的 out 缓冲区。复制规则和容量要求与 pop 相同。但这里没有后续的 top 递减操作,因此原元素仍留在栈里,元素数量保持不变。
return true;表示查看成功。调用者可以从 out 读取栈顶内容;再次 peek 会得到同一元素,除非中间进行了入栈、出栈或清空操作。
}结束 peek 函数体。peek 与 pop 最重要的区别不是复制字符串的方式,而是它不修改栈顶位置。
void clear(Stack *s) {定义清空函数。void 表示不返回结果值,Stack *s 表示通过地址修改原栈对象。该函数只改变逻辑状态,不释放栈对象占用的存储空间。
s->top = -1;把栈顶下标重置为空栈标志 -1,所有槽位在逻辑上都变为无效。capacity 不变,所以清空后仍可继续入栈。数组内旧字符串没有被擦除;这不是敏感数据的安全清除。如果要逐字节擦除数据,需要额外设计。
}结束 clear 函数体。清空不需要逐个移动节点或释放内存,因为栈的数据直接存在固定二维数组中。
上方展示的是类型与函数定义,不含完整的入口程序。真实 C 程序在调用前必须创建并初始化栈对象;只写 Stack s; 声明局部变量,会使成员值未初始化,不能直接拿来入栈。
Stack s = { .top = -1, .capacity = 6 };
char out[13] = {0};
if (push(&s, "10") && push(&s, "20")) {
if (peek(&s, out)) printf("TOP: %s\n", out);
if (pop(&s, out)) printf("POP: %s\n", out);
}
clear(&s);
Stack s = { .top = -1, .capacity = 6 }; 使用 C99 指定初始化器,把栈顶设为 -1、有效容量设为 6,未明确指定的数组成员会被零初始化。数组物理容量仍是 8。char out[13] = {0}; 分配 13 字节结果缓冲区,并将其初始化为全零;作为字符串时初始值为空字符串。&s 是取地址操作,把栈对象的地址传给函数;s 是对象本身,Stack *s 是函数中的指针形参,三者不要混淆。top=1。&& 具有短路行为:若第一次入栈失败,则第二次调用不会执行。两次调用也不是事务,第二次失败不会撤销第一次成功的入栈。top 仍为 1;出栈输出“20”后 top=0,“10”成为新的栈顶;最后清空使 top=-1。printf 把结果写到标准输出,与上方 snprintf 写到字符缓冲区不同。这段示例须放在真实 C 程序的函数内,例如 main 中。1 <= capacity <= MAX_CAPACITY,且 -1 <= top < capacity。本教学代码假设调用者维持这些条件,不会自动修复越界的容量或损坏的栈顶。\0 结束的字符串;输出缓冲区必须可写且至少有 13 字节。输入不要与当前入栈目标重叠,使用 strcpy 时源、目标也不能重叠。snprintf 的返回值是本来需要写入的字节数(不含结束符);返回值大于等于 13 表示截断,小于 0 表示失败。若业务不允许截断,应先验证长度或检查结果,并设计明确的失败/回滚策略。pop、peek 返回成功标志,字符串通过 out 传出。失败不保证清空输出缓冲区,因此必须先检查返回值。top,不清除旧数据,也不调用 free。数组属于栈对象本身,其生命周期由栈对象决定。
评论与讨论
评论保存在 GitHub Issues,登录 GitHub 后即可参与讨论。
评论暂未开放,站点管理员尚未配置 GitHub OAuth。
前往 GitHub Issues