C++ vector 常用接口全梳理从 push_back 到迭代器(一文搞定)
作者:xiangyun61
前言
在 C++ STL 中,vector 是最常用的容器,没有之一。它本质是一个动态数组——既能像普通数组一样通过下标高效随机访问(O(1)),又能在元素超出容量时自动扩容,完美解决了原生数组“长度固定、扩容麻烦”的痛点。
但正因为“太常用”,很多开发者对 vector 的理解停留在“会 push_back 就行”的层面,面试被追问 reserve 和 resize 的区别、迭代器为什么失效时往往答不上来。
本文是「C++ STL 源码剖析与模拟实现」专栏的第一篇,目的是帮你把 vector 的核心接口 + 易错点一次性梳理清楚。掌握这些之后,后续的模拟实现和源码剖析会轻松很多。
一、先搞懂 vector 的底层结构
vector 的底层由三个指针维护,这是理解它所有特性的基础:
template <typename T>
class vector {
private:
T* _start; // 指向数组起始位置
T* _finish; // 指向最后一个有效元素的下一个位置
T* _end_of_storage; // 指向容量末尾
};基于这三个指针,所有核心属性都可以 O(1) 算出:
| 属性 | 计算方式 | 含义 |
|---|---|---|
size() | _finish - _start | 当前有效元素个数 |
capacity() | _end_of_storage - _start | 当前可容纳的元素总数 |
empty() | _start == _finish | 是否为空 |
扩容时机:当 size() == capacity() 时,插入新元素会触发扩容。扩容过程是:申请更大的新内存 → 拷贝旧元素 → 释放旧内存 → 更新三个指针。
不同编译器的扩容倍数不同:VS (MSVC) 按 1.5 倍增长,GCC/Clang 按 2 倍增长。原因是用指数级增长可以将“扩容+拷贝”的均摊时间复杂度降到 O(1),固定增长会退化为 O(n)。
二、构造与初始化
vector 提供了多种构造方式,按使用频率排序:
| 构造方式 | 代码示例 | 说明 |
|---|---|---|
| 默认构造 | vector<int> v; | 空容器,size=0 |
| 填充构造 | vector<int> v(5, 3); | 5 个 3 |
| 拷贝构造 | vector<int> v2(v1); | 深拷贝 v1 |
| 迭代器区间 | vector<int> v3(arr, arr+5); | 从数组/容器区间构造 |
| 初始化列表 | vector<int> v4{1,2,3,4,5}; | C++11,最简洁 |
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> v1; // 空
vector<int> v2(5, 3); // 5 个 3
vector<int> v3(v2); // 拷贝
int arr[] = {10, 20, 30};
vector<int> v4(arr, arr + 3); // 区间构造
vector<int> v5{1, 2, 3, 4, 5}; // 初始化列表
cout << "v2: ";
for (auto e : v2) cout << e << " "; // 3 3 3 3 3
cout << endl;
return 0;
}三、容量管理:size、capacity、reserve、resize
这是面试最高频的考点区域,也是实战中最容易踩坑的地方。
3.1 size vs capacity
size():当前有效元素的个数capacity():当前已分配空间能容纳的元素个数
关系:0 <= size() <= capacity()
3.2 reserve vs resize(重点区分)
| 接口 | 作用 | 是否改变 size | 是否改变 capacity |
|---|---|---|---|
reserve(n) | 预分配空间 | ❌ 不变 | ✅ 至少变为 n |
resize(n) | 调整元素个数 | ✅ 变为 n | 可能变(n>capacity 时) |
一句话记忆:reserve 管“房子大小”,resize 管“住几个人”。
vector<int> v; v.reserve(10); cout << v.size() << " " << v.capacity() << endl; // 0 10(只扩空间,不加元素) v.resize(5); cout << v.size() << " " << v.capacity() << endl; // 5 10(加了 5 个默认值 0)
⚠️ 坑点:调用
reserve(n)后,不能用v[i]访问,因为size()仍为 0,元素并不存在。reserve 只保证 capacity 够用,不保证元素有效。
四、增删查改核心接口
4.1 尾插与尾删
vector<int> v; v.push_back(1); // [1] v.push_back(2); // [1, 2] v.pop_back(); // [1]
push_back 和 pop_back 是 vector 效率最高的操作(均摊 O(1))。如果已知元素总量,提前 reserve 可以避免多次扩容。
4.2 插入与删除
vector<int> v = {1, 2, 3, 4, 5};
v.insert(v.begin() + 2, 99); // {1, 2, 99, 3, 4, 5}
v.erase(v.begin() + 2); // {1, 2, 3, 4, 5}
insert 和 erase 在中间位置的时间复杂度是 O(n),因为需要搬移后续所有元素。如果频繁在中间插入删除,应考虑 std::list 或 std::deque。
4.3 元素访问
vector<int> v = {10, 20, 30};
cout << v[0] << endl; // 10(无边界检查)
cout << v.at(1) << endl; // 20(越界抛 out_of_range 异常)
cout << v.front() << endl; // 10
cout << v.back() << endl; // 30
五、三大高频避坑点
坑点 1:空 vector 不能用下标访问
这是新手最常犯的错误:
vector<int> v; // v[0] = 10; // ❌ 未定义行为!v 中没有任何元素 v.push_back(10); // ✅ 正确做法 v.resize(1); // ✅ 或先 resize v[0] = 10;
operator[] 不做边界检查,越界访问是未定义行为,可能崩溃也可能“看似正常运行”,非常隐蔽。
坑点 2:迭代器失效(面试必考)
迭代器失效的本质是:迭代器底层指向的内存空间被释放或移动了,继续使用会导致程序崩溃或逻辑错误。
三种典型失效场景:
场景一:扩容导致全失效
vector<int> v = {1, 2, 3};
auto it = v.begin();
v.push_back(4); // 可能触发扩容,旧内存被释放
// cout << *it; // ❌ it 已失效!
场景二:insert 导致失效
vector<int> v = {1, 2, 3, 4, 5};
auto it = v.begin() + 2;
v.insert(it, 99); // insert 后 it 可能失效
// cout << *it; // ❌ 不要再用 it
it = v.begin() + 3; // ✅ 重新获取
场景三:erase 导致后续迭代器失效
vector<int> v = {1, 2, 3, 4, 5};
// 错误写法
for (auto it = v.begin(); it != v.end(); ++it) {
if (*it == 3) v.erase(it); // ❌ erase 后 it 失效,++it 崩溃
}
// 正确写法
for (auto it = v.begin(); it != v.end(); ) {
if (*it == 3) it = v.erase(it); // erase 返回下一个有效位置
else ++it;
}核心原则:任何可能改变 vector 底层内存的操作(push_back、insert、erase、resize、reserve)之后,之前获取的迭代器、指针、引用都可能失效,必须重新获取。
坑点 3:resize 的默认值陷阱
vector<int> v(3, 100); // {100, 100, 100}
v.resize(5); // {100, 100, 100, 0, 0} ← 新元素默认初始化为 0
v.resize(2); // {100, 100} ← 截断
resize(n) 增大时,新元素会被值初始化(int 为 0,string 为空串)。如果存储的是自定义类型,需要有默认构造函数。
六、完整代码示例
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
void printVector(const vector<int>& v, const string& name) {
cout << name << ": [";
for (size_t i = 0; i < v.size(); i++) {
cout << v[i] << (i < v.size() - 1 ? ", " : "");
}
cout << "] size=" << v.size() << " capacity=" << v.capacity() << endl;
}
int main() {
// 1. 构造
vector<int> v{1, 2, 3, 4, 5};
printVector(v, "初始");
// 2. 尾插
v.push_back(6);
printVector(v, "push_back(6)");
// 3. 插入
v.insert(v.begin() + 2, 99);
printVector(v, "insert(索引2, 99)");
// 4. 删除
v.erase(v.begin() + 2);
printVector(v, "erase(索引2)");
// 5. reserve 预分配
v.reserve(20);
printVector(v, "reserve(20)");
// 6. 安全遍历删除偶数
for (auto it = v.begin(); it != v.end(); ) {
if (*it % 2 == 0) it = v.erase(it);
else ++it;
}
printVector(v, "删除所有偶数");
// 7. 排序
sort(v.begin(), v.end());
printVector(v, "排序后");
return 0;
}总结
| 要点 | 核心结论 |
|---|---|
| 底层结构 | 三个指针:_start / _finish / _end_of_storage |
| 扩容 | VS 1.5 倍,GCC 2 倍,均摊 O(1) |
| reserve vs resize | reserve 只扩容量,resize 改变元素个数 |
| 尾插 | push_back 均摊 O(1),建议提前 reserve |
| 中间插入删除 | insert/erase 为 O(n),频繁使用考虑换容器 |
| 迭代器失效 | 扩容/insert/erase 后必须重新获取迭代器 |
| 下标访问 | operator[] 无边界检查,空 vector 不能用下标 |
以上就是 vector 核心接口的全梳理。下一篇文章,我们将从零开始模拟实现 vector 的底层,从三个指针的设计讲起,手写构造、析构、push_back 和扩容逻辑——理解了实现,源码就不再神秘。
到此这篇关于C++ vector 常用接口全梳理:从 push_back 到迭代器,一篇就够了的文章就介绍到这了,更多相关C++ vector 常用接口内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
