vector是C++ 标准模板库(STL)的一个容器,它本质是一个封装了动态内存管理的数组,既保留了原生数组随机访问的高效性,又解决了固定长度、手动管理内存的痛点。
一,vector是什么
std::vector是 STL 中的顺序容器,底层在堆上开辟连续的内存空间存储元素,支持动态自动扩容,可以理解为「C++ 版的动态数组」。
它的核心特性可以总结为四点:
1,连续内存存储:所有元素在内存中紧密连续排列,支持 O (1) 时间复杂度的随机访问,原生指针可直接操作元素,兼容所有 C 风格接口。
2,动态自动扩容:无需提前声明固定长度,当元素数量超过当前内存容量时,会自动申请更大的内存并迁移数据,全程无需开发者手动管理。
3,尾操作高效,中间操作低效:尾部插入 / 删除的均摊时间复杂度为 O (1);头部或中间位置的插入 / 删除需要移动后续所有元素,时间复杂度为 O (n)。
4,类型安全:基于模板实现,编译期完成类型校验,相比原生 C 数组更安全,不易出现类型不匹配的问题。
二,vector的基础用法
2.1,构造和初始化
vector 提供了多种构造方式,覆盖不同初始化场景:
#include <vector> #include <iostream> int main() { // 1. 空 vector,无元素,size=0,capacity=0 std::vector<int> v1; // 2. 构造包含 5 个元素的 vector,元素为默认值(int 为 0) std::vector<int> v2(5); // 3. 构造包含 5 个元素,每个元素值都是 10 std::vector<int> v3(5, 10); // 4. 拷贝构造:用另一个 vector 完整复制 std::vector<int> v4(v3); // 5. 列表初始化(C++11 起),最直观的写法 std::vector<int> v5{1, 2, 3, 4, 5}; // 6. 迭代器范围初始化 std::vector<int> v6(v5.begin(), v5.begin() + 3); // 元素为 1,2,3 return 0; }2.2 ,元素访问
vector 提供了多种访问元素的方式,各有适用场景:
std::vector<int> v{10, 20, 30, 40, 50}; // 1. 下标访问 []:无越界检查,越界属于未定义行为,性能最高 int a = v[2]; // 30 // 2. at() 方法:越界时抛出 std::out_of_range 异常,更安全 int b = v.at(2); // 30 // 3. 访问首尾元素 int first = v.front(); // 10 int last = v.back(); // 50 // 4. 获取底层原生指针,可对接 C 语言接口 int* ptr = v.data();2.3 ,常用增删操作
std::vector<int> v{1, 2, 3}; // 尾部插入 v.push_back(4); // 尾部插入元素 4 v.emplace_back(5); // 尾部直接构造元素,性能更优 // 尾部删除 v.pop_back(); // 删除最后一个元素,无返回值 // 指定位置插入 v.insert(v.begin() + 1, 10); // 在第2个位置插入 10 // 指定位置删除 v.erase(v.begin() + 1); // 删除第2个元素 // 清空所有元素 v.clear();2.4, 三种遍历方式
std::vector<int> v{1, 2, 3, 4, 5}; // 1. 下标遍历,适合需要索引的场景 for (size_t i = 0; i < v.size(); ++i) { std::cout << v[i] << " "; } // 2. 迭代器遍历,容器通用的遍历方式 for (auto it = v.begin(); it != v.end(); ++it) { std::cout << *it << " "; // 3. 范围 for 循环(C++11 起),语法最简洁 for (auto num : v) { std::cout << num << " "; }三,底层原理:vector 是怎么工作的
只停留在「会用」层面,很容易踩坑。理解 vector 的底层实现,是写出高效、安全代码的关键。
3.1 ,核心数据结构:三个指针
1,_start:指向内存块的起始位置,也就是第一个元素的地址
2,_finish:指向最后一个有效元素的下一个位置
3,_end_of_storage:指向整块分配内存的末尾位置
通过这三个指针,就能推导出两个核心属性:
1,size():当前有效元素个数 =_finish - _start。这个计算基于指针算术,时间复杂度为 O(1)。size()返回的是容器中实际存储的元素数量,而不是内存容量。当容器为空时,_start == _finish,size()返回 0。需要注意的是,size()返回的类型是size_t(无符号整数),因此在循环条件中与有符号数比较时要注意类型转换问题。此外,size()是成员函数,调用时不会改变容器状态,是 const 成员函数。
2,capacity():当前内存总共能容纳的元素数 =_end_of_storage - _start
3.2,扩容机制全流程
当我们调用push_back/emplace_back时,如果size() == capacity(),说明当前内存已经满了,就会触发自动扩容,完整流程分为四步:
1,申请新内存:向系统申请一块更大的连续内存,扩容倍率由编译器实现决定 ——GCC 标准库为 2 倍扩容,MSVC 为 1.5 倍扩容。
2,迁移元素:将原内存中的所有元素,通过拷贝构造或移动构造(C++11 起)复制到新内存中。
3,释放旧内存:调用原内存中所有元素的析构函数,再把整块旧内存归还给系统。
4,更新指针:将三个指针全部更新,指向新内存的对应位置。
这也是为什么扩容后,所有指向原 vector 的迭代器、指针、引用都会全部失效 —— 因为原内存已经被释放了。
四,高频接口
4.1,reserve和resize:
这是最容易混淆的两个接口,核心区别在于操作的对象不同:
1,reserve(n):操作容量(capacity),只预分配内存,不创建、不销毁任何元素,不会改变size()。它的唯一作用是提前申请内存,避免后续插入时频繁扩容。
2,resize(n, val):操作有效元素数量(size),会真正构造或销毁元素,改变size();如果 n 大于当前 capacity,会自动触发扩容。
举个直观的例子:
std::vector<int> v; v.reserve(10); // capacity=10,size=0,没有任何有效元素 v[0] = 1; // 错误!越界访问,未定义行为 v.resize(10); // capacity=10,size=10,有10个值为0的元素 v[0] = 1; // 正确push_back vs emplace_back:性能差异后面的文章中我会讲到。
4.2,clear()会释放内存吗?
答案是:没有。
clear()只会调用所有元素的析构函数,将size()置为 0,但底层内存不会释放,capacity()保持不变。如果 vector 曾经装过大量数据,后续不再使用,直接调用clear()会造成内存闲置。
五,vector最容易犯的错误
5.1 迭代器失效:最经典的坑
迭代器失效是 vector 开发中最常见的 bug 来源,主要有两类场景:
1,触发扩容的插入操作:所有迭代器、指针、引用全部失效;
2,中间位置的插入 / 删除:操作点之后的所有迭代器、指针、引用全部失效。
最典型的错误写法 —— 循环中删除元素:
很多人误以为reserve(n)之后就有了 n 个元素,可以直接用下标赋值,这是典型的错误。reserve只分配了内存,没有构造元素,此时size()仍然是 0,下标访问属于越界行为,会导致未定义错误。
// 错误写法:erase 后迭代器失效,后续 ++ 是未定义行为 for (auto it = v.begin(); it != v.end(); ++it) { if (*it == target) { v.erase(it); } } // 正确写法:使用 erase 返回的下一个有效迭代器 for (auto it = v.begin(); it != v.end(); ) { if (*it == target) { it = v.erase(it); } else { ++it; } }5.2,reserve后直接下标访问
很多人误以为reserve(n)之后就有了 n 个元素,可以直接用下标赋值,这是典型的错误。reserve只分配了内存,没有构造元素,此时size()仍然是 0,下标访问属于越界行为,会导致未定义错误。
5.3,忽略扩容代价,频繁 push_back
如果知道最终要插入的元素数量,却不提前reserve,会导致 vector 多次扩容、多次拷贝全量数据,在数据量大时会带来非常明显的性能损耗。
六,性能优化最佳实践
1. 预知数据量时,提前 reserve 预分配
这是性价比最高的优化手段。如果能预估元素的大致数量,提前调用reserve()一次性分配足够内存,可以彻底避免中途扩容带来的拷贝开销。
2. 避免在头部 / 中间频繁插入删除
vector 的优势在尾部操作,头部 / 中间插入需要移动大量元素。如果业务场景需要频繁在中间增删,应该优先选择std::list。
最后
vector在最常见的场景(随机访问、尾部增删)下做到了极致性能,同时兼顾易用性。理解它的底层逻辑是为了写出更高效、更安全的代码:知道什么时候该预分配,知道哪些操作会导致迭代器失效,知道它的边界在哪里,才能真正发挥 C++ 容器的威力。