vector 底层是怎么扩容的?

vector 底层是怎么扩容的?

  1. vector 底层内存模型是什么
  2. push_back 时什么时候会触发扩容
  3. 扩容时一般按什么策略增长,为什么不能每次只加 1
  4. 扩容时旧元素怎么搬到新空间
    • 什么情况下拷贝
    • 什么情况下移动
  5. 扩容以后,原来的迭代器 / 指针 / 引用会不会失效,为什么
  6. resizereserve 的区别
  7. 为什么 vector 的尾插快,头插慢
  8. 从源码实现思路上讲,扩容的大致流程是什么

std::vector 本质上是一段连续内存上的动态数组。它底层一般就是三根指针,或者等价的三个位置概念:一根指向起始位置,一根指向当前最后一个元素的下一个位置,还有一根指向整块已分配内存的末尾。所以它能做到随机访问是 O(1),因为下标访问本质上就是首地址加偏移。

push_back 的时候,如果当前 size < capacity,那就直接在尾部那块还没使用的空间上构造一个新元素,不需要扩容;如果当前 size == capacity,说明预留空间已经满了,这时候就会触发扩容。

扩容一般不会每次只加 1,而是按一个倍增策略去长,常见是 1.5 倍或者 2 倍,具体增长因标准库实现而异。原因很简单,如果每次只加 1,那你插入 n 个元素时,几乎每次都要重新申请空间、搬迁旧元素、释放旧空间,总代价会退化得很差。按倍增做的话,虽然某一次扩容很重,但摊还下来,push_back 的均摊复杂度仍然是 O(1)。

你刚才追问的那个点很关键。像 vector<string> 扩容时,不是直接逐个 memcpy 过去,而是要在新空间里调用构造函数把旧元素搬过去。更准确地说,是对每个元素做“逐个迁移构造”——优先走移动构造,必要时走拷贝构造。因为 string 这种类型不是简单的 POD 裸数据,它内部自己还管理堆内存、指针、长度这些资源。如果直接 memcpy,只是把对象的字节生硬拷过去,会导致两个对象内部指向同一块资源,后面析构就会出大问题。所以对于这种非平凡类型,必须按对象语义来搬。

至于什么时候拷贝,什么时候移动,一般是这样理解:如果元素类型支持移动构造,并且这个移动构造是编译器和库认为“可以安全采用”的,那扩容时会优先移动,因为移动通常比拷贝便宜;如果没有移动构造,或者出于异常安全等原因不能放心移动,那就会退回拷贝构造。比如早期实现里,经常会倾向于“如果移动构造不是 noexcept,那为了维持强异常安全,可能宁可拷贝也不移动”。所以面试里说一句“通常优先移动,移动不合适时退回拷贝”会比较稳。

扩容以后,原来的迭代器、指针、引用基本都会失效。原因就在于 vector 的元素必须放在连续内存里,一旦扩容,通常会重新申请一块更大的新内存,然后把旧元素搬过去,原来那块地址就不用了。既然底层地址变了,那你之前拿到的元素地址、迭代器、引用,指向的就是老空间,自然失效了。这个是 vector 很典型的使用坑。

reserveresize 的区别也经常被问。reserve预留容量,它只管 capacity,告诉 vector 你至少给我准备这么多空间,但逻辑上的元素个数 size 不变;而 resize改元素个数,会真正把 size 改掉。比如把 resize(10),如果原来只有 5 个元素,那它会补出 5 个新元素;如果改小,还会析构多出来的元素。所以一个是管容量,一个是管实际元素数量。

为什么 vector 尾插快、头插慢,本质也是因为连续内存。尾插如果不扩容,直接在末尾构造就行了,代价很小;但头插或者中间插入时,为了保持连续性,后面的元素都得整体往后挪位置,这个代价就是 O(n)。如果再碰上扩容,还得先申请新空间再整体搬迁,成本更高。

如果从源码实现思路上讲,扩容的大致流程我会这么概括:

先算出新的容量,通常比旧容量更大;然后申请一块新的原始未初始化内存;接着把旧空间里的元素按顺序在新空间里逐个构造出来,这一步通常用 allocator 配合 uninitialized_moveuninitialized_copy 这类思路去做;如果中间出异常,要把新空间里已经构造成功的对象逐个析构掉,再释放新内存,保证不泄漏;等全部迁移成功后,再把旧空间原来的对象逐个析构,释放旧内存;最后更新内部的起始指针、结束指针和容量边界指针,让 vector 指向新空间。

所以总结一下,vector<string> 扩容绝不是简单 memcpy,而是按对象语义逐个构造搬迁。只有像特别简单、可平凡复制的类型,在某些实现和优化条件下,底层才可能做更激进的字节级优化;但从标准语义和通用回答上讲,你应该答“调用构造函数迁移,而不是无脑 memcpy”,这个更准确。

如果面试官继续追问,我还可以接着帮你答:“为什么有的类型移动了反而不一定更快”、“vector 扩容的异常安全怎么保证”、“emplace_back 和 push_back 在扩容场景下有什么区别”。