一、物理内存:绝对连续
vector 是 C++ 唯一保证元素在堆内存中绝对连续排列的容器(与 std::array 一样,但 array 在栈上)。
vector<int> v = {10, 20, 30, 40};
堆内存 (Heap):
+──────+──────+──────+──────+──────────────────+
| 10 | 20 | 30 | 40 | 预留空间 |
+──────+──────+──────+──────+──────────────────+
^ ^ ^ ^ ^
| | | | |
v[0] v[1] v[2] v[3] v.capacity() - 1
栈上 (Stack) — vector 对象本身 (24 字节,64位):
+────────────────────────────────────────────────────+
| _M_start (ptr) | _M_finish (ptr) | _M_end_of_storage (ptr) |
| 指向堆首 | 指向堆尾(size) | 指向容量尾(capacity) |
+────────────────────────────────────────────────────+
关键结论:
v.data()返回的指针可以做指针运算:*(v.data() + 2)就是v[2]。- 因为连续,CPU 预取器(Prefetcher)在遍历时会自动把相邻元素加载到缓存行,所以
vector的遍历速度是list的 10-50 倍。
二、扩容因子:为什么是 1.5 倍或 2 倍?
当 size() == capacity() 时,push_back 触发扩容:
v.push_back(50); // capacity() 不够了 → 分配新内存 → 搬移所有元素 → 释放旧内存
扩容流程:
旧内存: [10][20][30][40] 容量=4
↓ push_back(50)
1. 分配新内存: [ ][ ][ ][ ][ ][ ][ ][ ] 容量=8 (2倍)
2. 搬移: [10][20][30][40][50][ ][ ][ ]
3. 释放旧内存
为什么是 2 倍(GCC)或 1.5 倍(MSVC)?
| 扩容因子 | 均摊复杂度 | 内存浪费 | 内存复用可能性 |
|---|---|---|---|
| 2x (GCC) | $O(1)$ 均摊 | 最多浪费 50% | 低:新内存总是两倍大,旧内存碎片很难被复用 |
| 1.5x (MSVC) | $O(1)$ 均摊 | 最多浪费 33% | 高:1.5 倍增长满足"之前释放的总和 > 下次分配大小",有机会复用旧内存 |
面试经典题:“为什么 vector 扩容不固定加 N 个元素?”
- 如果每次固定 +N:连续 M 次插入的时间复杂度是 $O(M \times N)$ → 每次都是 $O(N)$。
- 如果是倍数增长:M 次插入的均摊复杂度是 $O(1)$(每次扩容的元素数量是指数增长的,拷贝次数是 1+2+4+…+M/2 ≈ M)。
三、size() vs capacity(),reserve() vs resize()
vector<int> v;
v.reserve(100); // 只扩大 capacity,不构造元素,不改变 size
v.resize(100); // 扩大 capacity 并构造 100 个默认元素,size=100
v.push_back(1); // reserve后:size=101,不触发扩容
// resize后:size=101,触发扩容(因为 size==capacity==100)
硬件代价对比:
reserve(10000): 一次 malloc(40KB),不构造元素 → 约 0.1μs
resize(10000): 一次 malloc + 10000次默认构造 → 约 10μs (100倍!)
默认 push_back 10000次: 多次 malloc + 多次拷贝搬移 → 约 50μs (最慢)
最佳实践:如果你预先知道元素数量,永远先 reserve。这是零成本优化。
四、迭代器失效(Iterator Invalidation)—— 面试必考
| 操作 | 迭代器失效情况 |
|---|---|
push_back / emplace_back |
如果触发扩容 → 所有迭代器失效;如果不触发 → end() 迭代器失效 |
insert / erase |
插入/删除位置之后的所有迭代器失效 |
pop_back |
被删除元素和 end() 的迭代器失效 |
reserve |
如果新容量 > 旧容量 → 所有迭代器失效 |
resize |
如果新容量 > 旧容量 → 所有迭代器失效 |
底层原因:扩容时 _M_start 指针变了,指向一块新的堆内存——所有基于旧地址的迭代器都变成了悬垂指针。
// ❌ 错误代码
vector<int> v = {1, 2, 3, 4};
int* ptr = &v[2]; // 指向第三个元素
v.push_back(5); // 可能触发扩容 → ptr 变成悬垂指针!
*ptr = 42; // 💥 Use-After-Free!
// ✅ 正确做法
v.reserve(8); // 提前预留空间
int* ptr = &v[2];
v.push_back(5); // 不触发扩容,ptr 依然有效