单片机/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 小时前
AI开发C语言应用按步走,表达式计算器calc的第一步,中缀表达式词法分析
c语言·开发语言·atomcode
灯澜忆梦14 小时前
GO_文件处理---字符串操作
开发语言·golang
-银雾鸢尾-14 小时前
里氏替换原则
开发语言·c#·里氏替换原则
এ慕ོ冬℘゜14 小时前
JavaScript 数组核心方法实战|新增元素 + 数组转字符串 零基础详解
开发语言·javascript·ecmascript
一个初入编程的小白14 小时前
C语言:函数栈帧与销毁
c语言·开发语言
czhc114007566314 小时前
718:public internl class;partial []
开发语言
临床数据科学和人工智能兴趣组14 小时前
R语言因其强大的统计功能、灵活的编程环境、活跃的社区支持和强大的R扩展包,迅速成为统计学和数据科学领域的首选工具之一
开发语言·数据分析·r语言·r语言-4.2.1
__log15 小时前
弱网环境下的“生命线“:从AI流式响应到大文件上传的稳定性
开发语言·人工智能·php
巧克力男孩dd15 小时前
Python超典型练习题(第一次作业)
开发语言·python·算法
TlSfoward15 小时前
TLSFOWARD TLS指纹
开发语言·数据库·爬虫·搜索引擎·https·php