1. 内存布局:栈、堆、全局/静态区、常量区、代码区详解

咱们写 C++ 的,天天跟内存打交道。但说真的,很多人写了几年代码,内存到底怎么布局的,心里还是笔糊涂账。我刚开始带团队的时候,有个同事 debug 一个野指针问题搞了三天,最后发现是栈内存返回了——说白了,就是没搞懂内存布局。

今天咱们就把这五个区域掰开揉碎讲清楚。你想想看,搞懂了这些,很多奇奇怪怪的崩溃问题,一眼就能看穿。

1.1 栈区(Stack)—— 自动管理的小能手

栈,是编译器自动分配和释放的。局部变量、函数参数、返回值,都在这上面。它的特点是:快,但小

我记得有一次,一个同事在函数里定义了一个 10MB 的局部数组,程序一跑就崩。为什么?栈空间默认也就 1MB 到 8MB(看平台),你一个 10MB 的数组直接怼上去,栈溢出了。

栈的核心特征:

  • 自动分配、自动释放(函数返回时弹出)
  • 连续内存,地址从高到低增长
  • 大小固定(通常 1MB ~ 8MB)
  • 线程私有,每个线程有自己的栈
void foo() {
    int a = 42;          // a 在栈上
    char buf[1024];      // buf 也在栈上
    // 函数结束,a 和 buf 自动销毁
}

这里有个坑:不要返回栈上变量的地址或引用。函数返回后,栈帧被回收,那块内存就成了悬空指针。我曾经在代码 review 时看到过这样的写法,当场就拦下来了——这种 bug 很难复现,但一旦出现,就是偶发性的崩溃。

避坑指南:我曾经接手过一个项目,里面大量使用返回局部变量引用的写法。结果线上服务每隔几小时就随机崩溃一次,查了整整一周才定位到。从那以后,我定了个规矩:函数返回值,要么传值,要么传堆上分配的对象(智能指针)。

1.2 堆区(Heap)—— 灵活但需谨慎

堆,是程序员手动管理的内存区域。new、malloc 分配的内存都在堆上。它的特点是:大,但慢

为什么慢?因为堆分配需要查找空闲内存块、处理碎片、可能触发系统调用。相比之下,栈分配只是移动一下栈指针,一条指令的事。

void bar() {
    int* p = new int(42);   // p 在栈上,但指向的 int 在堆上
    // 使用 p...
    delete p;               // 必须手动释放
}

堆上的内存不会自动释放,这就是内存泄漏的根源。我见过最夸张的一个项目,跑了一周后吃掉 32GB 内存——全是 new 了没 delete。

我的建议:现代 C++ 里,尽量别裸用 new/delete。用 std::unique_ptr 或 std::shared_ptr,让智能指针帮你管理生命周期。我个人的习惯是:代码里出现裸 new,就要有充分的理由。

1.3 全局/静态区(Global/Static Area)—— 程序启动就存在

全局变量、静态变量(包括函数内的 static 局部变量),都存放在这里。它们在程序启动时初始化,程序结束时才销毁。

嗯,这里要注意一点:全局变量的初始化顺序在不同编译单元之间是未定义的。我曾经踩过这个坑:两个全局对象互相依赖对方的构造函数,结果程序启动就崩。

int global_val = 100;           // 全局区
static int static_val = 200;    // 静态区

void func() {
    static int local_static = 0; // 也在静态区,但只在首次调用时初始化
    local_static++;
}

避坑指南:我曾经在一个大型服务里,看到几十个全局对象互相依赖。每次启动顺序稍微一变,就出诡异的问题。最后我重构了代码,用局部静态变量替代全局对象——C++11 保证了局部 static 变量的线程安全初始化,比全局变量靠谱多了。

1.4 常量区(Constant Area)—— 只读,别想改

字符串常量、const 修饰的全局变量,存放在常量区。这块内存是只读的,你试图修改它,程序直接崩溃给你看。

const char* str = "hello";  // "hello" 在常量区,str 指针在栈上
// str[0] = 'H';            // 崩溃!常量区不可写

const int MAX = 100;        // 如果编译器优化,可能直接嵌入代码中

我记得有个新手同事,写了个函数试图修改字符串常量,结果每次跑都 Segmentation Fault。他 debug 了半天,最后我过去看了一眼,说:「你改的是常量区,能不崩吗?」

1.5 代码区(Code Area / Text Segment)—— 你的指令在这里

代码区存放程序的机器指令。它是只读的,防止程序意外修改自己的代码。函数体、类的方法实现,编译后都放在这里。

你写的每个 if、for、while,最终都变成一堆二进制指令躺在代码区。CPU 从代码区取指令,一条一条执行。

void print_hello() {
    // 这个函数的机器码在代码区
    std::cout << "Hello" << std::endl;
}

1.6 一张图看懂内存布局

下面这张图,把五个区域的关系画清楚了。我建议你把它记在脑子里——debug 的时候特别有用。

C++ 进程内存布局(虚拟地址空间) 高地址 栈区(Stack) 局部变量、函数参数、返回地址 向下增长(高地址 → 低地址) 堆区(Heap) 动态分配(new/malloc) 向上增长(低地址 → 高地址) 全局/静态区(Data Segment) 全局变量、静态变量 常量区(Read-only Data) 代码区(Text Segment) 低地址

1.7 各区域对比一览

区域 分配方式 生命周期 大小 读写权限 典型问题
栈区 自动 函数执行期间 小(MB级) 读写 栈溢出、返回悬空指针
堆区 手动(new/delete) 从分配到释放 大(GB级) 读写 内存泄漏、野指针
全局/静态区 自动(程序启动) 整个程序运行期 取决于变量数量 读写 初始化顺序问题
常量区 自动(程序启动) 整个程序运行期 取决于常量数量 只读 修改常量导致崩溃
代码区 自动(程序加载) 整个程序运行期 取决于代码量 只读 无(但代码 bug 会导致其他区出问题)

1.8 一个综合示例

咱们写个例子,把五个区域串起来看看:

#include <iostream>
#include <string>

// 全局变量 → 全局/静态区
int g_count = 0;

// 字符串常量 "Hello" → 常量区
const char* g_str = "Hello";

// 静态全局变量 → 全局/静态区
static int s_value = 42;

void demo() {
    // 局部变量 → 栈区
    int local_var = 100;
    
    // 动态分配 → 堆区
    int* heap_var = new int(200);
    
    // 局部静态变量 → 全局/静态区(但作用域在函数内)
    static int call_count = 0;
    call_count++;
    
    std::cout << "local: " << local_var << "\n";
    std::cout << "heap: " << *heap_var << "\n";
    std::cout << "call_count: " << call_count << "\n";
    
    delete heap_var;  // 释放堆内存
}

// demo 函数的机器码 → 代码区
int main() {
    demo();
    demo();
    return 0;
}

运行结果:

local: 100
heap: 200
call_count: 1
local: 100
heap: 200
call_count: 2

看到没?call_count 是静态局部变量,它在全局/静态区,所以两次调用 demo 它的值会累加。而 local_var 每次都在栈上重新创建,所以始终是 100。

我的经验:搞懂内存布局,debug 效率能提升一倍。遇到崩溃,先问自己:这个变量在哪个区?生命周期对不对?有没有可能被其他线程意外修改?想清楚这些,问题往往就迎刃而解了。

好了,内存布局这块就讲到这里。记住这张图,记住每个区的特点,后面讲智能指针的时候,你会发现自己理解得更深。

专注资料整理