一、堆内存和栈内存到底有什么区别
先看最简单的代码:
void fun() { int a = 10; int *p = new int(20); delete p; }这里一般可以理解成:
栈: ┌──────────────┐ │ a = 10 │ ├──────────────┤ │ p │──────┐ └──────────────┘ │ ↓ 堆: ┌──────────────┐ │ int = 20 │ └──────────────┘注意一个非常重要的地方:
int *p = new int(20);不是说:
p在堆上而是:
p这个指针变量 ↓ 在栈上 p指向的int对象 ↓ 在堆上因为:
p本身是:
fun()中的局部变量而:
new int(20)才是在动态存储区申请的对象。
所以:
局部变量 ↓ 通常跟随函数栈帧存在 动态分配资源 ↓ 通常位于堆上两者主要区别可以整理成:
| 对比 | 栈 | 堆 |
|---|---|---|
| 分配方式 | 通常自动管理 | 动态申请 |
| 生命周期 | 随作用域结束自动销毁 | 由资源管理方式决定 |
| 分配速度 | 通常较快 | 通常开销更高 |
| 空间大小 | 一般较小 | 通常更大 |
| 常见对象 | 局部变量 | new/malloc的动态资源 |
| 管理方式 | 编译器/运行时维护 | 程序或分配器管理 |
例如:
void fun() { int a = 10; double b = 20.0; }可以简单理解成:
fun()栈帧: ┌─────────┐ │ a │ ├─────────┤ │ b │ └─────────┘函数:
fun();结束以后:
栈帧销毁 ↓ a和b自动失效但是:
void fun() { int *p = new int(10); }函数结束后:
p这个指针变量 ↓ 消失但是:
new出来的int ↓ 不会因为p离开作用域自动释放如果没有:
delete p;就可能产生:
内存泄漏所以:
栈 ↓ 主要体现自动生命周期 堆 ↓ 主要用于动态生命周期和动态容量不过现代 C++ 更推荐:
std::unique_ptr std::shared_ptr std::vector std::string通过 RAII 管理堆资源,而不是到处手动new/delete。
二、什么情况下会发生栈溢出
栈空间不是无限大的。
每个线程通常都会拥有自己的:
线程栈如果使用的栈空间超过限制,就可能出现:
Stack Overflow 栈溢出1. 无限递归
最典型:
void fun() { fun(); }调用过程:
fun() ↓ fun() ↓ fun() ↓ fun() ↓ ...每次函数调用都会创建新的:
栈帧其中可能保存:
局部变量 函数参数 返回地址 寄存器状态所以:
递归深度不断增加 ↓ 栈空间不断消耗 ↓ 最终超过栈大小 ↓ Stack Overflow即使不是无限递归:
void dfs(int n) { if (n == 0) return; dfs(n - 1); }如果:
dfs(10000000);递归层数过深,同样可能栈溢出。
2. 定义特别大的局部数组
例如:
void fun() { int arr[10000000]; }如果:
一个int = 4字节那么大概:
10000000 × 4 ≈ 40MB这个数组作为局部自动对象,可能尝试占用很大的栈空间。
而线程栈可能只有几 MB。
于是:
局部数组太大 ↓ 直接栈溢出这种场景更适合使用:
std::vector<int> arr(10000000);为什么?
因为:
vector对象本身 ↓ 很小 真正10000000个int ↓ 动态存储区后面会详细讲。
3. 局部对象本身非常大
例如:
struct BigData { char data[10 * 1024 * 1024]; }; void fun() { BigData data; }这里:
BigData对象本身 ≈ 10MB如果它作为局部变量存在,就可能直接占用很大栈空间。
所以要区分:
std::vector<int> v(1000000);和:
int arr[1000000];虽然都能保存很多整数,但内存布局完全不同。
前者:
vector控制对象 ↓ 栈 大量元素 ↓ 堆后者:
整个数组 ↓ 栈这也是 STL 容器非常重要的特点之一。
三、std::string到底占栈还是堆
这是一个特别典型的面试问题。
例如:
void fun() { std::string str = "hello world"; }有人会说:
string在堆上也有人说:
string在栈上其实都不够准确。
更准确的说法是:
str这个std::string对象本身是局部变量,所以对象本身通常位于当前函数的栈帧中;但它管理的字符数据是否位于堆上,要看字符串长度和具体标准库实现。
可以先忽略优化,把 string 简化理解成:
class String { private: char *data_; size_t size_; size_t capacity_; };那么:
std::string str = "hello world";大致可以理解成:
栈: str ┌────────────────┐ │ data_ │───────┐ │ size_ │ │ │ capacity_ │ │ └────────────────┘ │ ↓ 堆: ┌───────────────┐ │ hello world\0 │ └───────────────┘所以:
string对象本身 ↓ 栈 字符串动态缓冲区 ↓ 通常在堆但是这里还有一个重要优化:
SSO Small String Optimization 小字符串优化很多标准库实现对于非常短的字符串,不会马上申请堆内存。
而是把字符:
直接存进string对象内部例如概念上可能类似:
class String { private: size_t size_; union { char small[16]; char *heapPtr; }; };当字符串比较短:
std::string str = "abc";可能变成:
栈中的string对象: ┌───────────────────────┐ │ size = 3 │ │ "abc\0" │ └───────────────────────┘也就是:
没有额外堆分配如果字符串非常长:
std::string str = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz";超过 SSO 容量以后:
string对象 ↓ 栈 真正长字符串 ↓ 堆所以面试中问:
std::string 是在栈还是堆?
最好回答:
如果
std::string是函数局部变量,那么 string 对象本身通常在栈上。它管理的字符数据是否需要堆分配取决于实现和字符串长度,较长字符串通常使用动态内存,而很多标准库对短字符串会使用 SSO,直接把字符存放在 string 对象内部。
不要简单回答:
string一定在堆上四、std::string发生复制拷贝以后,内存怎么变化
假设:
void fun() { std::string a = "abcdefghijklmnopqrstuvwxyz"; std::string b = a; }这里:
a b都是函数内部局部对象。
所以:
a对象 ↓ 栈 b对象 ↓ 栈如果字符串足够长,需要动态内存,那么可以简化理解为:
栈: a ┌──────────┐ │ ptr ───────────────┐ └──────────┘ │ ↓ 堆: ┌──────────────────┐ │ abcdefghijkl... │ └──────────────────┘ b ┌──────────┐ │ ptr ───────────────┐ └──────────┘ │ ↓ 堆: ┌──────────────────┐ │ abcdefghijkl... │ └──────────────────┘通常:
a和b拥有各自独立的字符串内容也就是说:
std::string b = a;是:
拷贝语义修改:
b[0] = 'X';不会把:
a[0]一起改掉。
所以对于长字符串,拷贝一般意味着:
新的string对象 + 新的字符缓冲区 + 复制字符数据因此大字符串的频繁拷贝可能有性能开销。
如果改成:
std::string b = std::move(a);就是:
移动对于使用动态缓冲区的字符串,实现通常可以把:
a原来管理的堆资源转移给:
b概念上:
移动前: a ─────→ "abcdefghijklmnopqrstuvwxyz" b移动后:
a ↓ 合法但状态未指定 b ─────→ 原来的字符串缓冲区通常不需要把整个字符串重新复制一遍。
这也是移动语义的意义。
不过如果字符串使用:
SSO那么小字符串数据本来就在对象内部,实际移动过程可能仍然需要复制那几个小字符。
所以性能上:
move一定是简单交换一个指针也不能绝对这么说。
五、vector对象和push_back进去的对象到底存在哪
这也是非常重要的一部分。
假设:
void fun() { std::vector<int> a; a.push_back(10); a.push_back(20); a.push_back(30); }首先:
a是:
fun()中的局部变量所以:
vector对象本身 ↓ 通常位于栈上vector 本身可以简化理解成:
class Vector { private: int *start_; int *finish_; int *end_; };所以:
栈: a ┌──────────────────┐ │ start_ │──────┐ │ finish_ │ │ │ end_ │ │ └──────────────────┘ │ ↓ 堆: ┌────┬────┬────┬────┐ │ 10 │ 20 │ 30 │ │ └────┴────┴────┴────┘也就是说:
vector对象本身 ↓ 栈 vector内部动态数组 ↓ 堆这就是:
std::vector<int> a;最需要理解的地方。
再看一个对象类型:
class Student { public: int id; double score; };然后:
void fun() { std::vector<Student> students; Student s; s.id = 1; s.score = 90; students.push_back(s); }此时:
s ↓ 局部对象 ↓ 栈而:
students ↓ vector对象 ↓ 栈但是:
students.push_back(s);会把一个Student元素构造到:
vector管理的动态数组里面。
也就是:
栈: ┌──────────────────────┐ │ Student s │ │ id = 1 │ │ score = 90 │ └──────────────────────┘ ┌──────────────────────┐ │ vector students │───────┐ │ start │ │ │ finish │ │ │ end │ │ └──────────────────────┘ │ ↓ 堆: ┌──────────────────────────┐ │ Student │ │ id = 1 │ │ score = 90 │ └──────────────────────────┘所以会有:
两个Student对象一个:
局部变量s ↓ 栈另一个:
vector中的元素 ↓ vector动态存储区 ↓ 通常在堆执行:
students.push_back(s);因为:
s是左值,所以一般会调用:
Student拷贝构造将它复制到 vector 的动态空间中。
如果:
students.push_back(std::move(s));则可能调用:
Student移动构造如果直接:
students.emplace_back();则可以:
直接在vector内部存储区域构造对象再来看:
void fun() { std::vector<Student> a; a.push_back(Student{1, 90}); }其中临时:
Student{1, 90}会被移动或构造进 vector 的元素存储区。
最终长期留下的是:
vector内部的Student元素 ↓ 动态存储区而不是说:
vector里只存了一个指向栈对象的指针vector<Student>存的是:
Student对象本身这一点很重要。
但是如果定义:
std::vector<Student *> students;情况就不同了。
vector 的动态数组里面保存的是:
Student*也就是指针。
例如:
Student s; students.push_back(&s);布局:
栈: Student s ↑ │ │ vector对象 │ ↓ 堆中的vector数组: ┌───────────────┐ │ Student* ─────┘ └───────────────┘这里:
vector内部保存的是指针真正的 Student:
仍然在栈上如果这样:
students.push_back(new Student);那么:
vector对象 ↓ 栈 vector保存指针的数组 ↓ 堆 new Student ↓ 也是堆结构:
栈: vector │ ↓ 堆区域1: ┌──────────────┐ │ Student* │──────┐ └──────────────┘ │ ↓ 堆区域2: ┌──────────────┐ │ Student对象 │ └──────────────┘因此一定要看:
vector<T>中的:
T到底是什么如果:
vector<Student>存的是:
Student对象本身如果:
vector<Student *>存的是:
Student指针如果:
vector<std::unique_ptr<Student>>存的是:
unique_ptr对象这些unique_ptr位于 vector 的动态存储区,而它们所管理的Student通常又是另一块动态分配内存。
最后把最容易混淆的几个情况放到一起:
void fun() { int a = 10; int *p = new int(20); std::string str = "hello"; std::vector<int> nums; nums.push_back(100); }可以概念性地画成:
fun()栈帧 ┌────────────────────────────┐ │ int a = 10 │ ├────────────────────────────┤ │ int* p ─────────────────────────────┐ ├────────────────────────────┤ │ │ std::string str │ │ │ 可能直接SSO存hello │ │ ├────────────────────────────┤ │ │ std::vector<int> nums │───┐ │ │ start / finish / end │ │ │ └────────────────────────────┘ │ │ │ │ ↓ ↓ 堆内存 ┌───────────┐ │ 100 │ └───────────┘ ┌───────────┐ │ int = 20 │ └───────────┘如果str很长:
栈中的string对象 │ ↓ 堆中的字符缓冲区面试如果问:
堆和栈有什么区别?
可以回答:
栈通常用于保存函数调用栈帧和局部自动对象,由系统随着作用域自动管理,分配释放速度较快但空间有限;堆主要用于动态分配,容量通常更大,生命周期可以跨越函数作用域,但需要通过
delete/free或 RAII 对象正确管理,分配释放的成本通常也高于栈。
如果问:
什么情况下会栈溢出?
可以回答:
最常见的是递归层数过深或者在函数中定义特别大的局部数组、局部对象。因为每个线程的栈空间有限,函数栈帧不断累积或者单个栈帧占用过大,都可能导致 Stack Overflow。
如果问:
std::string在栈还是堆?
可以回答:
如果 string 是局部变量,那么 string 对象本身通常在栈上。对于较长字符串,它管理的字符缓冲区通常动态分配;但很多标准库存在 SSO,小字符串可以直接存放在 string 对象内部,因此不一定发生堆分配。
如果问:
string复制一份以后呢?
可以回答:
两个局部 string 对象本身都在各自的栈位置。发生普通拷贝时,从语义上看两个 string 拥有各自独立的内容;如果字符串需要动态存储,通常会分别管理自己的动态缓冲区。移动构造则可能直接转移原有动态缓冲区的所有权,减少大数据复制。
如果问:
void fun(){ std::vector<int> a; a.push_back(10); }中内存怎么分布?
可以回答:
a是局部 vector 对象,因此 vector 对象本身通常位于fun的栈帧中,内部主要维护指向元素存储区的指针或类似状态。push_back(10)后,真正的 int 元素通常存放在 vector 动态申请的连续内存中,也就是通常所说的堆内存。当 vector 扩容时,会重新申请更大的动态存储区,并把原有元素移动或拷贝过去。
最核心的理解可以总结为:
局部STL对象 不等于 所有数据都在栈上例如:
std::string std::vector本身只是:
一个管理对象真正的大量数据通常由它们内部:
动态管理所以判断一个对象到底占用哪里的内存时,应该分成两层:
第一层 ↓ 对象本身放在哪里? 第二层 ↓ 对象内部管理的资源放在哪里?只要把这两层分开,string、vector、智能指针以及各种动态容器的内存布局就不会容易混淆。
0voice · GitHub