单片机/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 指令),这些机器码被加载到内存的 代码段/文本段(只读、共享) 中。

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

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


相关推荐
酉鬼女又兒13 小时前
零基础快速入门前端ES6 核心特性详解:Set 数据结构与对象增强写法(可用于备赛蓝桥杯Web应用开发)
开发语言·前端·javascript·职场和发展·蓝桥杯·es6
人大博士的交易之路13 小时前
数据结构算法——python数据结构
开发语言·数据结构·python
Han_han91913 小时前
面向对象高级 继承(extends):
开发语言·python
前端老石人13 小时前
邂逅前端开发:从基础到实践的全景指南
开发语言·前端·html
初生牛犊不怕苦14 小时前
与AI一起学习《C专家编程》:数组与指针
c语言·学习·算法
白毛大侠14 小时前
Go Goroutine 与用户态是进程级
开发语言·后端·golang
ForteScarlet14 小时前
从 Kotlin 编译器 API 的变化开始: 2.3.20
android·开发语言·后端·ios·开源·kotlin
elseif12314 小时前
浅谈 C++ 学习
开发语言·c++·学习
SuperEugene14 小时前
Vue3 性能优化规范:日常必做优化(不玄学、可落地)|可维护性与兜底规范篇
开发语言·前端·javascript·vue.js·性能优化·前端框架
Binary-Jeff14 小时前
Spring 创建 Bean 的关键流程
java·开发语言·前端·spring boot·后端·spring·学习方法