一、为什么 vector 是"最强容器"?
在 C++ 标准模板库(STL)中,vector 是一个动态数组。它与普通数组的区别在于:
-
大小可以自动增长(不需要手动
realloc)。 -
支持随机访问(
[]运算符,O(1) 时间)。 -
在尾部增删元素效率高(均摊 O(1))。
-
提供丰富的成员函数,如
push_back、pop_back、insert、erase等。
学习 STL 有三个境界:能用 → 明理 → 能扩展。本文会帮你至少达到第二个境界,并向第三个境界迈进。
二、vector 的基础使用
使用 vector 需要包含 <vector> 头文件,并引入 std 命名空间。
2.1 构造方式
#include <vector>
using namespace std;
int main() {
vector<int> v1; // 空 vector
vector<int> v2(5, 10); // 5 个元素,每个都是 10
vector<int> v3(v2); // 拷贝构造
vector<int> v4(v2.begin(), v2.end()); // 迭代器区间构造
int arr[] = {1,2,3,4};
vector<int> v5(arr, arr+4); // 使用数组构造
return 0;
}
2.2 迭代器与遍历
vector 支持随机访问迭代器,常用方式有三种:
vector<int> v = {1, 2, 3, 4, 5};
// 1. 下标 operator[]
for (size_t i = 0; i < v.size(); ++i)
cout << v[i] << " ";
// 2. 迭代器
for (vector<int>::iterator it = v.begin(); it != v.end(); ++it)
cout << *it << " ";
// 3. C++11 范围 for
for (auto e : v)
cout << e << " ";
2.3 容量相关:size、capacity、resize、reserve
这是 vector 新手最容易混淆的地方。
-
size():当前实际存储的元素个数。 -
capacity():当前分配的内存能容纳的元素个数(capacity >= size)。 -
resize(n, val):改变size为n。若n > size,则用val填充(默认用0或默认构造);若n < size,则截断。它会影响size,也可能改变capacity。 -
reserve(n):预留至少n个元素的空间(改变capacity但不改变size)。常用于提前知道元素数量,避免多次扩容。vector
v;
v.reserve(100); // 容量至少 100,size 仍为 0
for (int i = 0; i < 100; ++i)
v.push_back(i); // 不会触发扩容
cout << v.size() << " " << v.capacity() << endl; // 100 至少100
2.4 扩容机制:1.5 倍还是 2 倍?
不同 STL 实现的扩容策略不同,这是一个常考的点。
-
VS (微软) :按 1.5 倍 扩容。
-
g++ (Linux / SGI STL) :按 2 倍 扩容。
测试代码:
vector<int> v;
size_t sz = v.capacity();
for (int i = 0; i < 100; ++i) {
v.push_back(i);
if (sz != v.capacity()) {
sz = v.capacity();
cout << "capacity changed: " << sz << endl;
}
}
VS 输出示例:1, 2, 3, 4, 6, 9, 13, 19, 28, 42, 63, 94, 141 ...
g++ 输出:1, 2, 4, 8, 16, 32, 64, 128 ...
结论 :不要迷信"2 倍"这个说法。编写可移植代码时,不要假设扩容因子。需要高效时,主动使用
reserve。
三、增删查改操作
| 函数 | 作用 |
|---|---|
push_back(val) |
尾插 |
pop_back() |
尾删 |
insert(pos, val) |
在迭代器 pos 前插入 val |
erase(pos) |
删除迭代器 pos 处的元素 |
find(begin, end, val) |
算法中的查找,不是 vector 成员 |
swap(vec) |
交换两个 vector 的数据 |
operator[] |
随机访问 |
示例:
vector<int> v = {1, 2, 3};
v.push_back(4); // 1 2 3 4
v.pop_back(); // 1 2 3
v.insert(v.begin(), 0); // 0 1 2 3
v.erase(v.begin() + 1); // 0 2 3
四、迭代器失效 ------ 最容易踩的坑
迭代器本质是一个指针(或封装后的指针),指向容器中的某个元素。当容器的内存布局发生变化时,旧的迭代器可能指向无效内存,称为迭代器失效。
4.1 会导致扩容的操作(insert、push_back、reserve、resize、assign)
这些操作可能重新分配内存,使得原有的迭代器全部失效。
vector<int> v{1,2,3};
auto it = v.begin();
v.reserve(100); // 扩容,it 失效
// 此时 it 已经不安全,不能再使用
while (it != v.end()) { // 错误!可能崩溃
cout << *it;
}
正确做法:在可能导致扩容的操作后,重新获取迭代器。
it = v.begin(); // 重新赋值
4.2 erase 导致的失效
erase 删除元素后,被删除元素及其之后的所有迭代器都会失效(因为元素发生了移动)。典型的错误写法:
vector<int> v{1,2,3,4};
auto it = v.begin();
while (it != v.end()) {
if (*it % 2 == 0)
v.erase(it); // 错误:erase 后 it 失效,再 ++it 就是野指针
++it;
}
正确写法 :利用 erase 返回下一个有效迭代器。
while (it != v.end())
{ if (*it % 2 == 0) it = v.erase(it);
// erase 返回被删除元素的下一个位置 else ++it;
}
4.3 Linux (g++) 与 VS 的差异
-
VS 对迭代器失效非常敏感,一旦使用失效迭代器,大概率立即崩溃(调试模式下会断言)。
-
g++ 则相对宽容,扩容后旧迭代器可能仍指向原内存(但已被释放),程序可能"看起来正常",实则存在隐患,例如输出乱码或段错误。
建议:统一按照"任何修改容量的操作都会导致迭代器失效"来编程,不要依赖编译器行为。
五、OJ 实战:巩固 vector 使用
5.1 只出现一次的数字(异或法)
int singleNumber(vector<int>& nums) {
int ret = 0;
for (auto e : nums) ret ^= e;
return ret;
}
5.2 杨辉三角(vector<vector<int>>)
vector<vector<int>> generate(int numRows) {
vector<vector<int>> vv(numRows);
for (int i = 0; i < numRows; ++i) {
vv[i].resize(i + 1, 1);
}
for (int i = 2; i < numRows; ++i) {
for (int j = 1; j < i; ++j) {
vv[i][j] = vv[i-1][j] + vv[i-1][j-1];
}
}
return vv;
}
练习推荐:
-
删除排序数组中的重复项
-
数组中出现次数超过一半的数字
-
电话号码的字母组合
六、模拟实现 vector:核心框架
为了深入理解 vector,我们尝试自己实现一个简化版,命名为 bit::vector。
6.1 成员变量与基本接口
namespace bit {
template<class T>
class vector {
public:
// 迭代器就是原生指针
typedef T* iterator;
typedef const T* const_iterator;
// 构造、析构
vector() : _start(nullptr), _finish(nullptr), _end_of_storage(nullptr) {}
vector(int n, const T& val = T()) : _start(nullptr), _finish(nullptr), _end_of_storage(nullptr) {
reserve(n);
for (int i = 0; i < n; ++i)
push_back(val);
}
~vector() {
delete[] _start;
_start = _finish = _end_of_storage = nullptr;
}
// 迭代器
iterator begin() { return _start; }
iterator end() { return _finish; }
const_iterator begin() const { return _start; }
const_iterator end() const { return _finish; }
// 容量
size_t size() const { return _finish - _start; }
size_t capacity() const { return _end_of_storage - _start; }
bool empty() const { return _start == _finish; }
// 元素访问
T& operator[](size_t pos) { return _start[pos]; }
const T& operator[](size_t pos) const { return _start[pos]; }
// 修改
void push_back(const T& val);
void pop_back();
void reserve(size_t n);
void resize(size_t n, const T& val = T());
iterator insert(iterator pos, const T& val);
iterator erase(iterator pos);
private:
iterator _start; // 指向数据起始
iterator _finish; // 指向最后一个有效数据的下一个位置
iterator _end_of_storage; // 指向已分配内存的末尾
};
}
6.2 核心实现:reserve 与 push_back
template<class T>
void vector<T>::reserve(size_t n) {
if (n > capacity()) {
size_t old_size = size();
T* new_data = new T[n];
if (_start) {
// 拷贝旧数据
for (size_t i = 0; i < old_size; ++i)
new_data[i] = _start[i];
delete[] _start;
}
_start = new_data;
_finish = _start + old_size;
_end_of_storage = _start + n;
}
}
template<class T>
void vector<T>::push_back(const T& val) {
if (_finish == _end_of_storage) {
size_t new_cap = capacity() == 0 ? 1 : capacity() * 2;
reserve(new_cap);
}
*_finish = val;
++_finish;
}
6.3 深拷贝与 insert/erase
template<class T>
typename vector<T>::iterator vector<T>::insert(iterator pos, const T& val) {
// 判断扩容
if (_finish == _end_of_storage) {
size_t offset = pos - _start;
reserve(capacity() == 0 ? 1 : capacity() * 2);
pos = _start + offset; // 更新 pos,因为扩容后原迭代器失效
}
// 后移元素
for (iterator it = _finish; it > pos; --it)
*it = *(it - 1);
*pos = val;
++_finish;
return pos;
}
template<class T>
typename vector<T>::iterator vector<T>::erase(iterator pos) {
for (iterator it = pos; it < _finish - 1; ++it)
*it = *(it + 1);
--_finish;
return pos; // 返回被删除元素的下一个位置
}
七、致命陷阱:memcpy 浅拷贝问题
在模拟实现 reserve 时,如果用 memcpy 进行内存复制会怎样?
// 错误示范
void reserve(size_t n) {
if (n > capacity()) {
T* tmp = new T[n];
if (_start) {
memcpy(tmp, _start, size() * sizeof(T)); // 危险!
delete[] _start;
}
_start = tmp;
// ...
}
}
问题 :
如果 T 是 string 或其他管理资源的类型(如 vector<int>),memcpy 只是按字节复制指针(浅拷贝),导致两个对象指向同一块堆内存。当旧对象被 delete[] 时,会析构每个元素,释放资源;而新对象中的元素仍持有已释放的指针,最终导致双重释放 或内存泄漏。
正确做法:使用赋值操作(深拷贝)。
for (size_t i = 0; i < old_size; ++i)
tmp[i] = _start[i]; // 调用 T 的拷贝赋值,实现深拷贝
因此,在编写通用容器时,绝不能使用 memcpy 处理非 POD 类型。
八、动态二维数组:vector<vector<T>>
杨辉三角的代码展示了 vector 的嵌套使用。物理上,外层 vector 的每个元素又是一个内层 vector,它们的内存不一定是连续的,但每个内层 vector 内部连续。
vector<vector<int>> vv(5); // 5 行
for (int i = 0; i < 5; ++i)
vv[i].resize(i+1, 1); // 每行长度 i+1,初始化 1
这种结构比 C 语言的"指针数组"更安全、更易用。
九、总结与建议
-
优先使用
vector:动态数组是绝大多数场景的最佳选择。 -
善用
reserve:提前分配空间,避免频繁扩容。 -
警惕迭代器失效:任何可能改变容量的操作后,原来持有的迭代器都可能失效,务必重新获取。
-
模拟实现是提升内功的最佳途径 :亲手实现
reserve、push_back、insert、erase,你会对深拷贝、浅拷贝、异常安全有更深理解。 -
不要用
memcpy拷贝非 POD 元素:始终使用赋值或拷贝构造。
vector 的用法看似简单,但其中的陷阱和原理值得每个 C++ 开发者反复琢磨。希望这篇文章能帮你彻底掌握 vector,并在面试和工程中游刃有余。
练习题推荐:
LeetCode 26. 删除有序数组中的重复项
LeetCode 118. 杨辉三角
LeetCode 17. 电话号码的字母组合
下一篇预告 :我们将深入 list 容器,对比 vector 与 list 的优劣,并探讨迭代器失效在不同容器中的表现。敬请期待!