
理解 vector 的三大指针内存模型、扩容真相、移动语义优化点、迭代器失效全规则并亲手写一个可运行的迷你 vector。引入vector 为什么是 C 最常用的容器如果你问一个 C 程序员最常用的容器是哪个十个里有九个会回答 std::vector。为什么连续内存vector 的元素在内存里紧密排列就像宿舍楼里连号的房间。这使得它访问第 i 个元素的耗时是 O(1)常数时间不随元素数量增长还能直接获得极高的缓存命中率CPU 读内存时会把相邻数据一起搬进高速缓存。动态扩容C 语言里数组大小必须在编译期定死vector 则像伸缩自如的行李箱装满了会自动变大。STL 算法友好因为内存连续std::sort、std::binary_search 等需要随机访问的算法可以放心使用。但是会用和懂它是两码事。很多资深面试题都围绕 vector 展开❓ 为什么 vector 扩容时建议 reserve 而不是坐等自动扩容 ❓ 为什么有的编译器扩容 ×1.5有的 ×2哪一个更好 ❓ push_back 抛异常后vector 里的数据还安全吗 ❓ 为什么 vectorbool 是声名狼藉的特化 ❓ 为什么往 vector 里插一个元素之前的迭代器可能全部失效这篇文章从内存布局出发一层层拆开 vector 的底层实现最后手写一个迷你 vector 并实测性能。读完你会发现STL 不是黑魔法它是一套精心设计的工程权衡。1. 内存布局与三大指针直觉建立1.1 先看本质vector 就是一个三指针结构体标准库并没有规定 vector 必须怎么存但几乎所有主流实现libstdc / libc / MSVC STL都采用同一个模型vector 内部维护三个指针┌─────────────────────────────────────────┐ begin (start) │ T0 │ T1 │ T2 │ T3 │ 未构造区 │ └──────┴──────┴──────┴──────┴─────────────┘ ▲ ▲ ▲ begin end capacity (首元素) (最后一个 (分配到的 元素之后) 内存终点)指针通常的成员名libstdc含义通俗类比beginstart_M_start指向第一个元素房间走廊的 0 号门endfinish_M_finish指向最后一个元素之后的位置最后一间已入住房间的门牌 1capacityend_of_storage_M_end_of_storage指向分配到的内存块的末尾整栋宿舍楼的围墙关键直觉size() end - begin已入住房间数capacity() end_of_storage - begin整栋楼能住的房间数size() capacity()永远成立end 指向的位置没有元素只是哨兵用来判断 empty()begin() end()1.2 sizeof(vector) 有多大既然 vector 只存三个指针64 位平台每个 8 字节那么#include iostream #include vector #include string int main() { // 64 位平台3 个指针 × 8 字节 24 字节 std::cout sizeof(std::vectorint) sizeof(std::vectorint) \n; std::cout sizeof(std::vectordouble) sizeof(std::vectordouble) \n; std::cout sizeof(std::vectorstd::string) sizeof(std::vectorstd::string) \n; return 0; }输出64 位⚠️ 惊讶吗不管元素是什么类型sizeof(vector) 都是 24 字节。因为vector 本体只存指针元素数据在堆上。这也解释了为什么往函数传 vector要传引用——传值会复制三个指针没问题但会深拷贝所有元素复制整栋楼代价巨大。1.3 内存增长示意从空 vector 到 5 个元素#include vector #include iostream int main() { std::vectorint v; // beginendcapnullptrsize0cap0 std::cout 初始: size v.size() cap v.capacity() \n; for (int i 0; i 5; i) { v.push_back(i); // 注意cap 变化是跳跃式的0→1→2→4→8...size 是连续1 std::cout push_back( i ): size v.size() cap v.capacity() \n; } return 0; }典型输出libstdc×2 增长初始: size0 cap0 push_back(0): size1 cap1 push_back(1): size2 cap2 push_back(2): size3 cap4 push_back(3): size4 cap4 push_back(4): size5 cap8看到规律了吗每次容量不够时容量直接翻倍而不是恰好 1。这就是第 2 节要深挖的扩容策略。2. 扩容策略深度剖析增长因子 1.5 vs 2 之争2.1 为什么不能差一个扩一个假设 vector 容量恰好等于 size再来一个元素就要扩容。如果每次只多分配 1 个插入第 n 个元素要复制/移动前面 n-1 个元素总复制次数 ≈ 123...N O(N²)插入 N 个元素的总开销是平方级。如果每次翻倍或 ×1.5总复制次数 ≈ N N/2 N/4 ... ≈ 2N是O(N) 摊还amortized的。摊还的含义是虽然某一次 push_back 很贵比如从 4 扩到 8 要搬 4 个元素但平均到每一次 push_back 上代价是常数。结论几何增长geometric growth让 push_back 的均摊复杂度保持 O(1)这是 vector 能作为动态数组的根基。2.2 扩容到底做了什么伪代码视角// 伪代码push_back 的底层扩容逻辑不完整仅示意 template typename T void vectorT::push_back(const T value) { if (size() capacity()) { // 满了需要扩容 size_t new_cap capacity() * growth_factor; // 新容量 T* new_buf allocate(new_cap); // 1. 分配一块更大的新内存 for (size_t i 0; i size(); i) construct(new_buf i, std::move_if_noexcept(old_buf[i])); // 2. 搬元素 destroy_all_old(); // 3. 析构旧元素 deallocate(old_buf); // 4. 释放旧内存 old_buf new_buf; // 5. 更新三大指针 } construct(end, value); // 6. 在 end 位置构造新元素 end; // 7. end 后移 }⚠️ 第 2 步有个大坑扩容 分配新内存 搬迁元素 释放旧内存绝不是在原地变大。C 的 realloc 可能原地扩展但 C 的 vector 面对的是任意自定义类型可能持有内部指针、资源无法安全地逐字节搬家必须逐个构造/移动。2.3 增长因子1.5 还是 2——内存分配器的视角增长因子代表实现优点缺点×2libstdcGCC扩容次数少、搬迁总次数少内存峰值高容易浪费近一半内存×1.5libcClang/LLVM、MSVC微软内存峰值更低内存分配器更容易复用之前释放的旧块扩容次数略多但仍满足 O(1) 均摊为什么 ×1.5 能复用旧内存块这是经典的分配器友好论证若增长因子 φ 满足φ² 2约 1.414 φ 2则相邻两次分配的块大小满足上一次的旧块能塞进新块空出来的空间里malloc 的 free-list空闲链表即分配器记住的已释放内存块清单有机会把旧块和新块合并复用从而避免向操作系统要新内存。×2 时新块 ≥ 旧块 已用部分的总和旧块永远塞不进去只能白白浪费。工程结论没有绝对优劣都是扩容次数与内存峰值之间的权衡。面试时说出×1.5 利于内存复用、×2 减少搬迁次数就已经及格。2.4 强异常安全保证strong exception guaranteepush_back 的承诺这是 vector 最容易被忽视却极其重要的设计如果 push_back 过程中抛出异常vector 保持原状size、内容都不变就像什么都没发生过。实现手段是 std::move_if_noexcept元素的移动构造不抛异常noexcept扩容时放心移动搬东西很快元素的移动构造可能抛异常如 std::string 的某些历史实现、自定义类型没标 noexcept扩容时退化为拷贝复制一份旧的不动因为拷贝失败时旧数据安然无恙可以回滚。#include vector #include stdexcept #include iostream struct Explosive { Explosive() default; Explosive(const Explosive) { /* 拷贝不抛异常随便复制 */ } // ⚠️ 故意不标 noexcept移动可能爆炸 Explosive(Explosive) { throw std::runtime_error(move boom!); } }; int main() { std::vectorExplosive v; v.reserve(1); // 先占 1 个位置 v.emplace_back(); // 放第 1 个不扩容正常 try { v.push_back(Explosive{}); // 第 2 个触发扩容 → 移动构造抛异常 } catch (const std::exception e) { std::cout 捕获异常: e.what() \n; } // 强异常安全异常后 vector 仍是 [1 个元素]没有被破坏 std::cout 异常后 size v.size() capacity v.capacity() \n; return 0; }预期输出启示自定义类型如果移动构造不抛异常记得标 noexcept——否则 vector 扩容时只能用更慢的拷贝性能白白损失。3. emplace_back 与移动语义右值插入优化、完美转发3.1 push_back 的两副面孔C11 之前push_back 只有一副面孔按值拷贝。C11 引入了右值引用右值即将销毁的临时对象如 std::string(abc) 这种表达式结果于是有了移动语义std::vectorstd::string v; std::string s hello; v.push_back(s); // 左值具名对象→ 拷贝s 仍然可用 v.push_back(std::move(s)); // 右值被 move 包装→ 移动s 的资源被偷走 v.push_back(world); // 临时对象 → 自动匹配移动重载移动move的通俗类比搬家时不是把家具一件件搬走而是直接把房本过户。对 std::string 来说移动就是把内部指针指过去把源对象的指针置空O(1) 搞定不用复制整个字符数组。⚠️ 被移动过的对象处于合法但未指定状态valid but unspecified可以析构、可以重新赋值但不要假设它还有原来的内容。std::move(s) 之后别再用 s 的值。3.2 emplace_back连临时对象都省了emplace_back 接受构造参数在 vector 的内存里直接构造元素省掉一次临时对象 → 移动到容器的中间步骤#include vector #include string #include iostream struct Person { std::string name; int age; Person(std::string n, int a) : name(std::move(n)), age(a) { std::cout 构造 Person( name , age )\n; } Person(const Person) { std::cout 拷贝构造\n; } Person(Person) noexcept { std::cout 移动构造\n; } }; int main() { std::vectorPerson v; v.reserve(3); // 预留 3 个位置避免扩容干扰输出 std::cout --- push_back 临时对象 ---\n; v.push_back(Person(alice, 20)); // 先构造临时对象再移动进容器 std::cout --- emplace_back 直接构造 ---\n; v.emplace_back(bob, 25); // 直接在容器内存里构造零临时对象 return 0; }输出对比emplace_back 少了一次移动构造。对于 std::string、Person 这种有资源要管理的类型这就是实打实的性能收益对于 int 这类平凡类型两者没有区别编译器会优化掉。3.3 完美转发perfect forwardingemplace_back 的底层魔法emplace_back 的实现长这样简化template typename... Args void vectorT::emplace_back(Args... args) { if (size() capacity()) grow(); // 扩容 // 转发如果调用处传左值args 就是左值引用传右值就是右值引用 ::new (static_castvoid*(end)) T(std::forwardArgs(args)...); end; }Args... 是转发引用forwarding reference不是普通的右值引用当调用方传左值时Args 被推导为左值引用类型参数绑定到左值传右值时绑定到右值。std::forwardArgs(args)... 的作用是原样转发实参的左右值属性保证传入左值就调用拷贝传入右值就调用移动不丢失信息。这就是完美转发。⚠️ 一个经典陷阱emplace_back 的参数是构造参数不是元素值所以不要写 v.emplace_back(std::move(someStr)) 当传参是字符串本身时这样反而多做一层 move。正确用法是传构造所需的参数。3.4 什么时候 push_back 反而更好已有现成的对象需要放进容器push_back(obj) 可读性最好想显式表达我要拷贝/移动这个对象emplace_back 有时会造成歧义例如构造一个 std::vectorint 成员时{1,2,3} 会被解析成多个参数而不是一个 initializer_list此时用 push_back 更稳妥。4. 迭代器失效全规则一张表讲透迭代器失效iterator invalidation迭代器本质上是指向容器某个位置的指针/句柄。当 vector 的内部存储发生变化内存搬走、元素挪位、元素被删旧的迭代器就指错地方了解引用它是未定义行为UBUndefined Behavior编译器不保证任何行为。4.1 失效规则速查表vector 专属操作begin 迭代器首个元素其他迭代器end 迭代器引用/指针说明push_back 未触发扩容不失效不失效失效end 位置变了不失效但 end 迭代器要重新获取push_back / insert 触发扩容全部失效全部失效全部失效全部失效内存整体搬走一切指针作废insert 在中间未扩容不失效插入点及之后全失效失效插入点及之后全失效元素集体后移erase 在中间不失效删除点及之后全失效失效删除点及之后全失效元素集体前移pop_back不失效不失效失效指向被删元素的引用失效最后一个元素被销毁clear全部失效全部失效全部失效全部失效元素全部销毁reserve 增大容量全部失效全部失效全部失效全部失效相当于扩容resize 增大同 push_back 规则同 push_back 规则失效同 push_back视是否扩容resize 缩小不失效被删元素之后的失效失效同上—swap全部失效但指向新容器同上同上同上迭代器跟随元素去新容器4.2 实战演示erase 的正确姿势最经典的 bug 是在循环里删元素#include vector #include iostream int main() { std::vectorint v {1, 2, 3, 4, 5, 6}; // ❌ 错误写法erase 后 it 已经失效却还在用 it // for (auto it v.begin(); it ! v.end(); it) { // if (*it % 2 0) v.erase(it); // UBerase 之后 it 失效 // } // ✅ 正确写法用 erase 的返回值拿到下一个有效迭代器 for (auto it v.begin(); it ! v.end(); ) { if (*it % 2 0) it v.erase(it); // erase 返回被删元素之后的位置 else it; } for (int x : v) std::cout x ; std::cout \n; // 输出: 1 3 5 return 0; }⚠️ 删除中间元素是O(n)操作后面的元素要整体前移这也是vector 适合随机访问、不适合中间插入删除的原因——中间插入/删除应改用 std::list 或 std::deque。4.3 引用失效的隐蔽场景把引用存起来#include vector #include iostream int main() { std::vectorint v {1, 2, 3}; int ref v[0]; // 引用指向内部元素 v.push_back(4); // 没扩容ref 依然有效运气好 v.push_back(5); v.push_back(6); // ⚠️ 此刻扩容元素搬走ref 悬空 // std::cout ref; // UB禁止解引用悬空引用 std::cout size v.size() cap v.capacity() \n; return 0; }工程建议不要长期保存 vector 的迭代器/引用每次可能修改容器的操作之后重新获取。需要稳定地址的元素请用 std::deque 或 std::list。5. vector 位压缩特化STL 最大的坑5.1 为什么会有这个特化标准库对 vectorbool 做了特化每个 bool 只占1 个 bit而不是 1 个字节节省 8 倍内存。初衷是好的但副作用是它不再是一个真正的 vector特性std::vectorintstd::vectorbool内存占用/元素4 字节1 bit元素类型int伪引用 std::_Bit_reference代理对象v[0]得到 int*可用❌ 编译错误拿不到 bool*迭代器真正指针伪迭代器内部是位索引引用有效性稳定代理对象是临时值存引用就悬空能否当普通容器用✅❌ 有各种怪癖线程/多线程容器通用性正常位操作可能产生数据竞争5.2 经典翻车现场#include vector #include iostream int main() { std::vectorbool vb(10, false); vb[0] true; // ✅ 可以[] 返回代理对象赋值能写回 auto ref vb[1]; // ⚠️ 代理对象不是 bool ref true; // 能用但别指望它是稳定引用 bool* p vb[0]; // ❌ 编译错误cannot convert ... // 正确做法先拷贝再取地址 bool val vb[0]; const bool* pp val; // ✅ std::cout size vb.size() 内存节省 8 倍\n; return 0; }5.3 如果你真的需要位数组怎么办想要标准容器行为用 std::vectoruint8_t 或 std::vectorchar1 字节一个布尔通用性最好想要省内存且能高效位操作用 std::bitsetN编译期定长或 boost::dynamic_bitset运行时定长想要容器语义省内存std::dequebool 每个元素独立分配避免代理问题。⚠️一句话总结vectorbool 为了省内存牺牲了标准容器契约是标准委员会自己也承认的设计教训。项目里默认不要用它。6. 手写可运行迷你 vector理解才算真懂下面实现一个功能最小但五脏俱全的 MiniVector支持默认构造、push_back、emplace_back、operator[]、size/capacity、扩容含 move_if_noexcept 精神、迭代器、析构。代码可直接编译运行C17#include iostream #include memory #include utility #include cassert #include string template typename T, typename Alloc std::allocatorT class MiniVector { public: using size_type std::size_t; using value_type T; using iterator T * ; using const_iterator const T*; MiniVector() default; ~MiniVector() { clear(); if (begin_) alloc_.deallocate(begin_, cap_ - begin_); } MiniVector(MiniVector other) noexcept : begin_(other.begin_), end_(other.end_), cap_(other.cap_) { other.begin_ other.end_ other.cap_ nullptr; } size_type size() const { return static_castsize_type(end_ - begin_); } size_type capacity() const { return static_castsize_type(cap_ - begin_); } bool empty() const { return begin_ end_; } T operator[](size_type i) { assert(i size()); return begin_[i]; } const T operator[](size_type i) const { assert(i size()); return begin_[i]; } iterator begin() { return begin_; } iterator end() { return end_; } const_iterator begin() const { return begin_; } const_iterator end() const { return end_; } void reserve(size_type new_cap) { if (new_cap capacity()) return; T* new_buf alloc_.allocate(new_cap); size_type old_size size(); size_type i 0; try { for (; i old_size; i) { std::allocator_traitsAlloc::construct( alloc_, new_buf i, std::move_if_noexcept(begin_[i])); } } catch (...) { for (size_type j 0; j i; j) std::allocator_traitsAlloc::destroy(alloc_, new_buf j); alloc_.deallocate(new_buf, new_cap); throw; } // 销毁旧元素并释放旧内存仅当旧指针非空 if (begin_) { for (size_type j 0; j old_size; j) std::allocator_traitsAlloc::destroy(alloc_, begin_ j); alloc_.deallocate(begin_, capacity()); } begin_ new_buf; end_ new_buf old_size; cap_ new_buf new_cap; } void push_back(const T value) { if (size() capacity()) grow(); std::allocator_traitsAlloc::construct(alloc_, end_, value); end_; } void push_back(T value) { if (size() capacity()) grow(); std::allocator_traitsAlloc::construct(alloc_, end_, std::move(value)); end_; } template typename... Args void emplace_back(Args... args) { if (size() capacity()) grow(); std::allocator_traitsAlloc::construct( alloc_, end_, std::forwardArgs(args)...); end_; } void clear() { for (T* p begin_; p ! end_; p) std::allocator_traitsAlloc::destroy(alloc_, p); end_ begin_; } private: // 修正后的扩容策略保证新容量严格大于旧容量 void grow() { size_type new_cap capacity() 0 ? 1 : capacity() * 3 / 2 1; reserve(new_cap); } T* begin_ nullptr; T* end_ nullptr; T* cap_ nullptr; Alloc alloc_{}; // 已移除 [[no_unique_address]]兼容 C11 }; // ---------- 测试 ---------- struct Point { int x, y; Point(int a, int b) : x(a), y(b) {} }; int main() { MiniVectorint v; for (int i 0; i 10; i) v.push_back(i); std::cout MiniVectorint: size v.size() capacity v.capacity() \n; for (auto it v.begin(); it ! v.end(); it) std::cout *it ; std::cout \n; MiniVectorstd::string sv; sv.emplace_back(hello); sv.emplace_back(world); std::cout sv[0] sv[0] sv[1] sv[1] \n; MiniVectorPoint pv; pv.emplace_back(3, 4); std::cout Point: ( pv[0].x , pv[0].y )\n; MiniVectordouble dv; for (int i 0; i 100; i) dv.push_back(i * 1.5); double sum 0; for (double d : dv) sum d; std::cout dv: size dv.size() sum sum \n; return 0; }预期输出这个迷你实现浓缩了 vector 的全部精髓三大指针 几何增长 构造/析构分离先分配内存再构造对象 强异常安全 完美转发。7. 性能实测reserve 前后对比7.1 为什么 reserve 重要reserve(n) 的作用提前把容量扩大到至少 n之后 push_back 到 n 个元素都不会触发扩容。好处省去多次分配新内存 搬迁旧元素的开销对自定义类型尤其明显搬迁要调用拷贝/移动构造避免反复扩容导致的内存碎片。7.2 实测代码#include vector #include chrono #include iostream // 计时工具返回执行 func 的毫秒数 template typename F double time_ms(F f) { auto t0 std::chrono::steady_clock::now(); f(); auto t1 std::chrono::steady_clock::now(); return std::chrono::durationdouble, std::milli(t1 - t0).count(); } int main() { constexpr int N 5000000; // 场景 A不 reserve让 vector 反复扩容 double t_a time_ms([] { std::vectorint v; for (int i 0; i N; i) v.push_back(i); }); // 场景 B先 reserve只分配一次内存 double t_b time_ms([] { std::vectorint v; v.reserve(N); for (int i 0; i N; i) v.push_back(i); }); // 场景 C复杂类型string不 reserve double t_c time_ms([] { std::vectorstd::string v; for (int i 0; i 500000; i) v.push_back(some string data); }); // 场景 D复杂类型stringreserve double t_d time_ms([] { std::vectorstd::string v; v.reserve(500000); for (int i 0; i 500000; i) v.push_back(some string data); }); std::cout A. int 不reserve: t_a ms\n; std::cout B. int reserve : t_b ms\n; std::cout C. str 不reserve: t_c ms\n; std::cout D. str reserve : t_d ms\n; return 0; }典型输出数值因机器而异但 B A、D C 的趋势稳定结论知道大概要放多少个元素时先 reserve能明显提速类型越重有资源管理、拷贝代价高reserve 收益越大不知道数量时也别乱 reserve会浪费内存可以按数量级预估后 reserve。⚠️ 注意reserve 只影响 capacity()不改变 size()它不构造任何元素。不要把它和 resize改变 size会构造/销毁元素搞混。8. C23 新能力vector 又进化了8.1 std::vector::resize_and_overwriteC23这是 C23 给 vector 带来的最重要新成员在未初始化内存上直接写数据跳过默认构造。适合把文件/网络数据读进 vector的场景#include vector #include iostream #include cstring int main() { // 经典写法先 resize 构造一堆默认值再逐个覆盖 —— 白白构造一次 std::vectorchar old_way; old_way.resize(100); // 假设从某处读入 100 字节...每个 char 都经历了默认构造 // C23 新写法resize_and_overwrite 直接在未构造内存里写 std::vectorchar v; v.resize_and_overwrite(100, [](char* buf, std::size_t n) { // 回调里直接往 buf 写不需要先构造 std::memset(buf, A, n); // 模拟读入数据 return n; // 返回实际写入的元素个数 }); std::cout size v.size() v[0] v[0] v[99] v[99] \n; return 0; }要点回调签名void(size_t n) 或 size_t(char* buf, size_t n)后者可返回实际有效长度相比 resize 覆盖省掉一次默认构造对 POD 类型平凡类型收益最大编译需要 C23g -stdc23 test.cpp。8.2 其他值得关注的新动向std::ranges::tostd::vectorC23让视图view一步转成容器containsC20unordered_map 等vector 本身没有但配合 std::ranges::containsC23可写 if (std::ranges::contains(v, 42))动态分配 API 进一步现代化std::allocator 的 allocate_at_leastC23允许分配器返回比请求更多的内存让扩容到 capacity 更大成为可能。9. 常见问题速查表FAQ问题一句话答案size() 和 capacity() 区别size 已构造元素个数capacity 已分配内存能容纳的元素个数。reserve 和 resize 区别reserve 只扩容量不构造元素resize 会改变 size 并构造/销毁元素。push_back 均摊复杂度是多少O(1)摊还靠几何增长保证。扩容因子 1.5 还是 2 好1.5 利于内存分配器复用旧块2 减少搬迁次数。没有绝对最优。扩容会搬走所有元素吗会。分配新内存 → 移动/拷贝旧元素 → 释放旧内存所以扩容后所有迭代器/引用失效。为什么自定义类型的移动构造要标 noexceptvector 扩容用 move_if_noexcept不标 noexcept 就退化为拷贝性能变差。push_back 抛异常会破坏 vector 吗不会有强异常安全保证异常时 vector 保持原状。emplace_back 一定比 push_back 快吗传构造参数直接构造时快省一次移动已有现成对象时两者差别不大。中间 insert / erase 复杂度O(n)元素要整体后移/前移。频繁中间操作应改用 list/deque。为什么不要用 vectorbool它是位压缩特化元素是代理对象拿不到 bool*引用不稳定不是标准容器。迭代器失效怎么防修改容器后重新获取迭代器不要在循环里直接 erase用返回值续迭代。vector 是线程安全的吗不同线程读同一个 vector 安全一个写一个读需要外部加锁。sizeof(vector) 是多少64 位平台一般 24 字节三个指针与元素类型无关。C23 有什么新能力resize_and_overwrite 跳过默认构造直接写内存适合读文件/网络数据。10. 总结vector 三指针模型begin / end / capacity本体 24 字节数据在堆上几何增长让 push_back 摊还 O(1)扩容是搬新家会全量失效迭代器move_if_noexcept 完美转发是 vector 高性能与异常安全的两大支柱迭代器失效规则有明确表格工程上改完容器重新取迭代器vectorbool 是历史包袱默认不用手写迷你 vector 后标准库不再是黑盒知道规模就 reserveC23 还能 resize_and_overwrite 进一步榨性能。