在 C++ STL 容器体系中,vector 是使用频率最高的序列式容器,没有之一。它本质上是一个支持动态扩容的数组,相比原生数组,它封装了自动化的内存管理能力,提供了丰富的成员接口,同时完美保留了数组 O (1) 时间复杂度的随机访问特性。
一、vector 基础用法:核心接口全掌握
vector 的接口繁多,但日常开发与面试中,我们只需重点掌握核心接口即可。下面我们按功能模块,拆解 vector 的核心用法。
1.1 构造函数:4 种核心构造方式
| 构造函数接口 | 功能说明 |
|---|---|
vector() |
无参构造,创建空的 vector 对象(最常用) |
vector(size_type n, const value_type& val = value_type()) |
构造并初始化 n 个值为 val 的元素 |
vector(const vector& x) |
拷贝构造,用已有 vector 对象创建新对象 |
vector(InputIterator first, InputIterator last) |
迭代器区间构造,用其他容器的区间初始化 vector |
| 代码示例: |
cpp
#include <vector>
#include <iostream>
using namespace std;
int main() {
// 1. 无参构造
vector<int> v1;
// 2. 构造5个值为10的元素
vector<int> v2(5, 10);
// 3. 拷贝构造
vector<int> v3(v2);
// 4. 迭代器区间构造
int arr[] = {1, 2, 3, 4, 5};
vector<int> v4(arr, arr + sizeof(arr)/sizeof(int));
// C++11 拓展:列表初始化
vector<int> v5 = {1, 2, 3, 4, 5};
return 0;
}
1.2 迭代器:vector 遍历的核心工具
迭代器是 STL 的核心设计,它让算法无需关心容器的底层数据结构,而 vector 的迭代器本质就是原生指针T*,支持随机访问。
核心迭代器接口分为两组:
1.正向迭代器:begin() + end()
begin():返回第一个元素的迭代器
end():返回最后一个元素的下一个位置的迭代器(左闭右开区间)
2.反向迭代器:rbegin() + rend()
rbegin():返回最后一个元素的反向迭代器
rend():返回第一个元素前一个位置的反向迭代器
三种遍历方式代码示例:
cpp
vector<int> v = {1, 2, 3, 4, 5};
// 1. 迭代器遍历
vector<int>::iterator it = v.begin();
while (it != v.end()) {
cout << *it << " ";
++it;
}
cout << endl;
// 2. 下标[]遍历(vector最常用的遍历方式)
for (size_t i = 0; i < v.size(); ++i) {
cout << v[i] << " ";
}
cout << endl;
// 3. C++11 范围for遍历(底层封装了迭代器)
for (auto e : v) {
cout << e << " ";
}
cout << endl;
1.3 空间管理:吃透容量与大小的核心接口
vector 的空间管理是其核心特性,也是面试高频考点,核心接口如下:
| 接口 | 功能说明 |
|---|---|
size() |
获取 vector 中有效元素的个数 |
capacity() |
获取 vector 当前的容量(可容纳元素的最大个数) |
empty() |
判断 vector 是否为空 |
resize(size_t n, val = value_type()) |
改变 vector 的有效元素个数 size |
reserve(size_t n) |
改变 vector 的容量 capacity |
核心考点 1:vector 的扩容机制
vector 作为动态数组,当插入元素时如果size() == capacity(),就会触发自动扩容。不同编译器的 STL 实现,扩容策略不同:
VS 编译器(PJ 版 STL):按1.5 倍扩容
g++ 编译器(SGI 版 STL):按2 倍扩容
扩容的完整流程:开辟新的更大内存空间 → 将旧空间的元素拷贝到新空间 → 释放旧内存空间 → 更新迭代器指针。这也是为什么扩容会导致迭代器失效的根本原因。
cpp
void TestVectorExpand() {
size_t sz;
vector<int> v;
sz = v.capacity();
cout << "making vector grow:\n";
for (int i = 0; i < 100; ++i) {
v.push_back(i);
if (sz != v.capacity()) {
sz = v.capacity();
cout << "capacity changed: " << sz << '\n';
}
}
}
核心考点 2:resize 与 reserve 的核心区别
这是面试必问的知识点,二者的核心差异如下:
1.reserve :只操作容量 capacity,不改变有效元素个数 size,不会创建 / 初始化对象,仅负责提前开辟空间。如果 n 小于当前 capacity,该函数在多数编译器下不会做任何操作。
2.resize :直接改变有效元素个数 size,同时会初始化对象。如果 n 大于当前 capacity,会先扩容再初始化;如果 n 小于当前 size,会析构掉超出的元素。
性能优化技巧:如果提前知道 vector 需要存储的元素个数,使用reserve()提前开辟足够空间,可以避免频繁扩容带来的元素拷贝、内存释放的性能损耗。
cpp
// 优化版:提前预分配空间,避免100次插入过程中的多次扩容
void TestVectorExpandOP() {
vector<int> v;
v.reserve(100); // 提前将容量设置为100
size_t sz = v.capacity();
cout << "after reserve, capacity: " << sz << '\n';
for (int i = 0; i < 100; ++i) {
v.push_back(i);
if (sz != v.capacity()) {
sz = v.capacity();
cout << "capacity changed: " << sz << '\n';
}
}
}
1.4 增删查改:高频操作接口
vector 的增删查改接口中,尾插尾删是最高效的(O (1)),而随机位置的插入删除需要移动元素,时间复杂度为 O (n),核心接口如下:
1.4 增删查改:高频操作接口
| 接口 | 功能说明 |
|---|---|
push_back(const T& val) |
尾插元素(最常用) |
pop_back() |
尾删元素 |
insert(iterator pos, const T& val) |
在 pos 迭代器位置前插入元素 val |
erase(iterator pos) |
删除 pos 迭代器位置的元素,返回下一个元素的迭代器 |
swap(vector& x) |
交换两个 vector 的底层数据空间(O (1) 时间复杂度) |
operator[] |
像数组一样通过下标随机访问元素 |
| 注意:vector 没有提供成员函数find,查找元素需要使用 STL 算法库的find函数,需要包含头文件。 |
cpp
#include <algorithm>
int main() {
vector<int> v = {1, 2, 3, 4};
// 尾插、尾删
v.push_back(5); // v: 1 2 3 4 5
v.pop_back(); // v: 1 2 3 4
// 插入:在开头插入0
v.insert(v.begin(), 0); // v: 0 1 2 3 4
// 查找:找到3的位置
vector<int>::iterator pos = find(v.begin(), v.end(), 3);
// 删除:删除3这个元素
if (pos != v.end()) {
v.erase(pos);
} // v: 0 1 2 4
// 下标访问修改
v[0] = 10; // v: 10 1 2 4
// 交换两个vector
vector<int> v2(5, 100);
v.swap(v2);
return 0;
}
二、vector 核心坑点:迭代器失效全解
迭代器失效是 vector 使用中最容易踩的坑,也是面试的绝对高频考点。我们需要彻底搞懂:什么是迭代器失效、哪些场景会导致失效、如何解决失效问题。
2.1 什么是迭代器失效?
vector 的迭代器底层本质就是原生指针T*,迭代器失效,本质就是迭代器对应的底层指针指向的内存空间被销毁,或者指针指向的位置已经不符合容器的逻辑状态。继续使用已经失效的迭代器,会导致未定义行为:轻则程序运行结果错误,重则直接程序崩溃。
2.2 迭代器失效的两大核心场景
场景 1:底层空间重新分配,导致原迭代器全部失效
所有可能触发 vector 扩容的操作,都会导致原迭代器全部失效,包括: resize、reserve、insert、push_back、assign等。
失效原因 :扩容会释放旧的内存空间,开辟新的内存空间,而原来的迭代器依然指向已经被释放的旧空间,变成了野指针,继续访问就会导致程序崩溃。
错误代码示例:
cpp
int main() {
vector<int> v = {1,2,3,4,5};
auto it = v.begin();
// 触发扩容,旧空间被释放,it失效
v.reserve(100);
// 错误:使用已经失效的迭代器it,vs下直接崩溃,g++下结果错误
while(it != v.end()) {
cout << *it << " ";
++it;
}
return 0;
}
解决方案:扩容操作完成后,重新给迭代器赋值,重新获取begin()和end()。
场景 2:erase 删除元素,导致指定位置迭代器失效
erase 删除元素后,pos 位置之后的元素会整体向前移动,底层空间并没有发生改变,理论上迭代器不会完全失效,但标准规定:erase 会导致指向被删除元素及其之后的所有迭代器失效。
这里有两个典型的坑:
1.删除元素后,直接访问原迭代器,导致非法访问;
2.循环删除元素时,直接对迭代器 ++,导致元素跳过甚至程序崩溃。
经典错误案例:删除 vector 中所有偶数
cpp
// 错误版本
int main() {
vector<int> v = { 1, 2, 3, 4 };
auto it = v.begin();
while (it != v.end()) {
if (*it % 2 == 0) {
v.erase(it); // 这里it已经失效
}
++it; // 失效的迭代器++,导致未定义行为
}
return 0;
}
错误原因 :
当 erase 删除 it 指向的元素后,it 已经失效,后续对 it 的 ++ 操作是非法的;
即使编译器不崩溃,也会导致元素跳过:比如删除 2 之后,3 前移到 2 的位置,it++ 直接指向 4,跳过了 3 的检查。
cpp
// 正确版本
int main() {
vector<int> v = { 1, 2, 3, 4 };
auto it = v.begin();
while (it != v.end()) {
if (*it % 2 == 0) {
// erase返回被删除元素的下一个位置的迭代器,更新it
it = v.erase(it);
} else {
// 没有删除元素,才对迭代器++
++it;
}
}
return 0;
}
2.3 不同编译器的行为差异
VS(PJ 版 STL) :对迭代器失效的检查非常严格,只要迭代器触发了失效场景,继续使用就会直接断言崩溃;
g++(SGI 版 STL) :对迭代器失效的检测比较宽松,非扩容的 erase 操作后,迭代器可能还能正常运行,但尾元素被删除后,迭代器指向 end (),再 ++ 必然会段错误。
重要提醒:绝对不能依赖编译器的宽松检查,必须严格按照 C++ 标准编写代码,主动规避迭代器失效问题,否则代码跨平台运行时会出现难以排查的 bug。
2.4 迭代器失效的通用解决方案
1.任何可能触发扩容的操作后,重新给迭代器赋值,重新获取begin()和end();
2.erase 删除元素时,用 erase 的返回值更新迭代器,禁止使用已删除位置的迭代器;
3.循环中修改 vector 结构时,优先使用下标访问,规避迭代器失效问题。
补充:与 vector 类似,string 在插入、扩容、erase 操作后,同样会发生迭代器失效,解决方案完全一致。