☰
【C++面试】堆内存与栈内存:string、vector和对象到底存在哪里
2026/10/7 8:13:48 网站建设 项目流程

一、堆内存和栈内存到底有什么区别

先看最简单的代码:

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询