一、堆内存和栈内存到底有什么区别先看最简单的代码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::vectorint arr(10000000);为什么因为vector对象本身 ↓ 很小 真正10000000个int ↓ 动态存储区后面会详细讲。3. 局部对象本身非常大例如struct BigData { char data[10 * 1024 * 1024]; }; void fun() { BigData data; }这里BigData对象本身 ≈ 10MB如果它作为局部变量存在就可能直接占用很大栈空间。所以要区分std::vectorint 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::vectorint 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::vectorint a;最需要理解的地方。再看一个对象类型class Student { public: int id; double score; };然后void fun() { std::vectorStudent 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::vectorStudent a; a.push_back(Student{1, 90}); }其中临时Student{1, 90}会被移动或构造进 vector 的元素存储区。最终长期留下的是vector内部的Student元素 ↓ 动态存储区而不是说vector里只存了一个指向栈对象的指针vectorStudent存的是Student对象本身这一点很重要。但是如果定义std::vectorStudent * 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对象 │ └──────────────┘因此一定要看vectorT中的T到底是什么如果vectorStudent存的是Student对象本身如果vectorStudent *存的是Student指针如果vectorstd::unique_ptrStudent存的是unique_ptr对象这些unique_ptr位于 vector 的动态存储区而它们所管理的Student通常又是另一块动态分配内存。最后把最容易混淆的几个情况放到一起void fun() { int a 10; int *p new int(20); std::string str hello; std::vectorint nums; nums.push_back(100); }可以概念性地画成fun()栈帧 ┌────────────────────────────┐ │ int a 10 │ ├────────────────────────────┤ │ int* p ─────────────────────────────┐ ├────────────────────────────┤ │ │ std::string str │ │ │ 可能直接SSO存hello │ │ ├────────────────────────────┤ │ │ std::vectorint 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::vectorint a; a.push_back(10); }中内存怎么分布可以回答a是局部 vector 对象因此 vector 对象本身通常位于fun的栈帧中内部主要维护指向元素存储区的指针或类似状态。push_back(10)后真正的 int 元素通常存放在 vector 动态申请的连续内存中也就是通常所说的堆内存。当 vector 扩容时会重新申请更大的动态存储区并把原有元素移动或拷贝过去。最核心的理解可以总结为局部STL对象 不等于 所有数据都在栈上例如std::string std::vector本身只是一个管理对象真正的大量数据通常由它们内部动态管理所以判断一个对象到底占用哪里的内存时应该分成两层第一层 ↓ 对象本身放在哪里 第二层 ↓ 对象内部管理的资源放在哪里只要把这两层分开string、vector、智能指针以及各种动态容器的内存布局就不会容易混淆。0voice · GitHub
阅读完成 · 觉得有帮助?