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

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

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


相关推荐
昨日余光1 天前
建议收藏!我开发了一个免费无限制的AI绘画公益站!
开发语言·前端·javascript·ai作画·typescript
ZHOUPUYU1 天前
我在PHP里学到的“套路”与“反套路” 设计模式与依赖注入
开发语言·php
马士兵教育1 天前
2026年IT行业基本预测!计算机专业学生就业编程语言Java/C/C++/Python该如何选择?
java·开发语言·c++·人工智能·python·面试·职场和发展
野犬寒鸦1 天前
面试常问:HTTP 1.0 VS HTTP 2.0 VS HTTP 3.0 的核心区别及底层实现逻辑
服务器·开发语言·网络·后端·面试
geovindu1 天前
python: Null Object Pattern
开发语言·python·设计模式
lisus20071 天前
GO并发统计文件大小
开发语言·后端·golang
梦游钓鱼1 天前
Logger.h和Logger.cc文件分析
开发语言·c++
CRMEB系统商城1 天前
CRMEB标准版系统(PHP)v6.0公测版发布,商城主题市场上线~
java·开发语言·小程序·php
yangminlei1 天前
openclaw对接飞书
开发语言·python·飞书
临溟夜空的繁星1 天前
C++ STL-- vector
开发语言·c++