在 C++ STL(标准模板库)中,vector 是最常用也最重要的容器之一。它以动态数组的形式存在,既能像普通数组一样高效访问元素,又能灵活地动态调整大小,满足不同场景下的数据存储需求。学习 vector,我们可以遵循 STL 的三个境界 ——能用、明理、能扩展,逐步掌握其核心用法与底层原理。本文将从 vector 的基础介绍、常用接口使用,到深度剖析与模拟实现,带你全面吃透 vector。
一、vector 基础:是什么与怎么用
1.1 认识 vector
vector 本质是一个动态数组,其底层通过连续的内存空间存储元素,并支持动态扩容。与普通数组相比,它的核心优势在于:
无需预先确定固定大小,可根据元素数量自动调整容量;
提供了丰富的接口(如插入、删除、容量管理等),简化开发;
支持随机访问(通过operator[]或at()),访问效率与普通数组一致(时间复杂度 O (1))。
在实际开发中,我们无需关心 vector 底层内存的分配与释放,只需调用其接口即可完成数据操作 —— 这是 “能用” vector 的第一步。
1.2 vector 核心接口实战
vector 的接口众多,但只需重点掌握常用接口,就能应对 80% 以上的场景。以下按 “定义→迭代器→空间管理→增删查改” 的顺序,梳理核心接口的用法。
1.2.1 vector 的定义(构造函数)
vector 提供了 4 种常用构造方式,具体如下表所示:
| 构造函数声明 | 接口说明 | 代码示例 |
|---|---|---|
vector()(重点) | 无参构造,创建空 vector | vector<int> vec;(空 vector,size=0,capacity=0) |
vector(size_type n, const value_type& val = value_type()) | 构造并初始化 n 个 val | vector<int> vec(5, 3);(包含 5 个 3,size=5,capacity=5) |
vector(const vector& x)(重点) | 拷贝构造,复制另一个 vector | vector<int> vec2(vec);(vec2 是 vec 的副本) |
vector(InputIterator first, InputIterator last) | 迭代器构造,复制 [first, last) 区间元素 | int arr[] = {1,2,3}; vector<int> vec(arr, arr+3);(vec 包含 1、2、3) |
1.2.2 迭代器:遍历 vector 的 “工具”
迭代器的作用是让算法(如find、sort)无需关心底层数据结构,vector 的迭代器本质是对 “原生态指针” 的封装(如T*)。核心迭代器接口如下:
| 迭代器接口 | 接口说明 | 代码示例 |
|---|---|---|
begin() + end()(重点) | begin():指向第一个元素;end():指向最后一个元素的下一个位置(无效位置) | for (auto it = vec.begin(); it != vec.end(); ++it) { cout << *it << " "; }(遍历所有元素) |
rbegin() + rend() | rbegin():指向最后一个元素(反向迭代器起点);rend():指向第一个元素的前一个位置 | for (auto it = vec.rbegin(); it != vec.rend(); ++it) { cout << *it << " "; }(反向遍历) |
注意:
end()和rend()指向的是 “无效位置”,不能直接解引用(*it)。
1.2.3 空间管理:size 与 capacity 的区别
vector 的 “空间” 分为两种:
size:当前已存储的元素个数;
capacity:当前底层内存可容纳的最大元素个数(容量)。
当size == capacity时,再插入元素会触发扩容—— 这是 vector 性能优化的关键场景。核心空间接口如下:
| 空间接口 | 接口说明 | 关键注意点 |
|---|---|---|
size() | 获取当前元素个数 | 时间复杂度 O (1) |
capacity() | 获取当前容量 | 容量 >= 元素个数 |
empty() | 判断是否为空(size==0) | 常用于遍历前判断 |
resize(size_type n, val)(重点) | 调整 size 为 n:- 若 n > 原 size,新增元素用 val 填充;- 若 n < 原 size,删除末尾元素 | 会改变 size,可能改变 capacity(n > 原 capacity 时扩容) |
reserve(size_type n)(重点) | 调整 capacity 为 n(仅当 n > 原 capacity 时生效) | 仅改变 capacity,不改变 size,不初始化元素 |
扩容机制的差异(面试常考)
不同编译器的 STL 实现,扩容倍数不同:
VS(PJ 版本 STL):1.5 倍扩容;
G++(SGI 版本 STL):2 倍扩容。
例如,插入 100 个元素时的扩容日志:
VS:1 → 2 → 3 → 4 → 6 → 9 → ... → 141;
G++:1 → 2 → 4 → 8 → 16 → 32 → 64 → 128。
优化建议:如果提前知道元素个数,用reserve(n)预先分配容量,可避免多次扩容(扩容需经历 “开辟新空间→拷贝元素→释放旧空间”,代价较高)。
示例代码(预分配容量优化):
void TestVectorExpandOP() {
vector<int> v;
v.reserve(100); // 提前分配100的容量,避免插入时扩容
for (int i = 0; i < 100; ++i) {
v.push_back(i); // 无扩容,效率更高
}
}
1.2.4 增删查改:常用操作接口
vector 的增删查改接口围绕 “尾操作” 和 “任意位置操作” 展开,核心接口如下:
| 接口名称 | 接口说明 | 时间复杂度 | 代码示例 |
|---|---|---|---|
push_back(val)(重点) | 尾插元素 val | O (1)(无扩容时)/ O (n)(扩容时) | vec.push_back(4);(在 vec 末尾加 4) |
pop_back()(重点) | 尾删最后一个元素 | O(1) | vec.pop_back();(删除末尾元素,size 减 1) |
find(iterator first, iterator last, val) | 查找 val 在 [first, last) 的位置(算法模块接口,非 vector 成员) | O(n) | auto pos = find(vec.begin(), vec.end(), 3);(找值为 3 的位置) |
insert(pos, val) | 在 pos 位置前插入 val | O (n)(pos 后元素需后移) | vec.insert(pos, 5);(在 pos 前插入 5) |
erase(pos) | 删除 pos 位置的元素 | O (n)(pos 后元素需前移) | vec.erase(pos);(删除 pos 位置元素) |
swap(vec2) | 交换两个 vector 的内存空间 | O (1)(仅交换指针,不拷贝元素) | vec.swap(vec2); |
operator[](index)(重点) | 像数组一样访问 index 位置元素 | O(1) | cout << vec[2];(访问第 3 个元素,无越界检查) |
注意:
operator[]不做越界检查,若需越界检查,可使用at(index)(越界时抛异常)。
1.3 迭代器失效:vector 的 “坑” 与解决办法
迭代器失效是 vector 使用中的常见问题,本质是 “迭代器底层指针指向的内存空间被销毁或失效”,继续使用会导致程序崩溃或结果错误。
哪些操作会导致迭代器失效?
-
触发扩容的操作:
resize()、reserve()、insert()、push_back()、assign()等 —— 这些操作可能开辟新内存、释放旧内存,导致原迭代器指向的旧内存无效。示例(扩容导致失效):
vector<int> vec{1,2,3}; auto it = vec.begin(); // it指向旧内存(地址A) vec.reserve(100); // 开辟新内存(地址B),释放旧内存(A) cout << *it; // 错误!it指向已释放的内存,程序崩溃(VS下) -
删除操作(erase ()):
- 若删除的是非末尾元素:pos 后元素前移,pos 迭代器仍指向原位置,但该位置元素已改变,逻辑上失效;
- 若删除的是末尾元素:pos 迭代器指向
end()(无效位置),直接失效。
示例(erase 导致失效):
vector<int> vec{1,2,3,4}; auto pos = find(vec.begin(), vec.end(), 3); vec.erase(pos); // 删除3,pos指向原位置(现在元素是4) cout << *pos; // VS下崩溃,G++下输出4(逻辑错误)
迭代器失效的解决办法
核心原则:操作后重新赋值迭代器。
扩容操作后:重新调用begin()/end()获取新迭代器;
erase()操作后:利用erase()的返回值(返回删除位置的下一个有效迭代器)重新赋值。
正确示例(删除所有偶数):
vector<int> vec{1,2,3,4};
auto it = vec.begin();
while (it != vec.end()) {
if (*it % 2 == 0) {
it = vec.erase(it); // 关键:用返回值更新it
} else {
++it;
}
}
// 结果:vec = {1,3}(正确)
1.4 vector 在 OJ 中的实战应用
vector 是算法题(OJ)中的 “常客”,以下列举两个经典案例,帮助理解接口的实际用法。
案例 1:只出现一次的数字(LeetCode 136)
题目:给定非空数组,除一个元素只出现一次外,其余均出现两次,找出该元素。思路:利用异或运算(^)的性质:a^a=0、a^0=a,遍历数组异或所有元素,结果即为目标值。
代码实现:
class Solution {
public:
int singleNumber(vector<int>& nums) {
int res = 0;
for (auto e : nums) {
res ^= e; // 遍历异或
}
return res;
}
};
案例 2:杨辉三角(LeetCode 118)
题目:生成前 n 行杨辉三角,每行首尾为 1,中间第 j 个元素 = 上一行第 j-1 个 + 上一行第 j 个。思路:用二维 vector(vector<vector<int>>)存储,先初始化每行大小并填充 1,再计算中间元素。
代码实现:
class Solution {
public:
vector<vector<int>> generate(int numRows) {
vector<vector<int>> vv(numRows); // 初始化n行
// 每行初始化大小为i+1,填充1
for (int i = 0; i < numRows; ++i) {
vv[i].resize(i + 1, 1);
}
// 计算中间元素(从第3行开始)
for (int i = 2; i < numRows; ++i) {
for (int j = 1; j < i; ++j) {
vv[i][j] = vv[i-1][j-1] + vv[i-1][j];
}
}
return vv;
}
};
二、vector 深度剖析:底层原理与模拟实现
掌握了 vector 的使用后,我们需要 “明理”—— 理解其底层结构,甚至 “扩展”—— 自己模拟实现核心接口。
2.1 vector 的底层结构
vector 的核心成员变量(SGI 版本)如下:
T* _start:指向底层内存的起始位置(第一个元素);
T* _finish:指向底层内存的末尾位置(最后一个元素的下一个位置);
T* _end_of_storage:指向底层内存的容量末尾位置(容量的最后一个位置)。
三者的关系满足:
size() = _finish - _start(元素个数);
capacity() = _end_of_storage - _start(容量);
当_finish == _end_of_storage时,插入元素触发扩容。
2.2 模拟实现 vector 的核心接口
我们以bit::vector为例,模拟实现 vector 的核心接口(构造、析构、push_back、reserve、resize 等)。
2.2.1 核心成员变量定义
namespace bit {
template <class T>
class vector {
public:
// 迭代器(本质是指针)
typedef T* iterator;
typedef const T* const_iterator;
// 核心成员变量
iterator _start;
iterator _finish;
iterator _end_of_storage;
// 构造函数、析构函数、接口...
};
}
2.2.2 构造函数与析构函数
// 无参构造
vector() : _start(nullptr), _finish(nullptr), _end_of_storage(nullptr) {}
// 析构函数(释放内存)
~vector() {
if (_start) {
delete[] _start; // 释放动态内存
_start = _finish = _end_of_storage = nullptr;
}
}
// 构造n个val
vector(size_t n, const T& val = T()) {
reserve(n); // 先开辟n的容量
for (size_t i = 0; i < n; ++i) {
push_back(val); // 尾插n个val
}
}
// 拷贝构造(深拷贝)
vector(const vector& v) {
reserve(v.capacity()); // 开辟与v相同的容量
// 遍历v,拷贝每个元素(调用T的拷贝构造)
for (const auto& e : v) {
push_back(e);
}
}
2.2.3 reserve 与 resize 的实现
// 调整容量(仅扩容,不初始化)
void reserve(size_t n) {
if (n > capacity()) {
size_t old_size = size();
// 1. 开辟新内存
T* new_mem = new T[n];
// 2. 拷贝旧元素到新内存(注意:不能用memcpy!)
if (_start) {
// 用循环调用T的赋值运算符,实现深拷贝
for (size_t i = 0; i < old_size; ++i) {
new_mem[i] = _start[i];
}
// 3. 释放旧内存
delete[] _start;
}
// 4. 更新指针
_start = new_mem;
_finish = _start + old_size;
_end_of_storage = _start + n;
}
}
// 调整size(初始化元素)
void resize(size_t n, const T& val = T()) {
if (n > size()) {
// 若n超过容量,先扩容
if (n > capacity()) {
reserve(n);
}
// 填充新元素
while (_finish < _start + n) {
*_finish = val;
++_finish;
}
} else {
// n小于size,直接缩小_finish
_finish = _start + n;
}
}
2.2.4 push_back 的实现
void push_back(const T& val) {
// 若容量不足,扩容(默认2倍,若原容量为0则扩为1)
if (_finish == _end_of_storage) {
size_t new_cap = (capacity() == 0) ? 1 : capacity() * 2;
reserve(new_cap);
}
// 尾插元素
*_finish = val;
++_finish;
}
2.3 模拟实现中的坑:memcpy 的浅拷贝问题
在reserve的实现中,我们用循环赋值代替memcpy—— 这是因为memcpy是 “二进制浅拷贝”,当 vector 存储的是自定义类型(如 string) 时,会导致内存泄漏或崩溃。
问题分析
以vector<string>为例,若用memcpy拷贝元素:
memcpy会直接拷贝string对象的二进制数据(包括string内部的指针_str);- 扩容后释放旧内存时,会调用
string的析构函数,释放_str指向的内存; - 新内存中的
string对象仍指向已释放的_str,后续访问会导致 “野指针” 错误。
结论
若 vector 存储的是内置类型(int、char 等),memcpy可正常工作;
若存储的是自定义类型(含资源管理),必须用 “深拷贝”(如循环赋值、调用拷贝构造),避免浅拷贝问题。
2.4 动态二维数组:vector 的嵌套使用
vector 支持嵌套定义(如vector<vector<int>>),可用来实现 “动态二维数组”—— 每行的长度可独立调整,比普通二维数组(如int arr[5][5])更灵活。
示例:杨辉三角的动态二维数组结构
当n=5时,vector<vector<int>> vv(5)的初始结构(每行无元素):
vv[0]:_start=nullptr,_finish=nullptr,_end_of_storage=nullptr;
vv[1]:同上;
...
vv[4]:同上。
调用vv[i].resize(i+1, 1)后,每行被初始化为i+1个 1,最终结构:
vv[0]:[1](size=1,capacity=1);
vv[1]:[1, 1](size=2,capacity=2);
vv[2]:[1, 2, 1](size=3,capacity=3);
...
三、总结
vector 作为 C++ STL 的核心容器,其学习路径清晰:
- 能用:掌握构造、迭代器、空间管理、增删查改等核心接口,能在项目和 OJ 中灵活使用;
- 明理:理解底层结构(
_start/_finish/_end_of_storage)、扩容机制、迭代器失效原因; - 能扩展:模拟实现核心接口,规避浅拷贝(如 memcpy)等问题,甚至根据需求自定义 vector。
1302

被折叠的 条评论
为什么被折叠?



