3. C/C++内存模型:堆、栈、全局区、常量区、代码区详解

内存泄漏,说白了就是内存管理出了问题。但你要想管好内存,首先得知道内存长什么样。C/C++程序跑起来之后,操作系统会给它划分出几个不同的区域。我当年刚入行时,觉得这东西就是背概念,直到有一次线上服务OOM,我才真正意识到——不理解内存模型,你连问题出在哪都找不到。

今天咱们就把这五个区掰开揉碎讲清楚。嗯,先从最常用的栈开始。

3.1 栈区(Stack)

栈是编译器自动管理的内存区域。你写的局部变量、函数参数、返回值,全都在栈上分配。它的特点是:自动分配,自动释放。

核心特征:

  • 生命周期:函数执行期间
  • 分配方式:后进先出(LIFO)
  • 大小限制:通常1MB~8MB(取决于编译器和操作系统)
  • 速度:极快(仅需移动栈指针)

举个例子你就明白了:

void func() {
    int a = 10;          // a 在栈上
    char buf[1024];      // buf 也在栈上
    // 函数结束,a和buf自动销毁
}

我个人习惯把栈想象成一个叠盘子的架子。你放盘子(压栈),取盘子(弹栈),永远只能从最上面操作。速度快,但空间有限。

我曾经踩过的坑:在递归函数里声明了一个512KB的局部数组,递归深度只有10层,结果栈溢出了。为什么?因为每层递归都会分配新的栈帧,512KB × 10 = 5MB,直接超过了默认栈大小。所以记住:大内存别放栈上,用堆。

3.2 堆区(Heap)

堆是程序员手动管理的内存区域。你用 malloc、new 申请的内存,全都在堆上。它的特点是:手动分配,手动释放。

核心特征:

  • 生命周期:从分配直到显式释放
  • 分配方式:任意顺序(通过内存管理算法)
  • 大小限制:取决于系统可用内存(通常远大于栈)
  • 速度:相对较慢(需要查找空闲块、可能触发系统调用)
void func() {
    int* p = (int*)malloc(sizeof(int) * 100);  // 堆上分配
    // 使用 p...
    free(p);  // 必须手动释放,否则泄漏
}

堆的问题在于——你忘了释放,它就泄漏了。我在项目中遇到过最典型的情况:一个循环里不断 new 对象,但忘记 delete,跑了一晚上,内存占用从200MB涨到了8GB。服务器直接挂了。

我的建议:能用智能指针就别裸用 malloc/free。C++11 之后的 std::unique_ptr 和 std::shared_ptr 能帮你自动管理堆内存生命周期,减少90%的泄漏风险。

3.3 全局区(静态区)

全局变量和静态变量存放在这里。程序启动时分配,程序结束时释放。说白了,它活在整个程序的生命周期里。

包含两类:

  • 已初始化全局区(.data段):存放已初始化的全局变量和静态变量
  • 未初始化全局区(.bss段):存放未初始化的全局变量和静态变量(程序加载时自动清零)
int g_val = 42;          // .data段
static int s_val = 10;   // .data段
int g_uninit;            // .bss段(自动初始化为0)

int main() {
    static int local_static = 5;  // 也是.data段
    return 0;
}

你想想看,全局变量为什么能跨函数访问?因为它不在栈上,也不在堆上,它在全局区,地址固定,整个程序都能看到。但这也带来了问题——多线程环境下,全局变量是共享的,不加锁就会出竞态条件。

注意:全局区不会泄漏,因为程序结束时会统一回收。但滥用全局变量会导致代码耦合严重,维护起来很痛苦。我见过一个项目,全局变量定义了上百个,改一个变量要排查所有文件……那滋味,谁改谁知道。

3.4 常量区

常量区存放的是字符串常量、const 修饰的全局常量等。这个区域通常是只读的,你试图修改它,程序会直接崩溃(段错误)。

const char* str = "Hello";  // "Hello" 存在常量区
// str[0] = 'h';  // 运行时崩溃!常量区不可写

const int MAX = 100;        // 如果是全局const,也在常量区

我记得有一次调试一个诡异的崩溃问题,查了半天发现是有人用 char* 指向了字符串常量,然后试图修改它。嗯,这种错误在C语言里很常见,C++里用 std::string 就能避免。

避坑指南:常量区的内容在程序整个生命周期内都存在,不会释放。所以不用担心泄漏。但要注意,如果你用 char* p = "abc"; 这种写法,p 指向的是常量区,千万别试图修改它。

3.5 代码区(文本段)

代码区存放的是程序的机器指令。也就是你写的那些函数、逻辑,编译后变成的二进制代码。这个区域也是只读的,防止程序在运行时意外修改自己的指令。

特点:

  • 存储:编译后的二进制指令
  • 权限:只读 + 可执行(不可写)
  • 生命周期:程序启动到结束
  • 大小:取决于代码量

代码区不会泄漏,因为它的大小在编译时就固定了。但如果你写了大量模板展开或者内联函数,代码区会膨胀。我见过一个项目,滥用模板导致生成的可执行文件有200MB……加载都慢半拍。

3.6 一张图看懂内存模型

说了这么多,咱们用一张SVG图把整个内存布局串起来。你一看就明白了:

C/C++ 进程内存布局 高地址 栈区(Stack) 局部变量、函数参数、返回地址 自动分配/释放,大小有限(~8MB) 堆区(Heap) 动态分配内存(malloc/new) 手动管理,容易泄漏 全局区(.data + .bss) 全局变量、静态变量 程序启动分配,结束释放 常量区(只读) 代码区(只读 + 可执行) 低地址

这张图从上到下,地址从高到低。栈和堆相向生长——栈向下,堆向上。如果它们撞上了,就是栈溢出或者堆内存耗尽。嗯,这就是为什么递归太深或者疯狂 new 会导致程序崩溃。

3.7 各区域对比总结

区域 存储内容 生命周期 管理方式 是否泄漏
栈区 局部变量、函数参数 函数执行期间 编译器自动管理 不会泄漏
堆区 动态分配的内存 分配到释放 程序员手动管理 最容易泄漏
全局区 全局/静态变量 程序整个生命周期 编译器自动管理 不会泄漏
常量区 字符串常量、const常量 程序整个生命周期 只读,不可修改 不会泄漏
代码区 机器指令 程序整个生命周期 只读 + 可执行 不会泄漏

一句话总结:栈和全局区你不用担心泄漏,编译器帮你管好了。常量区和代码区是只读的,也安全。真正让你头疼的,只有堆。后面咱们讲的所有内存泄漏定位与修复技术,99%都是在跟堆打交道。

好了,这一章的内容就到这里。理解了内存模型,你就能明白程序运行时数据到底待在哪,出了问题时该往哪个方向查。下一章咱们会深入堆内存的分配机制,看看 malloc 到底是怎么工作的。


可可精品资料库