
前言std::vector是 C 标准库中最常用的序列容器。它在堆上维护一段连续存储支持随机访问、尾部快速插入删除并在容量不足时自动扩容。连续存储意味着它可以和 C 风格数组、系统调用、图形 API 直接对接——data()返回的指针可以传给任何需要连续内存的接口。这也是它比std::list、std::deque更常被选作默认容器的原因。围绕vector有三类典型误解。第一以为「扩容是按固定倍数进行的」——标准只要求push_back的均摊复杂度为常数具体增长策略由实现决定常见实现取 1.5 倍或 2 倍但这不是标准保证。第二以为「迭代器只在push_back时失效」——插入、删除、reserve、resize都可能让迭代器、指针、引用失效规则需要逐条记。第三以为vectorbool和普通vectorT一样——它是对bool的特化按位存储operator[]返回代理对象拿不到bool*。本文按内存模型、常用操作、迭代器失效、性能实践四部分展开示例以 C17 为基准涉及 C20 的地方会单独标注。一、内存模型size 与 capacityvector内部通常有三个指针起始、末尾、容量末尾。size()是已构造元素个数capacity()是当前分配的空间能容纳的元素个数二者之间是「已分配但未构造」的区域。理解这一点才能理解为什么clear()不释放内存、为什么reserve()不改变元素个数。// 适用于 C17#include iostream#include vectorint main() {std::vectorint v;std::cout v.size() v.capacity() \n; // 0 0v.reserve(10); // 只改容量不构造元素std::cout v.size() v.capacity() \n; // 0 10v.push_back(1);v.push_back(2);std::cout v.size() v.capacity() \n; // 2 10v.resize(5); // 新增元素值初始化为 0std::cout v.size() v.capacity() \n; // 5 10v.clear(); // 销毁元素容量不变std::cout v.size() v.capacity() \n; // 0 10return 0;}resize(n)会改变元素个数变小时销毁尾部元素变大时追加值初始化的元素对int即 0resize(n, value)则用指定值填充。reserve(n)只在n大于当前容量时重新分配它不会缩短容量也不会构造元素。shrink_to_fit()是「请求」把容量降到与大小匹配标准明确说它是非强制性的non-binding实现可以忽略即使生效也会导致重新分配和迭代器失效。扩容时vector需要申请更大的块把现有元素搬过去再释放旧块。搬移使用移动构造如果元素的移动构造是noexcept或类型不可拷贝否则使用拷贝构造以保证强异常安全。这就是为什么自定义类型的移动构造函数应当标记noexcept否则扩容时可能退化为逐个拷贝。标准不规定增长因子也不保证扩容后容量恰好是原来的某个倍数依赖具体数值的代码不可移植。操作是否改变 size是否可能重新分配复杂度reserve(n)否是n 大于容量时至多线性resize(n)是是线性clear()是变为 0否线性销毁元素shrink_to_fit()否可能线性push_back是加一可能均摊常数pop_back是减一否常数二、常用操作与边界检查构造方式很多默认构造、指定个数、指定个数加初值、初始化列表、从迭代器区间构造、拷贝/移动构造。C17 起还有类模板实参推导CTAD可以写std::vector v{1, 2, 3};让编译器推导元素类型但要注意初始化列表与「个数加值」的歧义std::vectorint v(3, 5)是三个 5std::vectorint v{3, 5}是两个元素 3 和 5圆括号与花括号语义不同。访问元素有几种方式。operator[]不做边界检查下标越界是未定义行为at()做检查越界抛std::out_of_rangefront()/back()要求容器非空对空容器调用是未定义行为data()返回指向首元素的指针C11 起空容器时返回值不保证可解引用。调试阶段可以用at()或开启标准库的调试模式发布版本再按性能需要选择。// 适用于 C17#include iostream#include stdexcept#include vectorint main() {std::vectorint v{10, 20, 30};try {std::cout v.at(1) \n; // 20std::cout v.at(3) \n; // 抛 std::out_of_range} catch (const std::out_of_range e) {std::cout out of range: e.what() \n;}if (!v.empty()) {std::cout v.front() v.back() \n;}return 0;}插入与删除集中在中间时vector需要搬移元素复杂度是线性的。insert(pos, value)返回指向新元素的迭代器erase(pos)返回被删元素之后元素的迭代器C11 起。批量删除满足条件的元素惯用法是「擦除—移除」erase-remove idiom// 适用于 C17#include algorithm#include iostream#include vectorint main() {std::vectorint v{1, 2, 3, 4, 5, 6, 7, 8};// 删除所有偶数v.erase(std::remove_if(v.begin(), v.end(),[](int x) { return x % 2 0; }),v.end());for (int x : v) {std::cout x ;}std::cout \n;return 0;}std::remove_if并不删除元素它把保留的元素前移返回新的逻辑末尾真正的删除由erase完成。C20 起可以直接用std::erase_if(v, pred)一行完成且对容器大小处理更自然使用前要确认工具链支持 C20。emplace_back与push_back的区别在于前者把参数完美转发给元素类型的构造函数在容器内直接构造后者接受一个已构造的对象或可转换的值再拷贝或移动进去。对于std::string这类类型emplace_back(5, a)可以省掉临时对象。但emplace_back也可能匹配到非预期的构造函数参数含义要看目标类型的构造函数集合。三、迭代器、指针与引用的失效规则vector的迭代器本质上是指针或指针的包装它指向内部缓冲区。一旦缓冲区重新分配所有迭代器、指针、引用都会悬垂。即使没有重新分配插入或删除也会让插入/删除点之后的迭代器失效因为元素被搬移了。操作失效范围push_back/emplace_back未扩容尾后迭代器end()元素迭代器仍有效push_back/emplace_back发生扩容全部迭代器、指针、引用insert未扩容插入点及之后的迭代器insert发生扩容全部erase删除点及之后的迭代器reserve容量变化全部resize发生扩容全部clear全部元素迭代器swap迭代器仍有效但指向另一个容器中的元素一个经典错误是在循环里缓存end()// 错误示范适用于 C17但逻辑有缺陷std::vectorint v{1, 2, 3};auto endIt v.end();for (auto it v.begin(); it ! endIt; it) {if (*it 2) {v.push_back(4); // 可能扩容endIt 失效}}如果push_back触发扩容endIt悬垂比较或自增都是未定义行为。正确做法是每次重新取v.end()。另一种常见错误是在范围 for 循环中删除元素// 错误示范erase 后迭代器失效for (auto it v.begin(); it ! v.end(); it) {if (*it 0) {v.erase(it); // it 失效随后 it 是 UB}}erase返回下一个有效迭代器应写成it v.erase(it);且删除时不要额外自增或者用 erase-remove 惯用法一次性处理。范围 for 循环内部持有迭代器循环体中修改容器大小同样可能导致失效除非你能保证不触发重新分配且只修改当前元素。四、性能与实践建议第一已知规模时先reserve。这能避免多次扩容与搬移但不要为了「省一次分配」而预留远超实际需要的容量内存也是资源。第二优先emplace_back构造就地生成对象尤其是元素类型构造代价高时但不要把它当成绝对规则语义清晰更重要。第三删除大量元素用 erase-remove 或 C20 的erase_if不要在循环里逐个erase后者是 O(n²)。第四传递只读参数用const std::vectorT需要拷贝时按值传递并std::move不要既传引用又在内部拷贝。第五与 C 接口交互时用data()和size()注意空容器时data()可能返回空指针。vectorbool需要单独提醒。标准对bool做了特化按位压缩存储以节省空间代价是operator[]返回代理对象proxy reference不能取地址得到bool*也不满足「连续存放bool元素」的通常预期。它不提供data()成员。若需要与 C 接口交互或需要真正的bool数组用std::vectorchar、std::vectorunsigned char或std::bitset长度固定时。vectorbool本身不是错误但要知道它的特殊性。关于性能不要相信「vector一定比链表快」或「预留一定更快」这类绝对结论。vector的优势来自连续内存带来的缓存局部性、随机访问和较低的单元素开销劣势是中间插入删除的搬移成本。选择容器应基于访问模式与规模性能数字要在自己的数据与工具链上测量例如用std::chrono::steady_clock计时注意预热、优化等级和测量噪声而不是套用别人的倍数。常见坑点❌ 用operator[]访问可能越界的下标并期待抛异常。 ✅ 需要检查时用at()确认下标合法的热路径再用operator[]。❌ 认为clear()会释放内存。 ✅clear()只销毁元素要释放容量用shrink_to_fit()非强制或与空容器交换。❌ 在循环中缓存end()循环体内又push_back。 ✅ 每次重新取v.end()或在循环前reserve保证不扩容仍需谨慎。❌erase(it)后继续it。 ✅ 用it v.erase(it);删除分支不再自增。❌ 在范围 for 中改变容器大小。 ✅ 改用迭代器循环并正确处理返回值或先收集下标再统一删除。❌ 依赖扩容倍数或扩容后的确切容量。 ✅ 只依赖标准保证的均摊复杂度和迭代器失效规则需要确定容量就显式reserve。❌ 对vectorbool取元素地址或传给需要bool*的接口。 ✅ 改用std::vectorchar或std::bitset或接受代理对象的限制。❌ 自定义类型有移动构造却没标noexcept导致扩容时拷贝。 ✅ 不抛异常的移动构造标noexcept让容器选择移动路径。总结主题关键结论存储连续内存随机访问 O(1)尾部插入均摊 O(1)size/capacitysize是元素数capacity是已分配容量扩容标准不规定增长因子扩容使全部迭代器失效中间操作insert/erase为 O(n)并使插入/删除点之后失效删除惯用法erase-removeC20 可用std::erase_if特化vectorbool按位存储返回代理引用std::vector的 API 不复杂难的是把「容量、元素个数、迭代器有效期」三件事分开想。写插入删除逻辑时先判断会不会扩容再判断哪些迭代器还能用需要稳定地址时考虑std::deque或std::list但不要为了「怕失效」放弃vector的局部性优势。把reserve、erase-remove、at()与operator[]的边界用对绝大多数问题都能在编译期或测试期暴露。