c语言stack是什么

99ANYc3cd6
预计阅读时长 23 分钟
位置: 首页 C语言 正文

这是一个非常重要的概念,因为它既是数据结构,也是 C 语言程序运行时的核心机制。

c语言stack是什么
(图片来源网络,侵删)

我会从以下几个方面来解释:

  1. 栈的本质:一种数据结构
  2. 栈的两种形态:静态栈 vs. 动态栈
  3. C 语言程序运行时的栈
  4. 栈的核心思想

栈的本质:一种数据结构

栈是一种“后进先出”(Last-In, First-Out, LIFO)的数据结构

你可以把它想象成一个只有一个开口的容器,比如一摞盘子或者一盒子弹。

  • 压栈:你只能把新盘子放到最上面,或者把新子弹压到弹夹的顶端,在数据结构中,这个操作叫做 Push
  • 出栈:你只能从最上面拿走盘子,或者从顶端打出子弹,在数据结构中,这个操作叫做 Pop

核心特点:

c语言stack是什么
(图片来源网络,侵删)
  • LIFO (后进先出):最后被放入栈中的元素,将是第一个被取出的元素。
  • 操作受限:只能在栈顶进行插入和删除操作,不能在中间或底部操作,这使得它非常简单高效。

栈的基本操作:

  • push(item):将一个元素压入栈顶。
  • pop():弹出并返回栈顶元素。
  • peek()top():查看栈顶元素,但不移除它。
  • isEmpty():检查栈是否为空。
  • size():返回栈中元素的数量。

栈的两种形态:静态栈 vs. 动态栈

在 C 语言中,我们可以自己实现一个栈,根据底层数组的实现方式,可以分为静态栈和动态栈。

A. 静态栈

静态栈使用固定大小的数组来实现。

  • 优点:实现简单,内存分配在编译时完成。
  • 缺点:大小固定,如果数据量超过数组大小,会导致“栈溢出”;反之,会造成内存浪费。

示例代码:

#include <stdio.h>
#include <stdbool.h>
#define MAX_SIZE 100
// 定义一个静态栈结构体
typedef struct {
    int data[MAX_SIZE]; // 存储元素的数组
    int top;            // 栈顶指针,指向最后一个元素的位置
} StaticStack;
// 初始化栈
void initStack(StaticStack *s) {
    s->top = -1; // 栈为空时,top 初始化为 -1
}
// 检查栈是否为空
bool isEmpty(StaticStack *s) {
    return s->top == -1;
}
// 检查栈是否已满
bool isFull(StaticStack *s) {
    return s->top == MAX_SIZE - 1;
}
// 压栈
void push(StaticStack *s, int value) {
    if (isFull(s)) {
        printf("Stack Overflow! Cannot push %d\n", value);
        return;
    }
    s->data[++s->top] = value; // top 先加 1,再赋值
}
// 出栈
int pop(StaticStack *s) {
    if (isEmpty(s)) {
        printf("Stack Underflow! Cannot pop from an empty stack.\n");
        return -1; // 返回一个错误值
    }
    return s->data[s->top--]; // 先取值,top 再减 1
}
// 查看栈顶元素
int peek(StaticStack *s) {
    if (isEmpty(s)) {
        printf("Stack is empty.\n");
        return -1;
    }
    return s->data[s->top];
}
int main() {
    StaticStack myStack;
    initStack(&myStack);
    push(&myStack, 10);
    push(&myStack, 20);
    push(&myStack, 30);
    printf("Top element is: %d\n", peek(&myStack)); // 输出 30
    printf("Popped element: %d\n", pop(&myStack));  // 输出 30
    printf("Popped element: %d\n", pop(&myStack));  // 输出 20
    push(&myStack, 40);
    printf("Top element is: %d\n", peek(&myStack)); // 输出 40
    return 0;
}

B. 动态栈

动态栈使用动态内存分配(如 malloc, calloc, realloc)来实现,其大小可以在运行时改变。

  • 优点:灵活性高,可以根据需要动态调整大小,避免了静态栈的浪费和溢出问题。
  • 缺点:需要手动管理内存,有内存泄漏的风险。

实现思路: 通常使用链表来实现动态栈,每个节点包含数据和指向下一个节点的指针。push 操作在链表头部进行,pop 操作也从头部进行。

#include <stdio.h>
#include <stdlib.h>
// 定义链表节点
typedef struct StackNode {
    int data;
    struct StackNode *next;
} StackNode;
// 定义动态栈
typedef struct {
    StackNode *top; // 栈顶指针,指向链表的头节点
} DynamicStack;
// 初始化栈
void initDynamicStack(DynamicStack *s) {
    s->top = NULL;
}
// 压栈
void pushDynamic(DynamicStack *s, int value) {
    StackNode *newNode = (StackNode*)malloc(sizeof(StackNode));
    if (!newNode) {
        printf("Memory allocation failed!\n");
        return;
    }
    newNode->data = value;
    newNode->next = s->top; // 新节点指向原来的栈顶
    s->top = newNode;       // 新节点成为新的栈顶
}
// 出栈
int popDynamic(DynamicStack *s) {
    if (s->top == NULL) {
        printf("Stack is empty.\n");
        return -1;
    }
    StackNode *temp = s->top;
    int value = temp->data;
    s->top = s->top->next; // 栈顶指向下一个节点
    free(temp);            // 释放旧栈顶节点的内存
    return value;
}
// ... (可以继续实现 isEmpty, peek 等函数)
int main() {
    DynamicStack myStack;
    initDynamicStack(&myStack);
    pushDynamic(&myStack, 100);
    pushDynamic(&myStack, 200);
    printf("Popped: %d\n", popDynamic(&myStack)); // 输出 200
    printf("Popped: %d\n", popDynamic(&myStack)); // 输出 100
    return 0;
}

C 语言程序运行时的栈

这是 C 语言中“栈”更核心、更底层的含义,当你运行一个 C 程序时,操作系统会为其分配一块内存,这块内存被组织成几个区域,其中最重要的就是栈区

程序内存布局大致如下:

+------------------+
|      栈区        |  <- 高地址
|  (Stack)        |  - 局部变量
|  - 函数参数     |  - 函数返回地址
|  - 返回地址      |  - 函数调用帧
|  - 局部变量     |
+------------------+
|      堆区        |  <- 动态内存分配 (malloc, new)
|      (Heap)      |
+------------------+
|      (BSS段)     |  <- 未初始化的全局/静态变量
+------------------+
|      (数据段)    |  <- 已初始化的全局/静态变量
+------------------+
|      (代码段)    |  <- 程序的机器码
|      (Text)      |
+------------------+

栈区的工作原理:

  1. 函数调用:当程序调用一个函数时,系统会创建一个“栈帧”(Stack Frame)并压入栈顶,这个栈帧包含了:
    • 函数的参数
    • 函数的局部变量
    • 函数执行完后返回到调用点的地址
  2. 函数执行:函数在它的栈帧内执行,所有局部变量的内存都在这个栈帧上分配。
  3. 函数返回:当函数执行完毕并返回时,它的栈帧会被“弹出”栈,内存被自动释放。

栈区的重要特性:

  • 自动管理:内存的分配和释放是自动的,由编译器和运行时环境完成,程序员无需手动干预。
  • 速度快:因为内存分配和释放只是在移动一个栈顶指针,所以速度非常快。
  • 大小有限:栈的大小是有限的(Linux 上通常是几 MB),如果递归调用太深,或者在函数中定义了过大的局部变量(比如一个巨大的数组),就会导致栈溢出,程序会崩溃。
  • 生命周期:变量的生命周期与函数绑定,函数返回,变量就销毁了。

示例:理解函数调用栈

#include <stdio.h>
void functionB(int b) {
    int varB = b * 2;
    printf("In functionB: varB is at address %p, value is %d\n", &varB, varB);
}
void functionA(int a) {
    int varA = a + 10;
    printf("In functionA: varA is at address %p, value is %d\n", &varA, varA);
    functionB(varA);
}
int main() {
    int mainVar = 5;
    printf("In main: mainVar is at address %p, value is %d\n", &mainVar, mainVar);
    functionA(mainVar);
    printf("Back in main.\n");
    return 0;
}

当你运行这个程序时,调用栈的变化如下:

  1. main() 函数被调用,mainVar 被压入栈。
  2. main() 调用 functionA(5)functionA 的栈帧被压入栈,varAfunctionA 的栈帧中创建。
  3. functionA 调用 functionB(15)functionB 的栈帧被压入栈,varBfunctionB 的栈帧中创建。
  4. functionB 执行完毕,其栈帧被弹出。
  5. functionA 执行完毕,其栈帧被弹出。
  6. 最终只剩下 main() 的栈帧,程序结束。

注意:你可以打印出这些变量的地址,会发现 &mainVar > &varA > &varB(地址从高到低),这证明了它们是按照“后进先出”的顺序被分配在内存中的。


栈的核心思想

特性 作为数据结构的栈 作为程序运行时的栈
本质 一种 LIFO 的数据结构,用于存储数据。 程序运行时的一块内存区域,用于管理函数调用。
目的 提供一种受限的、高效的存取方式(如括号匹配、表达式求值、函数调用历史记录等)。 管理函数的执行流程、局部变量、参数和返回地址。
实现 可以用数组(静态)或链表(动态)手动实现。 由编译器和操作系统在底层自动实现和管理。
生命周期 由程序员手动创建和销毁(如果是自定义的)。 随函数调用自动创建和销毁。
核心操作 push, pop, peek 函数调用(压栈)和函数返回(出栈)。
  • 当你手动实现一个数据结构来解决特定问题时,你创建的是数据结构上的栈
  • 当你编写 C 程序时,系统为你自动创建和管理了一个运行时栈,它支撑着整个程序的执行。

理解这两种“栈”的区别和联系,对于掌握 C 语言的内存管理和程序运行机制至关重要。

-- 展开阅读全文 --
头像
dede点击排序如何实现?
« 上一篇 昨天
C语言rand函数源码如何实现随机数生成?
下一篇 » 昨天

相关文章

取消
微信二维码
支付宝二维码

目录[+]