C 语言 - 堆和栈解析

预先了解

栈指针(Stack Pointer, SP)

  • 栈指针又称栈顶指针。就像 栈的“手指”,指向 栈顶 的位置。它告诉 CPU / 系统:“栈顶在哪里,下一个要放的东西应该放在哪儿。”

    • 假设你的是一个竖着的书堆

      高地址 ↑
      [空闲内存]
      [局部变量2]      ← 先入栈的变量(稍早定义)
      [局部变量1]      ← 后入栈的变量(稍晚定义)
      [新的局部变量]   ← 栈顶,SP 指向这里
      低地址 ↓
      
  • 栈指针的工作方式

    • 入栈(push) → 把新变量放到栈顶,栈指针向下移动(在大多数系统中栈向低地址增长)
      • 放一本书 → 手指往下数,指向放的书 → SP 更新到新栈顶
    • 出栈(pop) → 栈指针向上移动,变量被“弹出”(释放)
      • 拿一本书 → 手指往上数,指向上一本书 → SP 回退
  • 栈指针就像这个手指,告诉你栈顶的位置

  • C 语言中的体现

    • 定义局部变量,系统通过 栈指针(SP) 来自动分配空间
    • 函数结束SP 回退,变量自动释放
    • 不用直接操作 SP(除非写汇编或底层嵌入式程序)

malloc 分配 — “向堆要空间”

  • 全称memory allocation(内存分配):在 堆(Heap) 上申请一块可以存储数据的空间。堆就像一个大书架,malloc 就是去书架上借一个空格放书。告诉系统:“我要一块大小 X 的空间”,系统找到合适的空闲区域,给你一个地址

  • 举例

    int *p;
    p = (int *)malloc(sizeof(int)); // 申请一个int大小的空间(32位系统中可以理解为4字节)
    *p = 42;                        // 往里面放数据
    //p 保存的是这块空间的地址
    //你可以通过 *p 来读写这个空间
    

free 释放 — “把空间还回去”

  • 把之前 malloc 分配的空间 释放 回堆,让系统可以重新使用

    • 这是必须的,如果free空间一直占用内存泄漏(Memory Leak)就像你借了书架的空格放书,用完要把书拿走,把空格归还给书架,否则别人就没地方放书了
  • 举例

    free(p); // 释放 p 指向的空间
    p = NULL; // 好习惯:避免悬空指针
    

栈(Stack)——“自动整理的书堆”

  • 比如在书桌上整理书

    • 每次用书就往书堆顶部放,用完就从顶部拿走越早放的也就堆到越后面才能使用,也就是 先进后出(LIFO) 的原则。
      • 先进后出(LIFO)先放进去的,后拿出来。栈顶就是最新放入的位置
      • 理解了栈指针后不难看出:栈的出栈和栈指针的出栈是相反的
  • 栈的全过程运作举例

    #include <stdio.h>
    
    void func(int a, int b) {
        int sum = a + b;          // 在 func 的栈帧中为 sum 分配空间(SP -= 4),让 SP 所指的地址往低 4 字节移动
        						  // int 占 4 个字节(这是标准 32 位系统的常见情况)
        						  //当编译器看到 int sum; 时,会生成汇编大致如下:
        						  //sub sp, sp, #4   ; SP = SP - 4,为 sum 分配 4 字节空间
    							  //总占用空间:sum、a、b等3个变量+返回地址(4*4).因此占用16字节
        printf("sum = %d\n", sum); // 调用 printf,会再建立一个新的栈帧
    } // 函数结束:SP += func 栈帧大小(sum、a、b、返回地址(func = 4*4) 全部出栈)
    	//因此 SP = SP + (4*4) 归0找回空间
    
    int main(void) {
        int x = 2;                // main 栈帧中分配 x (SP -= 4)
        int y = 3;                // main 栈帧中分配 y (SP -= 4)
        func(x, y);               // 调用 func 时执行如下操作(由 CPU + 编译器自动完成):
                                  //   ① 把参数 b 压栈(SP -= 4)
                                  //   ② 把参数 a 压栈(SP -= 4)
                                  //   ③ 把返回地址压栈(SP -= 4)
                                  //   ④ 跳转到 func()
                                  //
                                  // func() 运行完毕后:
                                  //   ⑤ 弹出返回地址(SP += 4)
                                  //   ⑥ 清理参数栈空间(SP += 8)
        return 0;                 // main 结束,整个程序栈由系统回收
    }
    // 一个程序只有一个返回地址
    //返回地址:当 CPU 执行类似 call func 的指令时,会把下一条指令的地址(即调用指令之后的位置)压入栈中,这就是返回地址
    	//目的:函数执行完后能回到调用点继续执行
    	//栈上只存这个返回地址,大小在 32 位系统上通常是 4 字节
    
  • 特点

    • 自动管理 → 函数里定义的局部变量,函数调用时自动分配,函数结束自动释
    • 快速 → 系统直接调整栈指针即可放
    • 空间有限太大容易溢出
  • 总结:栈就像桌上的书堆,每次放在顶上,用完从顶上拿走,整齐又快

堆(Heap)——“书架上的自由摆放”

  • 比如有一个大书架,可以随意把书放在书架上任何位置,需要的时候自己去找。就是 动态分配,系统不会自动管理。

  • 特点

    • 手动管理 → 需要程序员通过 malloc 分配,free 释放
    • 灵活空间大,可以存放各种大小的数据
    • 慢一点 → 系统要找到合适的空闲空间
  • 总结:堆就像书架,想放哪就放哪,但要记得收好,否则书架会乱掉

栈和堆的区别

特性 栈(Stack) 堆(Heap)
存储内容 局部变量、函数参数、返回地址 动态分配的对象或数据
分配方式 编译时确定 / 函数调用时自动分配 程序运行时手动分配(malloc / new)
生命周期 函数调用开始到结束 程序员控制,手动释放(free / delete)
管理方式 CPU + 编译器自动管理 程序员或垃圾回收管理
分配速度 快(连续空间,LIFO) 相对慢(需要查找空闲块)
大小限制 较小(通常几 MB) 较大(取决于可用内存)
访问方式 地址连续,可通过偏移访问 地址不连续,通过指针访问
典型用途 临时变量、函数调用 大型对象、动态数组、链表、树等结构
  • 简单举例

    #include <stdio.h>
    #include <stdlib.h>
    
    void func() {
        int a = 10;              // 栈上分配,函数结束后自动回收
        // *p 指针本身 → 栈上分配
        int *p = (int *)malloc(sizeof(int)); // 内容在堆上分配
        *p = 20;
        
        /*
        栈:
        | p (指针)      | 4 字节,存储堆地址
        | a (局部变量)  | 4 字节
        -----------------
        堆:
        | *p (int)      | 4 字节,存储实际值
        */
    
    
        printf("a = %d, *p = %d\n", a, *p);
        free(p);                 // 手动释放堆内存
    }
    
    int main() {
        func();
        return 0;
    }
    

结论

  • :自动、快速、空间小,就像桌上的书堆
  • :手动、灵活、空间大,就像书架上的书,需要自己管理。
posted @ 2025-10-11 15:55  阿俊学编程  阅读(66)  评论(0)    收藏  举报