C/C++ 的内存管理,函数栈帧详讲

一、C/C++ 程序的内存布局

程序运行时,内存分为几个区域:

二、各区域详解

1. 栈区

特点:

  • 自动管理,编译器分配和释放
  • 速度快
  • 空间有限(通常几MB)
  • 向下增长(高地址→低地址)

存放什么:

cpp 复制代码
void func() {
    int a = 10;           // 栈上
    double b = 3.14;      // 栈上
    char arr[100];        // 栈上
    int* p = &a;          // p 在栈上,指向栈上的 a
}
// 函数结束,a、b、arr、p 自动销毁

生命周期:栈区(Stack) 的生命周期与作用域(Scope) 严格绑定:变量在进入作用域时创建,在离开作用域时自动销毁 。这是C/C++ 中最简单、最安全的内存管理方式,无需手动 new/delete

1.1验证栈向下增长:

函数调用时的栈增长:

cpp 复制代码
#include <iostream>

void func2() {
    int x = 100;
    std::cout << "func2 中 x 的地址:" << &x << std::endl;
}

void func1() {
    int y = 200;
    std::cout << "func1 中 y 的地址:" << &y << std::endl;
    func2();
}

int main() {
    int z = 300;
    std::cout << "main 中 z 的地址:" << &z << std::endl;
    func1();
    return 0;
}

结果:

|--------------|-------------|
| main 地址最高 | 栈底在高地址 |
| func1 地址更低 | 新栈帧在更低地址 |
| func2 地址最低 | 更深调用在更低地址 |
| 地址递减 | 栈向下增长 ✅ |

因为****栈向下增长,所以 栈顶在低地址,栈底在高地址

2. 堆区

特点:

  • 手动管理(new/malloc 分配,delete/free 释放)
  • 速度较慢(需要查找空闲内存)
  • 空间大(可达几GB)
  • 向上增长(低地址→高地址)
  • 生命周期:手动 delete 才销毁

存放什么:

cpp 复制代码
void func() {
    int* p1 = new int(10);      // 堆上
    int* p2 = (int*)malloc(4);   // 堆上
    int* arr = new int[1000];    // 堆上
    
    delete p1;
    free(p2);
    delete[] arr;
}
// 注意:p1、p2、arr 本身在栈上,它们指向的内存在堆上

3. 全局/静态数据区

存放什么:

cpp 复制代码
int global_var = 100;        // 全局区
static int static_var = 200; // 静态区

void func() {
    static int count = 0;    // 静态区(只初始化一次)
    count++;
}

生命周期: 程序开始到程序结束。

4. 常量区

存放什么:

cpp 复制代码
const char* str = "hello";   // "hello" 在常量区
const int MAX = 100;         // 可能被优化到常量区

// 注意:不能修改
// str[0] = 'H';  // ❌ 运行时错误

5. 代码段

存放什么:

cpp 复制代码
void func() {
    // 函数编译后的机器码存放在代码段
}

int main() {
    // main 的机器码也在代码段
}

三、栈 vs 堆 对比

特性
管理方式 自动 手动
分配速度
空间大小 小(几MB) 大(几GB)
生命周期 随作用域 手动控制
碎片问题
访问方式 直接 通过指针

四、详细示例:什么在栈,什么在堆

cpp 复制代码
int global = 10;              // 全局区

void func() {
    int a = 1;                // 栈
    int b = 2;                // 栈
    int arr[10];              // 栈
    
    int* p1 = new int(100);   // p1 在栈,指向堆
    int* p2 = new int[1000];  // p2 在栈,指向堆
    
    static int s = 5;         // 静态区
    const char* str = "hello";// str 在栈,"hello"在常量区
    
    delete p1;
    delete[] p2;
}

五、函数栈帧详解

什么是栈帧?

栈帧 = 一个函数在栈上占用的内存块

每次调用函数,都会在栈上创建一个栈帧,函数返回时销毁

栈帧结构:

关键点:

  • 局部变量在 ebp 的上方(高地址)
  • 参数在 ebp 的下方(低地址)
  • 返回地址在更下方

示例:函数调用过程:

cpp 复制代码
int add(int a, int b) {
    int c = a + b;
    return c;
}

int main() {
    int x = 1;
    int y = 2;
    int z = add(x, y);
    return 0;
}

栈帧变化过程:

步骤1:程序启动

cpp 复制代码
    高地址 ↓
            │
            ▼
┌─────────────────────┐
│  (空)              │
│                      │
│                      │
│                      │
│                      │
└─────────────────────┘
    低地址 ↑

步骤2:进入 main,创建 main 的栈帧

cpp 复制代码
    高地址 ↓
            │
            ▼
════════════════════════════════════════════════════
┌─────────────────────────────────────────────┐
│              main 的栈帧                     │  ← 高地址(先创建)
├─────────────────────────────────────────────┤
│  局部变量: z = ?                             │  ← main.ebp + 12
├─────────────────────────────────────────────┤
│  局部变量: y = 2                             │  ← main.ebp + 8
├─────────────────────────────────────────────┤
│  局部变量: x = 1                             │  ← main.ebp + 4
├─────────────────────────────────────────────┤ ← 0x7FFFFFFF (main.ebp)
│  保存的旧 ebp                                │  ← main.ebp + 0
├─────────────────────────────────────────────┤
│  返回地址(返回到系统)                       │  ← main.ebp - 4
└─────────────────────────────────────────────┘
════════════════════════════════════════════════════
    低地址 ↑

步骤3:准备调用 add(x, y)

调用前,压入参数和返回地址:

cpp 复制代码
    高地址 ↓
            │
            ▼
════════════════════════════════════════════════════
┌─────────────────────────────────────────────┐
│              main 的栈帧                     │
├─────────────────────────────────────────────┤
│  局部变量: z = ?                             │
├─────────────────────────────────────────────┤
│  局部变量: y = 2                             │
├─────────────────────────────────────────────┤
│  局部变量: x = 1                             │
├─────────────────────────────────────────────┤
│  保存的旧 ebp                                │
├─────────────────────────────────────────────┤
│  返回地址(返回到系统)                       │
└─────────────────────────────────────────────┘
════════════════════════════════════════════════════
┌─────────────────────────────────────────────┐
│          调用 add 的信息区                   │  ← 中间区
├─────────────────────────────────────────────┤
│  返回地址(main调用add后返回的位置)           │  ← 压栈
├─────────────────────────────────────────────┤
│  参数: b = 2(传递给add的第二个参数)         │  ← 压栈
├─────────────────────────────────────────────┤
│  参数: a = 1(传递给add的第一个参数)         │  ← 压栈
└─────────────────────────────────────────────┘
════════════════════════════════════════════════════
    低地址 ↑

注意:

  • 参数从右到左压栈:先压 b,再压 a
  • esp 向下移动(数值变小)

步骤4:进入 add,创建 add 的栈帧

cpp 复制代码
    高地址 ↓
            │
            ▼
════════════════════════════════════════════════════
┌─────────────────────────────────────────────┐
│              main 的栈帧                     │
├─────────────────────────────────────────────┤
│  局部变量: z = ?                             │
├─────────────────────────────────────────────┤
│  局部变量: y = 2                             │
├─────────────────────────────────────────────┤
│  局部变量: x = 1                             │
├─────────────────────────────────────────────┤
│  保存的旧 ebp                                │
├─────────────────────────────────────────────┤
│  返回地址(返回到系统)                       │
└─────────────────────────────────────────────┘
════════════════════════════════════════════════════
┌─────────────────────────────────────────────┐
│          调用 add 的信息区                   │
├─────────────────────────────────────────────┤
│  返回地址                                    │
├─────────────────────────────────────────────┤
│  参数: b = 2                                 │
├─────────────────────────────────────────────┤
│  参数: a = 1                                 │
└─────────────────────────────────────────────┘
════════════════════════════════════════════════════
┌─────────────────────────────────────────────┐
│              add 的栈帧                      │
├─────────────────────────────────────────────┤
│  保存的旧 ebp(指向main.ebp)                │
├─────────────────────────────────────────────┤
│  局部变量: c = 3                             │  ← 计算完成
└─────────────────────────────────────────────┘
════════════════════════════════════════════════════
    低地址 ↑

步骤6:add 返回

  1. 保存返回值 c = 3(通常放到 EAX 寄存器)
  2. 销毁 add 的栈帧
  3. 恢复 main 的栈帧
  4. 根据"返回地址"跳回 main
cpp 复制代码
    高地址 ↓
            │
            ▼
════════════════════════════════════════════════════
┌─────────────────────────────────────────────┐
│              main 的栈帧                     │  ← 高地址
├─────────────────────────────────────────────┤
│  局部变量: z = ?                             │  ← 等待接收返回值
├─────────────────────────────────────────────┤
│  局部变量: y = 2                             │
├─────────────────────────────────────────────┤
│  局部变量: x = 1                             │
├─────────────────────────────────────────────┤
│  保存的旧 ebp                                │
├─────────────────────────────────────────────┤
│  返回地址(返回到系统)                       │
└─────────────────────────────────────────────┘
════════════════════════════════════════════════════
    低地址 ↑

注意:esp 向上移动(数值变大),add 的栈帧被销毁

步骤7:main 接收返回值

int z = add(x, y); // z = 3

cpp 复制代码
    高地址 ↓
            │
            ▼
════════════════════════════════════════════════════
┌─────────────────────────────────────────────┐
│              main 的栈帧                     │
├─────────────────────────────────────────────┤
│  局部变量: z = 3                             │  ← 接收返回值
├─────────────────────────────────────────────┤
│  局部变量: y = 2                             │
├─────────────────────────────────────────────┤
│  局部变量: x = 1                             │
├─────────────────────────────────────────────┤
│  保存的旧 ebp                                │
├─────────────────────────────────────────────┤
│  返回地址(返回到系统)                       │
└─────────────────────────────────────────────┘
════════════════════════════════════════════════════
    低地址 ↑

步骤8:main 结束,程序退出

六、两个关键指针

ebp(栈帧基址指针)

  • 固定指向当前栈帧的基址
  • 用于定位局部变量和参数
  • 通过"旧 ebp"形成调用链

esp(栈顶指针)

  • 永远指向"最新"的内存位置
  • 动态变化:
    • 调用函数:esp 减小(向下移动)
    • 返回函数:esp 增大(向上移动)

七、常见问题

问题1:栈溢出

cpp 复制代码
void func() {
    int arr[10000000];  // 栈上分配 40MB
    // ❌ 栈溢出!栈通常只有几MB
}

// 正确做法:用堆
void func2() {
    int* arr = new int[10000000];  // 堆上分配
    delete[] arr;
}

问题2:返回局部变量地址

cpp 复制代码
int* func() {
    int a = 10;
    return &a;  // ❌ 危险!返回栈上局部变量的地址
}
// 函数结束,a 已销毁,返回的指针指向已释放的栈空间

int main() {
    int* p = func();
    *p = 20;  // 未定义行为
}

问题3:内存泄漏

cpp 复制代码
void func() {
    int* p = new int(10);
    // 忘记 delete
}
// 堆上的内存泄漏,直到程序结束才回收

// 正确做法
void func2() {
    int* p = new int(10);
    delete p;
}

八、堆内存管理详解

malloc / free

cpp 复制代码
int* p = (int*)malloc(sizeof(int) * 10);  // 分配 40 字节
// 使用 p
free(p);  // 释放

new / delete

cpp 复制代码
int* p1 = new int;        // 分配一个 int
int* p2 = new int(10);    // 分配并初始化为 10
int* arr = new int[10];   // 分配数组

delete p1;
delete p2;
delete[] arr;             // 数组用 delete[]

数组用delete[ ]

new vs malloc

特性 new malloc
是运算符还是函数 运算符 函数
返回类型 类型安全 void*(c++需要强制类型转换)
构造函数 会调用(创建对象会调用构造函数) 不会调用
失败处理 抛异常 返回 NULL
内存大小 自动计算 手动计算

九、总结图表

存储位置 内容 生命周期
局部变量、函数参数 随作用域
new/malloc 分配的内存 手动控制
全局区 全局变量、静态变量 程序全程
常量区 字符串常量、const 程序全程
代码段 程序机器码 程序全程
相关推荐
文静小土豆2 小时前
Java 应用上 K8s 全指南:从部署到治理的生产级实践
java·开发语言·kubernetes
wuyoula2 小时前
AI导航智能决策系统源码 附教程
c++·tcp/ip·源码
zhimingwen2 小时前
初探 Java 後端開發:解決 macOS 環境下 Spring Boot 項目啟動的各類「坑」
java·spring boot
浅念-2 小时前
从LeetCode入门位运算:常见技巧与实战题目全解析
数据结构·数据库·c++·笔记·算法·leetcode·牛客
Rsun045512 小时前
3、Java 工厂方法模式从入门到实战
java·开发语言·工厂方法模式
田梓燊2 小时前
leetcode 142
android·java·leetcode
亚空间仓鼠2 小时前
Ansible之Playbook(三):变量应用
java·前端·ansible
码路飞3 小时前
昨天还在发 Qwen3.5,今天技术负责人就被阿里云赶走了
java·javascript
程序员老邢3 小时前
【技术底稿 15】SpringBoot 异步文件上传实战:多线程池隔离 + 失败重试 + 实时状态推送
java·经验分享·spring boot·后端·程序人生·spring