| 上一篇 | 下一篇 |
|---|---|
| 栈内存和堆内存的区别 |
目 录
- [C 程序运行时内存布局的动态变化](#C 程序运行时内存布局的动态变化)
-
- 1)加载时内存布局(静态视图)
- 2)运行时内存的动态变化
- 3)关键概念补充
-
- [3.1)栈帧(Stack Frame)](#3.1)栈帧(Stack Frame))
- 4)总结
- 5)再提醒:函数相关内存的变化
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 指令),这些机器码被加载到内存的 代码段/文本段(只读、共享) 中。
函数名 在链接阶段会被解析为该函数第一条指令在内存中的 地址 ,所以函数名本质上是一个指向其代码起始地址的常量指针 。
函数体本身(代码)永远在代码段 ,但调用函数时产生的普通局部变量、参数等 是在栈上分配的。