单片机/C语言八股:(七)C 程序运行时内存布局的动态变化

上一篇 下一篇
栈内存和堆内存的区别

目 录


C 程序运行时内存布局的动态变化

1)加载时内存布局(静态视图)

当一个 C 程序被操作系统加载后,其虚拟地址空间通常划分为以下几部分(自低地址 → 高地址):

复制代码
+---------------------+
|      栈 (Stack)     | ← 向下增长(高地址 → 低地址)
|        ↓            |
+---------------------+
|        ↑            |
|        堆 (Heap)    | ← 向上增长(低地址 → 高地址)
+---------------------+
| 未初始化数据段 (.bss) | ← 全局/静态变量(未初始化或 =0)
+---------------------+
| 初始化数据段 (.data)  | ← 全局/静态变量(已显式初始化)
+---------------------+
|    代码段 (Text)     | ← 只读,存放函数机器码、字符串常量等
+---------------------+
|    保留/内核空间      | (用户程序不可访问)
+---------------------+

注意:这是 逻辑上的虚拟内存布局 ,实际物理内存由操作系统管理。

其中核心内存区域详解⭐️:

区域 存放内容 生命周期 可读写? 举例
栈(Stack) 函数调用时的普通局部变量、参数等 函数执行期间 可读写 void foo() { int a = 5; } 中的 a
堆(Heap) 动态分配的内存(malloc/.. 手动控制(free 可读写 int *p = malloc(100);
.bss 段 未初始化或初始化为 0 的 全局/static 变量 整个程序 可读写 int y;static int z = 0;
.data 段 已初始化的 全局变量/static 变量 整个程序 可读写 int x = 10;
代码段(Text) 函数编译后的机器指令、 字符串字面量("hello") 整个程序 只读 int main() { ... } 的二进制码

2)运行时内存的动态变化

我们通过一个具体例子,观察程序执行过程中内存如何变化:

c 复制代码
#include <stdio.h>
#include <stdlib.h>

int global_init = 42;     // .data(初始化的全局变量)
int global_uninit;        // .bss(未初始化的全局变量)

int add(int b) {
    static int a=5;	      // .data(初始化的static变量)
    int sum = a+b;        // 栈上(sum是函数的普通局部变量)
    return sum;
}

int main() {
    static int s = 100;   // .data(初始化的static变量)
    int x = 5;            // 栈
    int *p = malloc(sizeof(int)); // 指针变量p在main的栈帧中,但malloc从堆上分配了4字节的空间,且地址存入p
    *p = 20;

    int result = add(*p); // result存在main函数的栈帧中,并且调用函数add时,会压入新栈帧

    free(p);
    return 0;
}

逐阶段分析内存变化:

  • 阶段 1:程序加载(exec)

    • 操作系统将可执行文件的 .text.data.bss 段映射到进程虚拟地址空间。

    • .bss 被清零(C 标准要求未初始化全局变量为 0)。

    • 此时堆和栈为空(但已预留空间)

  • 阶段 2:进入 main() 函数

    • 创建 main 的栈帧(stack frame)

      • 局部变量 x = 5 分配在栈上
      • 指针 p(本身是局部变量)也分配在栈上,初始值未定义
    • 执行 malloc(sizeof(int))

      • 上分配 4 字节(int 为 4 字节)
      • 返回堆地址(如 0x1000),存入栈上的 p
      • *p = 20 → 堆地址 0x1000 处写入 20
    • 此时内存中:

      • 栈:x=5, p=0x1000

      • 堆:[0x1000] = 20

  • 阶段 3:调用 add(x, *p)

    • 压入新的栈帧(用于 add 函数)

      • 参数 a=5, b=20(通过寄存器或栈传递,取决于调用约定)

      • 局部变量 sum = 25 分配在 add 的栈帧中

      • 执行 return sum

        • 将结果(25)放入寄存器
        • 弹出 add 的栈帧 (局部变量 sum 自动销毁)
    • 回到 main,将返回值赋给 result(栈上新变量)

  • 阶段 4:free(p) 和程序结束

    • free(p) 通知堆管理器:地址 0x1000 的内存可回收(但内容不一定立即清零)

    • main 返回,销毁 main 的栈帧

    • 程序退出,操作系统回收整个进程的内存(包括堆、栈、数据段等)

3)关键概念补充

3.1)栈帧(Stack Frame)

  • 每次函数调用都会创建一个栈帧(栈中一段连续的内存),包含:
    • 参数
    • 局部变量
    • 返回地址(调用者下一条指令地址)
    • 保存的寄存器(如 BP)
  • 函数返回时自动销毁 → 局部变量生命周期 = 函数调用期间

4)总结

  • 函数代码 → 代码段(只读)
  • 全局/静态变量 → .data / .bss
  • 局部变量/参数 → 栈(自动管理)
  • 动态内存 → 堆(手动管理)
  • 程序运行 = 栈帧不断压入/弹出 + 堆内存分配/释放

5)再提醒:函数相关内存的变化

当你编写了一个 C 函数,编译的时候,编译器会将这段 C 代码翻译成 CPU 能直接执行的二进制机器码 (如 x86 指令),这些机器码被加载到内存的 代码段/文本段(只读、共享) 中。

函数名 在链接阶段会被解析为该函数第一条指令在内存中的 地址 ,所以函数名本质上是一个指向其代码起始地址的常量指针

函数体本身(代码)永远在代码段 ,但调用函数时产生的普通局部变量、参数等 是在上分配的。


相关推荐
励志的小陈3 小时前
贪吃蛇(C语言实现,API)
c语言·开发语言
Makoto_Kimur3 小时前
java开发面试-AI Coding速成
java·开发语言
laowangpython3 小时前
Gurobi求解器Matlab安装配置教程
开发语言·其他·matlab
wengqidaifeng3 小时前
python启航:1.基础语法知识
开发语言·python
观北海3 小时前
Windows 平台 Python 极简 ORB-SLAM3 Demo,从零实现实时视觉定位
开发语言·python·动态规划
Ulyanov5 小时前
《PySide6 GUI开发指南:QML核心与实践》 第二篇:QML语法精要——构建声明式UI的基础
java·开发语言·javascript·python·ui·gui·雷达电子对抗系统仿真
码界筑梦坊5 小时前
357-基于Java的大型商场应急预案管理系统
java·开发语言·毕业设计·知识分享
anzhxu5 小时前
Go基础之环境搭建
开发语言·后端·golang
yu85939586 小时前
基于MATLAB的随机振动仿真与分析完整实现
开发语言·matlab
赵钰老师6 小时前
【结构方程模型SEM】最新基于R语言结构方程模型分析
开发语言·数据分析·r语言