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

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

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


相关推荐
xieliyu.3 小时前
Java算法精讲:双指针(二)
java·开发语言·算法
何以解忧,唯有..4 小时前
Python包管理工具pip:从入门到精通
开发语言·python·pip
雪的季节4 小时前
RabbitMQ详解
开发语言
ice8130331815 小时前
【Python】Matplotlib折线图绘制
开发语言·python·matplotlib
三品吉他手会点灯5 小时前
C语言学习笔记 - 44.运算符和表达式 - 运算符2 - 除法与取余运算符
c语言·开发语言·笔记·算法
kkeeper~5 小时前
0基础C语言积跬步之动态内存管理
c语言·开发语言
橘右今5 小时前
2026 Java后端高频面试宝典
java·开发语言·面试
艾iYYY5 小时前
string 类的模拟实现
android·服务器·c语言·c++·算法
微小冷5 小时前
Julia卫星工具箱SatelliteToolbox简介
开发语言·航天·坐标转换·julia·卫星工具箱
2601_colin5 小时前
Codex插件全流程实战指南
开发语言·经验分享·笔记·微信开放平台