C++ 的栈与堆:变量到底住在哪

一句话:普通局部变量住在栈上 — 纳秒级分配、离开作用域自动销毁。堆给那些必须活得比作用域久、或运行期才知道大小的东西 — 更大更灵活,但要你分配、你释放。

亲眼看生命周期差异

下面的函数有个局部变量,程序有个全局变量。把函数调三次,看谁记得住什么:

#include <iostream>
using namespace std;

int counter = 0;   // 全局:活整个程序

void bump() {
    int local = 0; // 栈:每次调用重新出生
    local++;
    counter++;
    cout << "local=" << local << " counter=" << counter << endl;
}

int main() {
    bump();
    bump();
    bump();
    return 0;
}

local 每次都打出 1 — 它在 bump() 的一次调用里出生、存活、死亡,在栈上。counter 跨调用保持自己的值,因为它的生命周期是整个程序。生命周期就是栈/堆之别的核心。

并排对比

栈堆
谁来分配自动 — 声明即得你 — new(或容器/智能指针)
生命周期随作用域结束直到 delete(或它的主人死)
速度极快(挪一个指针)较慢(记账、找空位)
大小小 — 一般 ~1–8 MB基本等于"可用内存"
何时死作用域退出 — 有保证你释放才死 — 不放就是泄漏
典型用途局部变量、函数参数大数据、运行期定大小的数据、共享对象

堆分配:new 与 delete

(浏览器引擎暂不支持 new/delete — 这段在桌面编译器上跑。)

int main() {
    int* p = new int(42);   // 在堆上分配,p 装着地址
    cout << *p << endl;     // 42
    delete p;               // 还内存 — 你的活,不是作用域的
    return 0;
}

每个 new 必须恰好配一个 delete。漏掉 delete → 内存泄漏(再也无法使用的内存)。delete 两次,或删完还继续用指针(悬垂指针)→ 未定义行为。

现代答案:几乎不手写 new/delete

C++ 对这个问题的杀手锏是 RAII — 资源获取即初始化:把资源的生命周期绑到对象的作用域上,作用域保证清理。你其实早就在用 RAII 只是没叫它名字:std::vector 自动管理它的堆数组。单个对象由智能指针做同样的事:

#include <memory>

int main() {
    auto p = std::make_unique<int>(42);  // 堆内存,p 持有它
    cout << *p << endl;                  // 42
    return 0;                            // p 在这里析构 — 内存自动释放
}

现代代码的经验法则:默认栈;要堆就通过主人(vector、string、unique_ptr)— 几乎不碰裸 new/delete。

两个著名的翻车方式

这个主题的面试题

  1. "什么住在栈上,什么住在堆上?" — 局部/参数 vs 显式分配的数据(以及 vector/string 背后替你管的东西)。
  2. "栈分配为什么快?" — 只是挪一个栈指针;堆分配要记账还要找空位。
  3. "一句话说清内存泄漏?" — 再也够不着、永远不会被释放的堆内存。
  4. "什么是 RAII?" — 把资源生命周期绑到对象作用域,清理自动化;vector、string、fstream、智能指针全是这么工作的。
  5. "返回局部变量的引用/指针为什么坏?" — 本文开头那个生命周期问题,武器化版本。

去练习场练手

  1. 改生命周期演示:再加一个全局和一个局部,运行前预测全部六行输出。
  2. 写 int* broken() { int x = 7; return &x; } — 别运行(未定义行为!);写一行注释,解释桌面编译器为什么会警告它。
  3. 递归深度:斐波那契示例的 fib(10) 安全;推算 fib 的栈大约多深,以及 fib(100000) 为什么会栈溢出。

相关:传值、指针与引用、什么是 int main()、15 道初学者面试题。