6、vector容器(上):vector的构造与赋值、vector的容量与大小、vector的插入与删除
好,咱们今天来聊聊 vector。说实话,vector 是我在 C++ 里用得最频繁的容器,没有之一。无论是刷算法题还是写业务代码,它几乎无处不在。你想想看,一个能自动扩容的动态数组,谁不爱呢?
不过,用归用,很多人其实没搞懂它的底层机制。比如「容量」和「大小」到底有什么区别?为什么插入元素有时候会特别慢?这些问题,咱们今天一次性讲清楚。
6.1 vector的构造与赋值
先说说构造。vector 提供了好几种构造方式,我平时最常用的是默认构造和带初始大小的构造。
#include <vector>
#include <iostream>
using namespace std;
int main() {
// 1. 默认构造 —— 空容器
vector<int> v1;
// 2. 带参构造 —— 10个元素,每个都是5
vector<int> v2(10, 5);
// 3. 拷贝构造
vector<int> v3(v2);
// 4. 迭代器范围构造
int arr[] = {1, 2, 3, 4, 5};
vector<int> v4(arr, arr + 5);
// 5. 列表初始化(C++11起)
vector<int> v5 = {1, 2, 3, 4, 5};
return 0;
}
我个人习惯用列表初始化,代码看着清爽。不过要注意,如果你写 vector<int> v(10),它创建的是 10 个默认初始化的元素(int 就是 0),而不是一个容量为 10 的空 vector。这个坑我见过不少人踩过。
赋值方面,除了直接用 = 操作符,还有个 assign 成员函数。它可以把容器里的内容整个替换掉。
vector<int> v;
v.assign(5, 10); // 现在v里有5个10
v.assign({1, 2, 3}); // 现在v里有1,2,3
6.2 vector的容量与大小
这是很多初学者容易混淆的地方。我简单说一下:
- size():当前容器里有多少个元素
- capacity():当前容器在不重新分配内存的情况下,最多能装多少个元素
- empty():判断是否为空
- max_size():系统允许的最大元素数量(基本用不到)
为什么会有 capacity 这个概念?因为 vector 底层是一块连续内存。如果每次添加元素都重新分配内存,性能会非常差。所以 vector 会预先多申请一些空间。
举个例子:
vector<int> v;
cout << "size: " << v.size() << ", capacity: " << v.capacity() << endl;
// 输出: size: 0, capacity: 0
v.push_back(1);
cout << "size: " << v.size() << ", capacity: " << v.capacity() << endl;
// 输出: size: 1, capacity: 1
v.push_back(2);
cout << "size: " << v.size() << ", capacity: " << v.capacity() << endl;
// 输出: size: 2, capacity: 2
v.push_back(3);
cout << "size: " << v.size() << ", capacity: " << v.capacity() << endl;
// 输出: size: 3, capacity: 4 ← 注意这里!
看到了吗?插入第 3 个元素时,capacity 直接翻倍到了 4。这就是 vector 的扩容策略——通常是 1.5 倍或 2 倍增长,具体看编译器实现。
reserve() 预分配空间,能避免多次扩容。
resize() 和 reserve() 的区别,我再说一遍:
resize(n):改变 size 为 n。如果 n 大于当前 size,新元素用默认值填充;如果 n 小于当前 size,末尾元素被丢弃。reserve(n):改变 capacity 至少为 n。它不改变 size,只预留空间。
我曾经在项目中遇到过一个性能问题:循环里不断 push_back 几万个元素,程序跑得特别慢。后来加了一行 v.reserve(50000),速度直接提升了十几倍。嗯,这就是预分配的魅力。
6.3 vector的插入与删除
插入和删除操作,是 vector 的「软肋」。因为 vector 是连续存储的,在中间插入或删除元素,需要移动后面的所有元素。
| 操作 | 函数 | 时间复杂度 | 说明 |
|---|---|---|---|
| 尾部插入 | push_back(val) | O(1) 均摊 | 最常用的插入方式 |
| 尾部删除 | pop_back() | O(1) | 只删除,不返回元素 |
| 任意位置插入 | insert(pos, val) | O(n) | pos 是迭代器位置 |
| 任意位置删除 | erase(pos) | O(n) | 返回下一个元素的迭代器 |
| 清空 | clear() | O(n) | size 变 0,capacity 不变 |
来看代码:
vector<int> v = {1, 2, 3, 4, 5};
// 尾部插入
v.push_back(6); // v: 1,2,3,4,5,6
// 尾部删除
v.pop_back(); // v: 1,2,3,4,5
// 在开头插入 0
v.insert(v.begin(), 0); // v: 0,1,2,3,4,5
// 删除第 3 个元素(索引 2)
v.erase(v.begin() + 2); // v: 0,1,3,4,5
// 删除区间 [1, 3)
v.erase(v.begin() + 1, v.begin() + 3); // v: 0,4,5
// 清空
v.clear(); // v: empty, capacity 不变
另外,pop_back() 只删除元素,不返回它。如果你想拿到最后一个元素再删除,可以先 v.back() 再 v.pop_back()。
说到 clear,它只把 size 置为 0,capacity 保持不变。也就是说,内存并没有还给系统。如果你确定不再需要这块内存,可以用 vector<int>().swap(v) 这个技巧来释放。
vector<int> v(1000, 1);
v.clear();
cout << v.capacity(); // 输出 1000,内存还在
// 释放内存
vector<int>().swap(v);
cout << v.capacity(); // 输出 0
这个 swap 技巧说白了就是:用一个空 vector 和当前 vector 交换内部指针,原来的大块内存就被析构掉了。嗯,有点 hack,但确实好用。
好了,vector 的上半部分就讲到这里。记住一句话:尾部操作是 vector 的强项,中间操作是它的软肋。如果你需要在中间频繁插入删除,那可能要考虑 list 或 deque 了。