vector学习总结
目录
vector 学习总结
std::vector 是 C++ 标准库中最常用的顺序容器,本文从零到熟练梳理它的所有核心用法。
1. vector 是什么
- 存储连续的元素序列,支持随机访问(按下标 O(1) 访问)。
- 底层是动态数组:在堆上分配一块连续内存,元素紧密排列。
- 自动扩容:元素增多时自动重新分配更大内存(O(n) 均摊插入)。
- 属于"序列容器",头文件
<vector>。
#include <vector>
vector<int> v; // 空 vector,int 类型
对比记忆:vector 是"动态数组",内存连续、访问快、尾部增删快;list 是"双向链表",任意位置增删快但不能按下标访问;map 是"红黑树",按 key 有序存储键值对。
2. 定义与初始化
#include <vector>
#include <string>
using namespace std;
// 1. 空 vector
vector<int> v1;
// 2. 指定大小,元素值初始化为 0
vector<int> v2(5); // 5 个元素,都是 0
vector<int> v3(5, 42); // 5 个元素,都是 42
// 3. 初始化列表
vector<int> v4{1, 2, 3, 4, 5};
// 4. 从其他容器构造
string s = "hello";
vector<char> v5(s.begin(), s.end()); // {'h','e','l','l','o'}
// 5. 拷贝构造
vector<int> v6(v4);
// 6. 移动构造(C++11)
vector<int> v7(std::move(v4)); // v4 被掏空
// 7. resize / reserve
vector<int> v8;
v8.resize(10); // size 变为 10,新元素默认初始化
v8.reserve(100); // capacity 变为 100,size 不变
关键概念:
- size:当前元素个数
- capacity:已分配内存能容纳的最大元素个数
reserve()只改 capacity,不改 size;resize()两者都改
3. 核心操作一览
| 操作 | 写法 | 说明 | 复杂度 |
|---|---|---|---|
| 尾部插入 | v.push_back(x) |
在末尾添加元素 | O(1) 均摊 |
| 尾部插入 | v.emplace_back(args...) |
C++11,原地构造,避免拷贝 | O(1) 均摊 |
| 中间插入 | v.insert(it, x) |
在迭代器 it 前插入,后续元素后移 | O(n) |
| 尾部删除 | v.pop_back() |
删除最后一个元素 | O(1) |
| 中间删除 | v.erase(it) |
删除迭代器 it 处元素,返回下一个迭代器 | O(n) |
| 范围删除 | v.erase(first, last) |
删除 [first, last) 范围内元素 | O(n) |
| 按下标访问 | v[i] |
不检查越界 | O(1) |
| 按下标访问 | v.at(i) |
越界抛 out_of_range 异常 |
O(1) |
| 首元素 | v.front() |
返回第一个元素的引用 | O(1) |
| 尾元素 | v.back() |
返回最后一个元素的引用 | O(1) |
| 指针 | v.data() |
返回底层数组指针 | O(1) |
| 大小 | v.size() |
当前元素个数 | O(1) |
| 容量 | v.capacity() |
当前分配的内存容量 | O(1) |
| 预留空间 | v.reserve(n) |
至少分配 n 个元素的空间 | O(n) 最坏 |
| 调整大小 | v.resize(n) |
改变 size,多删少补 | O(n) 最坏 |
| 收缩空间 | v.shrink_to_fit() |
使 capacity == size(非强制) | O(n) |
| 清空 | v.clear() |
删除所有元素,size=0,capacity 不变 | O(n) |
| 判空 | v.empty() |
size == 0 时返回 true | O(1) |
| 交换 | v.swap(other) |
与另一个 vector 交换内容 O(1) | O(1) |
4. 增删查改实战
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
vector<int> v;
// ===== 插入 =====
v.push_back(10);
v.push_back(20);
v.push_back(30);
v.emplace_back(40); // 原地构造
// 在中间插入
auto it = v.begin() + 2;
v.insert(it, 25); // v: {10, 20, 25, 30, 40}
// 插入多个相同元素
v.insert(v.begin(), 3, 0); // v: {0, 0, 0, 10, 20, 25, 30, 40}
// ===== 访问 =====
cout << v[0] << endl; // 0
cout << v.at(4) << endl; // 20
cout << v.front() << endl; // 0
cout << v.back() << endl; // 40
// ===== 查找(vector 没有内置 find,用 std::find) =====
auto pos = find(v.begin(), v.end(), 25);
if (pos != v.end()) {
cout << "Found at index: " << (pos - v.begin()) << endl; // 6
}
// ===== 遍历 =====
// 1. 下标遍历
for (int i = 0; i < v.size(); i++) {
cout << v[i] << " ";
}
cout << endl;
// 2. 迭代器遍历
for (auto it = v.begin(); it != v.end(); ++it) {
cout << *it << " ";
}
cout << endl;
// 3. 范围 for(C++11)
for (auto& x : v) {
cout << x << " ";
}
cout << endl;
// ===== 删除 =====
v.erase(v.begin()); // 删除首元素
v.pop_back(); // 删除尾元素
v.erase(v.begin(), v.begin() + 2); // 删除前两个
v.clear(); // 清空所有元素
cout << v.size() << endl; // 0
return 0;
}
5. reserve() vs resize()(重点)
这是面试高频考点,两者本质不同:
vector<int> v;
v.reserve(10);
// size = 0, capacity = 10, 没有任何元素
// v[0] 是未定义行为!
v.resize(10);
// size = 10, capacity >= 10, 10 个值初始化为 0 的元素
cout << v[0]; // 合法,输出 0
reserve(n) |
resize(n) |
|
|---|---|---|
| 改变 size | 不改 | 改 |
| 改变 capacity | 改 | 可能改 |
| 元素个数 | 不变 | 多删少补 |
| 用途 | 预分配内存,避免频繁扩容 | 改变元素个数 |
坑:reserve 后直接用下标访问未初始化的内存 → 未定义行为!
vector<int> v;
v.reserve(100);
for (int i = 0; i < 100; i++) {
v[i] = i; // 未定义行为!size 还是 0
}
// 正确做法:
for (int i = 0; i < 100; i++) {
v.push_back(i); // 每次 push_back 才会真正构造元素
}
// 或者先 resize
v.resize(100);
for (int i = 0; i < 100; i++) {
v[i] = i; // 合法,元素已构造
}
6. 扩容机制:size 与 capacity 的关系
vector 扩容时会分配一块更大的内存,把旧元素拷贝过去,再释放旧内存:
size: 0 1 2 3 4 5 6 7 8 ...
capacity: 0 1 2 4 4 8 8 8 8 16 ...
- 每次 push_back 导致 size > capacity 时触发扩容。
- 扩容倍数由实现决定,通常是 2 倍(GCC)或 1.5 倍(MSVC)。
- 扩容代价大:O(n) 拷贝 + 内存分配,所以批量插入前用
reserve()预分配。
vector<int> v;
v.reserve(1000); // 预分配,后续 1000 次 push_back 不触发扩容
for (int i = 0; i < 1000; i++) {
v.push_back(i); // 全部 O(1) 均摊,无重新分配
}
7. 迭代器的特点
- vector 的迭代器是随机访问迭代器:支持
++、--、it + n、it - n、it1 - it2、下标比较。 - 这是最强大的迭代器类型,可以配合任何 STL 算法。
迭代器失效规则(重点):
| 操作 | 失效情况 |
|---|---|
push_back / emplace_back |
可能导致全部迭代器失效(扩容时) |
pop_back |
尾迭代器失效 |
insert / erase |
插入点及之后的所有迭代器失效 |
reserve / resize / shrink_to_fit |
全部迭代器失效 |
clear |
全部迭代器失效 |
// 危险:边遍历边 insert 可能死循环
for (auto it = v.begin(); it != v.end(); ++it) {
if (*it == 5) {
v.insert(it, 10); // it 失效!后续 ++it 未定义行为
}
}
// 正确做法:insert 返回新的有效迭代器
for (auto it = v.begin(); it != v.end(); ) {
if (*it == 5) {
it = v.insert(it, 10); // 用返回值更新
++it; // 跳过新插入的元素
++it; // 跳过原元素
} else {
++it;
}
}
// erase 的正确用法:erase 返回下一个有效迭代器
for (auto it = v.begin(); it != v.end(); ) {
if (*it == 5) {
it = v.erase(it); // 返回下一个
} else {
++it;
}
}
8. vector 与 array、deque、list 的对比
vector |
array |
deque |
list |
|
|---|---|---|---|---|
| 底层 | 动态数组 | 静态数组 | 分段数组 | 双向链表 |
| 内存连续 | 是 | 是 | 否(分段连续) | 否 |
| 随机访问 | O(1) | O(1) | O(1) | 不支持 |
| 尾部增删 | O(1) 均摊 | N/A(固定大小) | O(1) | O(1) |
| 头部增删 | O(n) | N/A | O(1) | O(1) |
| 中间增删 | O(n) | N/A | O(n) | O(1)(已知位置) |
| 扩容 | 自动 | 不可 | 两端增长 | 不需要 |
| 内存碎片 | 小 | 无 | 中等 | 大(每节点独立分配) |
选择建议:
- 默认选择 vector:大多数场景 vector 都是最快的(缓存友好)。
- 频繁头部增删 →
deque。 - 频繁中间增删且不需要随机访问 →
list。 - 固定大小 →
array(栈上分配,零开销)。
9. 用 vector 模拟栈和队列
#include <vector>
#include <stack>
#include <queue>
using namespace std;
// 用 vector 模拟栈(最常用)
vector<int> stk;
stk.push_back(1); // 入栈
stk.push_back(2);
stk.pop_back(); // 出栈
int top = stk.back(); // 栈顶
// 用 vector 模拟队列(不推荐,O(n) dequeue)
vector<int> q;
q.push_back(1); // 入队
q.erase(q.begin()); // 出队 O(n),慢
// 建议用 deque 或 queue 代替
10. 常见坑汇总
reserve后用下标访问:size 仍是 0,元素未构造,未定义行为。push_back导致迭代器失效:扩容时所有迭代器失效,不能在循环中依赖旧迭代器。insert/erase后使用旧迭代器:插入/删除点及之后的迭代器全部失效。- 在循环中
push_back可能无限扩容:循环前用reserve预分配。 vector<bool>不是真正的 vector:它是位压缩的特殊实现,operator[]返回代理对象而非bool&。如果需要位集合,用bitset。pop_back不返回被删元素:需要先back()再pop_back()。clear不释放内存:size 变 0 但 capacity 不变。想释放内存用shrink_to_fit()或 swap 技巧:
vector<int> v(1000000);
v.clear(); // size=0, capacity 还是 1000000,内存未释放
// 释放内存的技巧
vector<int>().swap(v); // 临时空 vector 和 v 交换,v 的内存被释放
// 或 C++20
v.shrink_to_fit();
insert在尾部插入和push_back效果一样但更慢:尾部插入优先用push_back/emplace_back。- 误以为
vector是线程安全的:多线程读写同一个 vector 需要加锁。
11. emplace_back vs push_back(C++11)
vector<pair<int, string>> v;
// push_back:先构造临时对象,再移动/拷贝到 vector
v.push_back({1, "hello"}); // 构造 pair → 移动到 vector
// emplace_back:直接在 vector 的内存上构造,零拷贝
v.emplace_back(1, "hello"); // 直接用 (1, "hello") 构造 pair
经验法则:能用 emplace_back 就用 emplace_back,性能更好。
struct Expensive {
string data;
Expensive(const string& s) : data(s) {
cout << "构造" << endl;
}
Expensive(const Expensive& other) : data(other.data) {
cout << "拷贝" << endl;
}
Expensive(Expensive&& other) : data(std::move(other.data)) {
cout << "移动" << endl;
}
};
vector<Expensive> v;
v.push_back(Expensive("test")); // 构造 + 移动
v.emplace_back("test"); // 只构造,无拷贝无移动
12. 实战:动态数组管理(vector 最经典的场景)
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
using namespace std;
int main() {
// ===== 场景:成绩管理 =====
vector<int> scores;
// 批量录入
scores.reserve(100); // 预分配
for (int i = 0; i < 10; i++) {
scores.push_back(60 + rand() % 41); // 60~100 随机分数
}
// 排序
sort(scores.begin(), scores.end());
// 统计
double avg = accumulate(scores.begin(), scores.end(), 0.0) / scores.size();
cout << "最高分: " << scores.back() << endl;
cout << "最低分: " << scores.front() << endl;
cout << "平均分: " << avg << endl;
// 去重(先排序)
scores.erase(unique(scores.begin(), scores.end()), scores.end());
// 删除不及格的成绩
scores.erase(
remove_if(scores.begin(), scores.end(), [](int s) { return s < 60; }),
scores.end()
);
// 遍历输出
for (auto [i, s] : enumerate(scores)) { // C++23 enumerate
cout << s << " ";
}
cout << endl;
return 0;
}
13. 实战场景
给定一个需求:需要存储一组动态增长的数据,支持按下标快速访问,同时需要频繁在尾部增删。
方法一:使用std::vector建模,内存连续、缓存友好、按下标O(1)访问、尾部增删O(1)均摊。
方法二:使用std::deque建模,支持头部O(1)增删、尾部O(1)增删,但内存分段、缓存不如vector。
方法三:使用std::list建模,任意位置O(1)增删,但不支持随机访问、内存开销大。
总结:
1、默认选择 vector,连续内存缓存命中率最高,大多数场景最快
2、频繁头部增删选择 deque
3、频繁中间增删且不需要随机访问选择 list
4、固定大小选择 array(栈上分配,零开销)
5、批量插入前用 reserve 预分配内存,避免频繁扩容的拷贝开销
6、vector 的迭代器是随机访问迭代器,支持 it+n 这种跳跃操作,而 list 和 map 的迭代器只支持 ++/--
另外 vector 使用连续存储,遍历速度远快于 list(链表跳内存)和 map(红黑树跳内存)

浙公网安备 33010602011771号