C++ 内存到底分配在哪?

C++ 内存到底分配在哪?------ 栈 vs 堆 全景指南

这是一个看似简单却能把人问倒的问题。很多人会脱口而出"new 就是堆、局部变量就是栈",其实远不止这样 ------ C++ 里还有全局/静态区、thread_local 区、字面量区(.rodata)、以及对象"成员嵌套"带来的混合情况

本文按"存储期 (storage duration)"为主线梳理,这是 C++ 标准真正用的术语。


1. C++ 标准的四种存储期

C++ 规定每个对象都有一种"存储期",决定了它的生命周期,也间接决定了它的存放位置:

存储期 典型位置 什么时候分配 / 释放
自动存储期 (automatic) 进入 / 离开作用域
动态存储期 (dynamic) 堆(自由存储区) new / mallocdelete / free
静态存储期 (static) 全局/静态区 (.bss/.data) 程序启动 / 程序退出
线程存储期 (thread) TLS 区 线程创建 / 线程退出

注意:标准只规定存储期,不规定"栈"或"堆"。这些只是主流实现的惯例。编译器理论上可以把所有自动变量都放到堆上,只要遵守"离开作用域就销毁"的语义。


2. 栈上分配:自动存储期

2.1 谁走栈?

  • 函数的局部非 static 变量
  • 函数的形参
  • 函数的返回地址 / 被调用者保存寄存器(ABI 细节,你一般看不见)
  • 临时对象(包括 f() + g() 产生的中间结果)
cpp 复制代码
void foo() {
    int x = 42;               // 栈
    double arr[1024];         // 栈(8KB)
    std::string s = "hi";     // 对象本身在栈;字符可能 SSO 内联在栈,也可能在堆
    Point p{1, 2};            // 栈
}                             // 所有上面这些,这一刻一起销毁

2.2 栈上分配的特点

  • 极快 :只是 sub rsp, N 一条汇编指令。
  • LIFO:后进先出,释放顺序严格反向。
  • 大小受限 :主线程栈通常 1~8 MB,子线程更小(pthread 默认 2 MB,有的平台 64 KB)。
  • 不能跨作用域存活:函数一返回,栈上对象立即析构 ------ 返回指向它的指针/引用就是经典 UB。

2.3 容易踩的坑

cpp 复制代码
int* bad() {
    int x = 42;
    return &x;        // 💥 栈变量离开作用域,指针悬空
}

std::string_view bad2() {
    std::string s = "hi";
    return s;         // 💥 隐式转成 string_view,指向一个马上析构的栈对象
}

3. 堆上分配:动态存储期

3.1 谁走堆?

唯一的入口是这几个:

  • new / new[](会调用对象构造函数)
  • malloc / calloc / realloc(C 风格,不调构造)
  • std::allocator::allocate(STL 容器底层走它)
  • 一些操作系统 API:mmapVirtualAlloc(大块内存时)
cpp 复制代码
int*  p  = new int(42);             // 堆
auto  sp = std::make_shared<Foo>(); // 堆(对象 + 控制块常常一起在堆)
auto  up = std::make_unique<Foo>(); // 堆
void* m  = std::malloc(1024);       // 堆

3.2 隐式走堆的 STL 容器

这是最容易被忽视的一类。你没写 new,但容器内部替你写了

cpp 复制代码
std::vector<int> v;     v.push_back(1);   // 缓冲区在堆
std::string      s;     s = "xxxxxxxxxxxxxxxxxxx";  // 超出 SSO → 堆
std::unordered_map<...> m; m[k] = v;      // 桶数组、节点都在堆
std::list<T>     l;                       // 每个节点在堆
std::deque<T>    d;                       // 分块缓冲区在堆
std::function<void()> f = [big_obj]{ ... }; // 闭包太大 → 堆
std::any         a = BigStruct{};          // 对象太大 → 堆
std::shared_ptr<T> sp;                    // 控制块在堆

反之不上堆的 STL:

  • std::array<T, N>:大小编译期固定,全部内联
  • 短串 std::string:命中 SSO,内联在对象里。
  • 小闭包 std::function:命中 SBO(Small Buffer Optimization),内联在对象里。
  • std::pair / std::tuple / std::optional<T> 本身:就是普通聚合,跟着外层对象走(外层在栈它就在栈)。

3.3 堆的特点

  • malloc 几十~上百 ns,还可能触发 mmap/锁竞争。
  • 生命周期自由 :只要不 delete,就一直活着;这也意味着容易泄漏
  • 容量大:一般只受进程地址空间和系统内存限制。
  • 内存碎片:频繁分配/释放不同大小的对象会让堆变得零散。

3.4 RAII:让堆"看起来像栈"

new/delete 容易出事,现代 C++ 几乎总用智能指针 / 容器包一层:

cpp 复制代码
{
    auto p = std::make_unique<Foo>();   // 堆上构造 Foo
    ...
}                                        // unique_ptr 析构 → delete Foo

这叫 RAII:堆资源的生命周期绑定在一个栈对象上。这是 C++ 管理堆内存的主流范式。


4. 全局 / 静态区:静态存储期

4.1 谁走这里?

  • 全局变量
  • namespace 作用域变量
  • 函数内的 static 变量
  • 类的 static 成员变量
  • 字符串字面量 "hello"(放在 .rodata 只读段)
cpp 复制代码
int g = 42;                          // .data 段(有初值、非零)
int g0;                              // .bss 段(零初始化)

const char* msg = "hi";              // 指针在 .data;"hi" 在 .rodata

void foo() {
    static int counter = 0;          // 仅第一次进入 foo 时初始化
    ++counter;
}

struct S { static int x; };
int S::x = 10;                       // .data

4.2 特点

  • 程序启动时初始化,退出时销毁,整个程序生命周期有效
  • 不进栈、不进堆,占独立的段。
  • 不同翻译单元的全局变量初始化顺序未定义 ------ 著名的 "static initialization order fiasco",尽量避免互相依赖。

5. 线程存储期:TLS

cpp 复制代码
thread_local int tid_cache = -1;
  • 每个线程有一份独立副本。
  • 底层实现通常是:主线程放 .tdata/.tbss,线程创建时为其 TLS 段分配一块(通常来自堆或特殊的 TLS 管理区)。
  • 生命周期:线程开始到线程结束。

6. 成员对象:跟着"外层"走

这是初学者最容易混淆的一点 ------ 成员变量不独立分配,而是嵌入在外层对象里

cpp 复制代码
struct Point { int x, y; };     // 8 字节聚合

struct Foo {
    int n;
    Point p;                    // 内嵌 8 字节
    double arr[4];              // 内嵌 32 字节
};                              // sizeof(Foo) ≈ 48

Foo f1;                         // f1 整个在栈 → n, p, arr 全在栈
auto f2 = std::make_unique<Foo>(); // 整个在堆 → 成员全在堆
std::vector<Foo> v(10);         // vector 的缓冲在堆 → 10 个 Foo 全在堆
static Foo f3;                  // 全在 .bss/.data

换句话说:"成员在哪"只取决于"它的最外层对象在哪"

但!如果成员本身是指针或含堆分配的类型stringvector......),那成员是内联的,成员指向的数据仍然在堆

cpp 复制代码
struct Bar {
    int         n;       // 嵌在 Bar 里
    std::string s;       // string 对象本身嵌在 Bar 里
                         //   短串:字符在 s 的 inline buffer(跟 Bar 走)
                         //   长串:字符在堆上另一块
    std::vector<int> v;  // vector 对象嵌在 Bar 里,元素永远在堆
};

Bar b;                   // Bar 在栈;但 v 的元素一定在堆

7. 临时对象 / 返回值:栈 + RVO

cpp 复制代码
std::string make() { return std::string("hello"); }

std::string s = make();   // 临时对象在哪?
  • 概念上make 里的临时对象在 make 的栈帧,返回时要拷贝/移动出来。
  • 实际上 :C++17 起的 RVO / 强制 copy-elision 通常让编译器直接在调用方 s 的位置构造,一次构造,零拷贝。

对延长临时对象生命周期的经典规则:

cpp 复制代码
const std::string& r = make();   // 临时对象被绑到 const 引用 → 生命周期延长到 r
std::string_view  sv = make();   // ❌ string_view 不享受这个延长规则,立刻悬空

8. 一张综合速查表

代码形式 对象本身 "内部数据"
int x;(局部) ---
int x;(全局/static) .bss/.data ---
int* p = new int; p 在栈 *p 在堆
std::string s = "hi"; 字符:SSO 命中在栈,否则堆
std::string s = std::string(100,'x'); 字符:超出 SSO → 堆
std::vector<int> v; 元素:堆
std::array<int, 10> a; 元素:栈(内联)
auto p = std::make_unique<Foo>(); p 在栈 Foo 在堆
auto sp = std::make_shared<Foo>(); sp 在栈 Foo + 控制块:堆
thread_local int t; TLS ---
"hello"(字面量) .rodata ---
类成员变量 跟外层对象 若成员自身是容器/智能指针,再间接到堆

9. 实用判断流程

当你拿到一行代码想问"它在哪?",按这三步走:

  1. 这个对象的"宿主"是谁?
    • 局部变量 → 栈
    • 全局 / static → 静态区
    • 成员变量 → 追到最外层对象,重新从第 1 步开始
    • new / 容器内部 → 堆
  2. 这个对象里有没有"指向堆的字段"? (比如内嵌 vector、长 stringunique_ptr
    若有,那元数据在步骤 1 的位置,真正的数据在堆
  3. 它是不是字面量 / static const 若是,多半在 .rodata

举例:std::vector<std::string> v; 里第 3 个 string 的字符在哪?

  • v 是局部 → 在栈。
  • v 的元素数组是容器内部分配 → 堆上一段 std::string[n]
  • 第 3 个 string 对象本身就在那段堆里。
  • 它的字符呢?短串 → 就在那段堆内联 buffer 里;长串 → 另一块堆。

10. 小结

  • 栈 vs 堆的区别不在"用了什么语法",而在"这个对象的存储期是什么"。
  • 所有 new / malloc / STL 容器内部分配的数据结构,都上堆。
  • 局部变量、形参、临时对象、成员变量(跟随外层)------ 通常在栈。
  • 全局/static/字面量 ------ 在静态区,跟栈堆无关。
  • 混合最多的类型是 std::string / std::vector / std::function 这类"对象内联 + 数据堆分配"的结构,看待它们时要把"外壳"和"数据"分开想。

一句话记忆:

栈管"作用域能活多久",堆管"你让我活多久",静态区管"全程都活"。

相关推荐
NWU_白杨2 小时前
VoiceMockInterview项目MVP开发
java·ai
2401_832365522 小时前
Chart.js 4 中基于数据实际范围的线性渐变填充方案
jvm·数据库·python
RDCJM2 小时前
Springboot的jak安装与配置教程
java·spring boot·后端
qq_342295822 小时前
如何让 Bootstrap 图标在 Vue 3 中持续旋转动画
jvm·数据库·python
呱牛do it2 小时前
企业级门户网站设计与实现:基于SpringBoot + Vue3的全栈解决方案(Day 4)
java·vue
云烟成雨TD2 小时前
Spring AI Alibaba 1.x 系列【39】四大多智能体(Multi-agent)架构
java·人工智能·spring
Xingxing?!2 小时前
Java 后端分层架构详解
java·架构·状态模式
兩尛2 小时前
c++面试常问1
jvm·c++·面试
weixin_568996062 小时前
如何用 IndexedDB 存储从 API 获取的超大列表并实现二级索引
jvm·数据库·python