1. 先聊清楚 vector 的“性格”——底层实现与内存模型
上一篇文章我们把 STL 的容器体系整体过了一遍,这一篇专门把 vector 拎出来聊透。之所以把 vector 放在第二篇单独讲,原因很简单:它是 STL 里使用频率最高、同时也是最容易产生隐性性能问题的一个容器。很多人写了几年代码,对 vector 的理解还停留在“动态数组,尾部插入快,中间插入慢”这个口头禅层面,一旦遇到迭代器失效、内存碎片问题就一脸懵了。
vector 的本质,用一句话概括就是:封装了动态数组内存管理的序列容器。它在内部维护了一块连续的内存空间,同时记录三个关键指针(不同实现略有差异,但思路一致):
start(或者叫begin):指向数组起始位置;finish(或者叫end):指向当前已使用元素的末尾;end_of_storage:指向整块已分配内存的末尾。
弄清楚这三个指针,vector 的很多行为就能自洽地解释了。比如size()返回的是finish - start,而capacity()返回的是end_of_storage - start,两者之间的差值就是“已分配但尚未使用”的预留空间。
连续内存带来的好处是显而易见的:随机访问是 O(1),支持指针算术,缓存友好性极高,迭代器本质就是指针(对T*的封装)。因为内存连续,CPU 预取数据时能把相邻元素一批拉进缓存,遍历性能非常出色,实测在某些场景下比链表快一个数量级,这也是 vector 在绝大多数场景下被默认推荐的核心原因。
但有得必有失,连续内存也意味着存储的元素类型必须是可复制的(或可移动的),且插入、删除元素时涉及大量数据搬移。更重要的是,当空间不够时 vector 需要整体重新分配一块更大的内存,把所有旧元素挪过去,再释放旧内存,这个过程叫扩容(reallocation)。后面我会专门用一整节来讲扩容,因为这里藏着至少 80% 的 vector 性能问题根源。
在工程实践里,我建议把 vector 理解成一层“带缓冲的手动数组管理工具”——它替你干了malloc/realloc/memcpy/free这些脏活,但你仍然需要理解它在底层做了什么,否则很难解释为什么同样的代码在不同编译器下性能差异巨大,或者为什么程序运行着运行着突然内存暴涨。
2. 扩容机制深度解析——别让 vector 变成“扩容狂魔”
2.1 为什么是倍增扩容而不是固定增量
vector 在push_back时发现size() == capacity(),就会触发扩容。扩容的典型流程是:
- 申请新内存块,大小为旧容量的若干倍;
- 把旧元素逐个构造(或移动)到新内存;
- 释放旧内存;
- 更新三个内部指针。
这里最关键的问题:新容量取多少倍?标准里没有硬性规定,只要求均摊复杂度为 O(1)。常见的做法是 2 倍(GCC libstdc++)或 1.5 倍(MSVC 较早版本,新版本也接近 1.5~2 倍之间变动)。
为什么不用“每次多加 10 个”这种固定增量?道理其实很好理解。如果容量按固定数量k增长,那么插入n个元素的总复制次数是O(n²/k)——每加满一次就要整体搬移一次,累计开销是平方级的。而倍增策略下,搬移总次数是O(n)量级,每次搬移的规模呈几何级数增长,越往后单次搬移越重,但搬移次数越来越少,均摊下来每个元素的成本非常低。
我见过有人写代码时用for循环给 vector 做了几万次push_back,然后抱怨性能不如原生数组。这往往是没开-O2,同时频繁扩容导致大量搬移——每次扩容都是实打实的元素拷贝(甚至是深拷贝),自然慢得离谱。
2.2 2 倍还是 1.5 倍,到底哪个好
这里有个容易被忽略的工程细节:2 倍扩容在内存分配器上更容易产生碎片,1.5 倍扩容在内存复用上更友好。原因是分配器(比如 ptmalloc2)通常会按大小类别管理空闲块,如果每次扩容都严格翻倍,旧块和新块之间往往夹着其他对象,释放旧块后很难被复用,时间一长堆上就会散布大小不一的空洞。
1.5 倍之所以被一些编译器青睐,是因为它能让新旧容量之间出现“重叠区间”,使得旧内存块在释放后可能正好能被下一次扩容使用,内存复用率更高。代价是均摊搬移次数稍微多一点点。说实话,普通业务代码不用特别纠结这个差异,但如果你在做嵌入式开发或内存受限的高性能服务,建议用reserve提前规划好容量,从根上避免反复扩容,这才是根治方案。
下面给一个简单的扩容观察代码,实测你环境下的倍数策略:
#include <iostream> #include <vector> int main() { std::vector<int> v; size_t last_cap = v.capacity(); for (int i = 0; i < 100; ++i) { v.push_back(i); if (v.capacity() != last_cap) { std::cout << "size=" << v.size() << " capacity=" << v.capacity() << " growth=" << (double)v.capacity() / last_cap << "\n"; last_cap = v.capacity(); } } return 0; }运行后你会清晰地看到 capacity 的跳跃轨迹,这个表格在你自己环境里实测记录一下,比任何文档都有说服力。
2.3 reserve、resize 和 shrink_to_fit 到底该怎么用
三者的区别很多人背过,但实际用起来就拿不准:
reserve(n):只改容量,不改变size()里的元素个数。它只保证后续n个元素的插入不会再触发扩容;resize(n):同时改容量和元素个数。如果n > size(),新元素被值初始化;如果n < size(),尾部元素被析构;shrink_to_fit():请求把容量压缩到和size()一样,但这只是一个“非绑定请求”,实现可以忽略它,C++ 标准没有强制保证。
工程里的正确打开方式:如果能预估元素数量的上界(或者下界也行),直接用reserve预先分配。比如你要从一个配置文件里解析一批节点,明确知道行数大概几百行,那就vector<Node> nodes; nodes.reserve(1024);,一次性分配到位,后续所有push_back都不会再触发内存搬移。这是我做性能优化时最常用、见效最快的一招。
要注意reserve和resize同时使用容易出现的事故:resize会把元素个数也改了,你再用push_back时元素会追加到已调整的末尾之后,导致元素数量比预期多出一截,而且前面的元素还是默认值。如果只想预留容量不改变元素个数,务必只用reserve。
2.4 扩容过程中的迭代器失效问题
扩容最直接的后果就是:所有指向旧内存的迭代器、指针、引用全部失效。因为元素搬到了新地址,旧地址成了一块被释放的内存。以下代码就是典型翻车现场:
std::vector<int> v{1, 2, 3}; auto it = v.begin(); v.push_back(100); // 若触发扩容,it 变成野指针 std::cout << *it; // 未定义行为很多人觉得“push_back 之后迭代器失效”是个常识,但真正写代码时依然会漏。最坑的是,小容量 vector 前几次push_back未必触发扩容,跑得好好的,一旦哪天数据量超过阈值,程序就随机崩溃或产生脏数据,这种问题极难复现和定位。
所以我的习惯是:只要元素数量边界不确定,且后续还要持有指向元素的指针或迭代器,就先用 reserve 把容量一次定到位,让扩容永远不发生。
3. 迭代器失效——vector 陷阱的“重灾区”
vector 的迭代器失效规则其实可以用一句话记牢:只要容器重新分配了内存,所有迭代器全部失效;如果是插入/删除操作导致元素搬移,受影响的是从操作点一直到末尾的所有迭代器。具体拆开看:
3.1 insert 与 erase 的失效范围
insert(pos, val)在pos处插入元素,从pos到末尾的所有元素都会向后搬一个位置,因此这些位置的迭代器全部失效;erase(pos)删除元素,从pos到末尾的元素向前搬移,迭代器同样失效。注意,尾后迭代器end()通常也会失效,因为它指向的内存位置已经变化了。这跟std::list完全不同,链表删除一个节点只影响被删节点的迭代器,其他迭代器安然无恙,这也是为什么需要频繁删除中间元素时很多人改选 list 的原因。
很多新手写删除循环,一开始就用for (auto it = v.begin(); it != v.end(); ++it),然后在循环体里v.erase(it),这就是标准未定义行为。正确的擦除循环写法:
for (auto it = v.begin(); it != v.end(); ) { if (*it % 2 == 0) { it = v.erase(it); // erase 返回下一个有效迭代器 } else { ++it; } }C++11 以后erase返回被删除元素的下一个元素的迭代器,这大大方便了连续删除场景。不过这段代码的时间复杂度是 O(n²)——每次 erase 都会把后续元素往前搬。如果只是要“删除满足条件的元素”,标准库提供了更优的惯用法:
v.erase(std::remove_if(v.begin(), v.end(), [](int x) { return x % 2 == 0; }), v.end());这叫erase-remove 惯用法。remove_if通过覆盖搬移把满足条件的元素挪到末尾,返回新逻辑末尾的迭代器,然后erase把后面的残留元素一次性清掉。这样的拷贝次数是 O(n),比逐个erase的 O(n²) 快一个数量级,实测删除 10 万元素时性能差距肉眼可见。
3.2 push_back 导致 reference 失效的隐蔽场景
比迭代器失效更隐蔽的是引用失效。比如:
std::vector<std::string> v{"hello"}; auto& ref = v.front(); v.push_back("world"); // 扩容后 ref 可能失效 ref += "!";如果 v 的容量足够容纳新元素,push_back不会触发扩容,ref 依然有效;一旦扩容,ref 指向的内存被释放,对 ref 的写操作变成了悬垂写。这种问题在单元测试里往往因为数据量小从来不发作,上线后数据一大就随机崩溃。我的排查经验是:任何在容器操作后仍保留的引用/指针都需要重新获取,不要在两次修改性操作之间缓存引用。
3.3 vector 重分配后获取新首元素的惯用法
如果确实需要在push_back之后继续操作某个元素,不要缓存引用,而是即时取:
v.push_back(newItem); auto& last = v.back(); // 每次使用都现取,不要存下来跨操作或者提前reserve保证不扩容。对于长期持有的元素地址,更稳妥的方案是存下标而不是存迭代器/指针——即使扩容,元素在 vector 中的相对位置不会变,v[i]永远能取到正确元素。这是我多次踩坑后总结出来的土办法,简单但极其好用。
4. 拷贝、移动与元素管理——别让 vector 偷偷做深拷贝
4.1 vector 的拷贝构造与浅拷贝陷阱
vector 的拷贝构造函数会逐个拷贝元素,这是一个深度拷贝操作。如果元素是自定义类型且拷贝代价很高(比如包含堆内存的字符串、图像缓冲),整个拷贝过程会非常昂贵。以下场景容易被忽视:
std::vector<std::string> big; // ... 塞入大量长字符串 ... std::vector<std::string> copy = big; // 深拷贝,每个 string 都要拷贝如果不小心把 vector 按值传参或从函数按值返回,就可能发生多次深拷贝。C++11 之前这是性能毒瘤,C++11 之后移动语义缓解了很大一部分问题:右值可以直接转移内部指针,按值返回时触发的是移动而不是拷贝。
真正容易埋雷的是容器里存放原始指针的情况:
std::vector<Foo*> v; v.push_back(new Foo());vector 析构时不会替你去delete这些指针。如果忘了在销毁前手动释放,就是经典的内存泄漏。很多人写业务代码时图省事直接vector<Foo*>,项目结束了一大堆泄漏。我的建议是:优先用std::vector<std::unique_ptr<Foo>>或std::vector<std::shared_ptr<Foo>>,让所有权语义在编译期就确定下来,省心也安全。
4.2 emplace_back 和 push_back,到底差在哪
push_back接收一个现成的对象,把它拷贝或移动到容器中;emplace_back接收构造参数,直接在容器预留的内存上原地构造对象,省掉一次拷贝/移动构造。区别主要体现在:
v.push_back(MyType(a, b)); // 先构造临时对象,再拷贝/移动到容器 v.emplace_back(a, b); // 直接在容器内构造理论上emplace_back更高效,但实际差异取决于MyType的拷贝/移动成本。如果类型只是一个int或轻量 POD,差距微乎其微;如果类型构造复杂、拷贝昂贵,emplace_back的优势就很明显了。
不过要注意一个坑:emplace_back的构造函数参数会参与重载决议,在某些环境下可能导致隐式转换问题,不如push_back直观。我的工程习惯是:优先写emplace_back,如果遇到类型转换相关的编译报错,再退回push_back并显式构造。
4.3 移动语义对 vector 扩容的影响
C++11 之后,vector 扩容时如果元素的移动构造函数被声明为noexcept,就会优先使用移动而不是拷贝来搬移元素;否则为了保证强异常安全,标准库会退化为拷贝。这一条极其重要,因为如果你的自定义类型可以移动但不标记noexcept,vector 扩容时还是会走昂贵的拷贝路径。所以:
class MyType { public: MyType(MyType&&) noexcept; // 关键:明确 noexcept MyType& operator=(MyType&&) noexcept; };不写noexcept的后果是:一个明明可以低成本移动的对象,在 vector 扩容时被老老实实地深度拷贝一遍,性能直接打回原形。我用static_assert(std::is_nothrow_move_constructible_v<MyType>)在编译期把这个约束卡死,一旦有人改了类的移动构造签名导致noexcept丢失,编译直接报红,从根上避免这个坑。
5. 二维 vector 实战——创建、遍历与清空的各种姿势
二维vector<vector<T>>本质是“外层 vector 的每个元素又是一个 vector”。这种嵌套结构写起来自然,但在性能和内存布局上有不少暗坑,这一节把使用频率最高的几种操作捋明白。
5.1 创建二维 vector 的几种方式
方式一:默认构造后逐层 push_back
std::vector<std::vector<int>> mat; mat.push_back(std::vector<int>{1, 2, 3}); mat.push_back(std::vector<int>{4, 5});这种方式灵活,每行长度可以不一样,但外层 vector 会不断扩容,且每一行 vector 都是独立的堆分配,内存碎片化比较严重。
方式二:一次性指定行数和列数
int rows = 10, cols = 20; std::vector<std::vector<int>> mat(rows, std::vector<int>(cols, 0));这个最常用。外层直接构造好rows个元素,每个元素都是一个长度为cols的 int 型 vector,初始值全部为 0。注意这里有个易错点:如果只写vector<vector<int>> mat(rows),那么每行是默认构造的空 vector,访问mat[i][j]直接越界。必须有第二层参数把内层 vector 的大小和初值指定好。
方式三:从一维数组批量构造
std::vector<int> data = {1, 2, 3, 4, 5, 6}; int cols = 3; std::vector<std::vector<int>> mat; for (size_t i = 0; i < data.size(); i += cols) { mat.push_back(std::vector<int>(data.begin() + i, data.begin() + i + cols)); }这种方式适合把拍平的数据重新分块成矩阵。
5.2 二维 vector 的“整块清空”误区
热词里出现了“二维 vector 清空”,这里有个高频翻车点。如果你只是想清空每行元素但保留行数,正确操作是:
for (auto& row : mat) { row.clear(); } // 此时 mat.size() 不变,但每一行 size()==0如果想要整个二维 vector 全部清空(行也没了),直接:
mat.clear();但要注意,clear()只析构元素,不会释放容量。也就是说清空后mat.capacity()可能依然很大,内存并没有还给操作系统。如果想连内存一起释放,需要:
std::vector<std::vector<int>>().swap(mat); // 交换一个临时空对象 // 或者 C++11 以后 mat.clear(); mat.shrink_to_fit(); // 注意也不保证一定释放我自己常用vector<vector<int>>().swap(mat)这个老把式,它确保析构原容器并释放底层内存,在内存敏感的长生命周期服务里很有用。
5.3 遍历二维 vector 的缓存友好性问题
二维 vector 在内存布局上是“每位一行的连续数组,但行与行之间未必连续”。因为每一行是独立的vector<int>,内存块在堆上散布在不同地址,遍历时容易发生缓存行跳跃。
一种常见优化是把二维数据拍平成一维 vector:
std::vector<int> flat(rows * cols); // 访问 mat[i][j] 改为 flat[i * cols + j]这样一整块内存完全连续,遍历性能和缓存命中率都有明显提升。追求极致性能的数值计算场景,我甚至不建议用vector<vector<...>>,直接用一维 flat vector 会比嵌套 vector 快 20%~50% 不等,具体数据跟矩阵规模、编译器优化级别有关,但趋势非常稳定。
5.4 嵌套 vector 在元素生命周期上的隐性开销
外层 vector 扩容时,需要对每一行 vector 做移动或拷贝。因为内层是独立对象,移动成本不高,但外层容量预估不准时反复搬移所有行,成本也不低。一个可行的折中方案是:确定行数后先reserve外层,再逐个初始化内层,减少外层扩容次数。
我更推荐的替代方案是用std::array描述固定内层长度:vector<array<int, 4>>适合每行长度固定的场景——内层不再有独立的堆分配,内存布局更紧凑,遍历性能和分配性能都优于嵌套 vector。实际业务里如果每行长度确一致,vector<array<T, N>>是比vector<vector<T>>更好的选择。
6. 工程实践中的性能优化与常见坑排查
6.1 预分配容量的黄金法则
前面反复提到reserve,这一节把实践方法一次性讲清楚。核心法则是:在向 vector 写入数据之前,先预估数据规模并 reserve。
举一个实际例子:你从一个十几万行的文本文件里逐行读取并存入 vector:
std::vector<std::string> lines; // 直接开干 while (std::getline(ifs, line)) { lines.push_back(line); }这个循环里 vector 反复扩容,每次扩容都要把已存的所有std::string搬一遍。如果文件有 20 万行,大概要经历十几次扩容,每一次搬移 1 万、2 万、4 万……20 万个 string,总拷贝量接近 40 万次 string 移动。如果改成:
std::vector<std::string> lines; lines.reserve(200000); // 预估行数 while (std::getline(ifs, line)) { lines.push_back(line); }扩容完全不会发生,20 万个 string 只移动一次各自的内部指针,性能提升肉眼可见。所以我在写这种“从文件/网络读取大量记录”的代码时,无条件先 reserve 一个合理的估算值,多估几个元素只是多占一点内存,换来的是稳定的性能保障。
6.2 vector 与其他容器的选型对照
经常有人问“什么时候用 vector,什么时候用 list/deque”。我用一张表整理自己的选型逻辑,这属于经验浓缩,可以直接参考:
| 场景特征 | 首选容器 | 原因 |
|---|---|---|
| 频繁随机访问、遍历为主 | vector | 连续内存、缓存友好,O(1) 随机访问 |
| 仅在尾部插入/删除 | vector | 尾部操作摊还 O(1) |
| 需要在头部大量插入/删除 | deque 或 list | vector 头部插入要搬移所有元素 |
| 需要频繁在中间插入/删除且元素很大 | list | vector 搬移成本高;list 只改指针 |
| 数据量小且频繁增删 | vector 仍可考虑 | 小 vector 的连续内存优势明显,搬移成本低 |
| 大块数据但长度固定 | vector + reserve | 避免动态扩容 |
| 大量小对象且需稳定迭代器 | list | vector 扩容会使迭代器失效 |
注意,很多文章会告诉你“中间插入选 list”,我个人的实测经验是:如果你的元素是int这类轻量类型,vector 即使做中间插入,实际也往往比 list 快,因为 list 每个节点有独立的堆分配开销和更差的缓存访问模式。不要凭直觉选容器,先 benchmark 再决定。
6.3 vector 的“著名问题”
vector<bool>是标准库里的一个特殊存在——它不是真正的 bool 数组。为了节省内存,标准库把它实现成位压缩版本,每个 bool 只占 1 bit。听起来很美好,但这带来一系列诡异行为:
v[i]返回的是一个代理对象(std::vector<bool>::reference),不是真正的bool&;- 无法用
auto& ref = v[0]这样获取 bool 引用; - 把它当成普通 vector 去套模板、传指针时经常编译不过或行为怪异。
所以工程上有一条不成文的规矩:需要 bool 数组且重视行为一致性时,用vector<char>或deque<bool>替代vector<bool>。位压缩省下的那点内存,远没有调试时踩坑的成本高。
6.4 用 vector 存储大对象时的指针方案
如果元素非常大(比如每个对象几百字节甚至几 KB),且对象数量庞大,vector 在扩容时的搬移成本会变得极其高昂。此时有三种常见优化:
- 提前 reserve,避免扩容搬移;
- 存储智能指针:
vector<unique_ptr<BigObj>>,扩容时只搬移指针,不搬移对象,不过多了一次解引用,且堆上碎片增加; - 使用 deque 替代:deque 按块分配,扩容不搬移已有元素,对超大元素更友好。
我见过很多把大对象直接塞进 vector 然后抱怨“每次 push_back 都卡”的案例。解决思路往往是先评估对象的拷贝/移动成本,再决定是否上指针方案。
6.5 热词里“vector 工具链”相关的说明
防混淆一下:日常 C++ 开发里的 STL vector 通常和网络工具/汽车总线工具链里的 CANoe、HexView 等软件没有直接关系。搜索热词里出现的“configuration1.cfg [offline] vector canoe”“vector hexview 下载”是 Vector Informatik 公司的汽车电子工具产品,属于另一个领域。写 C++ 的同学如果搜到这些内容不要困惑,关注“stl vector 容器”本身即可。同样的,stl 缩略图不显示这类问题,大多指 STL 三维模型文件的资源管理器预览插件缺失,也不是 C++ 容器层面的问题,注意区分。
7. 浏览器实践环节——手写一个迷你 vector 理解内部机制
光说不练假把式。这一节带大家用一个简化版 vector 实现,把刚刚说的内存管理机制落到代码上。这个迷你 vector 不做完整 STL 兼容,只实现核心的 push_back、扩容、索引访问,目的就是让你通过调试一窥容量增长和迭代器失效的本质。
实现思路:
template <typename T> class MiniVector { public: MiniVector() : start_(nullptr), end_(nullptr), storage_end_(nullptr) {} ~MiniVector() { for (size_t i = 0; i < size(); ++i) { start_[i].~T(); // 显式析构 } ::operator delete(start_); // 释放原始内存 } void push_back(const T& val) { if (end_ == storage_end_) { grow(); } new (end_) T(val); // placement new,在已有内存上构造 ++end_; } T& operator[](size_t idx) { return start_[idx]; } size_t size() const { return end_ - start_; } size_t capacity() const { return storage_end_ - start_; } private: void grow() { size_t new_cap = capacity() == 0 ? 1 : capacity() * 2; T* new_start = static_cast<T*>(::operator new(new_cap * sizeof(T))); for (size_t i = 0; i < size(); ++i) { new (new_start + i) T(std::move_if_noexcept(start_[i])); start_[i].~T(); } ::operator delete(start_); start_ = new_start; end_ = new_start + size(); storage_end_ = new_start + new_cap; } T* start_; T* end_; T* storage_end_; };这段代码的关键动作:
grow里先分配new_cap个 T 大小的原始内存(不构造对象);- 用 placement new 把所有旧元素移动或拷贝到新内存;
- 把旧元素显式析构;
- 释放旧内存;
- 更新三个指针后返回。
你在断点模式下观察size()、capacity()、start_的变化,就能直观感受到“扩容导致元素地址搬家”是怎么发生的。运行几次push_back,会发现每轮capacity()翻倍,且start_的地址每次都变化——这就是迭代器失效的根源。
这个迷你实现没有考虑异常安全,真实 STL 的做法要精巧得多,但理解这个骨架之后,再去读 libstdc++ 或 MSVC 的 vector 源码会轻松很多。我强烈建议想深入 vector 的读者花一个小时亲手把这个实现跑通、打上断点,这比看十篇博客都有用。
8. 常用 API 一览与实际使用建议
最后把 vector 的常用接口整理成一份速查清单,方便日常写代码快速查阅:
| 操作 | 接口 | 时间复杂度 | 关键提示 |
|---|---|---|---|
| 末尾追加 | push_back(val)/emplace_back(args) | 摊还 O(1) | 可能触发扩容、使迭代器失效 |
| 尾部弹出 | pop_back() | O(1) | 不减少容量,只析构尾部元素 |
| 随机访问 | v[i]/at(i) | O(1) | at 有越界检查,抛 out_of_range |
| 首尾访问 | front()/back() | O(1) | 空容器上调用是未定义行为 |
| 指定位置插入 | insert(pos, val) | O(n) | 需要搬移后续元素 |
| 指定位置删除 | erase(pos) | O(n) | 返回下一个有效迭代器 |
| 批量删除 | erase(remove_if(...), end()) | O(n) | 推荐惯用法,避免逐个 erase |
| 容量预留 | reserve(n) | O(n) | 只影响容量,不改 size |
| 调整大小 | resize(n) | O(n) | 改变元素个数,是否扩容不定 |
| 清空元素 | clear() | O(n) | 不释放容量 |
| 释放内存 | vector<T>().swap(v) | O(1) | 强硬释放底层内存 |
| 比较 | ==/< | O(n) | 逐元素比较 |
| 交换 | swap(v1, v2) | O(1) | 只交换内部指针,非常快 |
实际使用建议总结成三条:
- 默认选 vector:除非你明确知道需要频繁头尾插入或稳定迭代器,否则 vector 是最稳的七边形战士;
- 无脑 reserve:任何能预估规模的写入循环,先 reserve 再写,收益极其明显;
- 存对象时想清楚所有权:能用 RAII 智能指针就不要裸指针,能标记 noexcept 就不要让移动退化成拷贝。
我写 C++ 这么多年,vector 是每天都会碰的容器,但它隐藏的细节之深,直到我自己动手实现一个才真正理解。很多人觉得 STL 开箱即用没必要深究,可一旦项目规模上去、性能瓶颈出现,翻来覆去排查半天最后发现是 vector 扩容惹的祸,那种滋味实在不好受。下一篇我打算把std::string的实现与优化机制拆开聊一聊,它和 vector 的不少行为类似,但又在 SSO 短字符串优化上有自己独特的逻辑,先在这挖个坑,后面慢慢填。