文章目录
- 1. vector的介绍
- 2. vector的常见构造
- 3. vector的遍历方式
- 🍑 [ ] + 下标
- 🍑 迭代器
- 🍑 范围for
- 4. vector 迭代器使用
- 🍑 begin 和 end
- 🍑 rbegin 和 rend
- 5. vector 空间增长问题
- 🍑 size
- 🍑 capacity
- 🍑 reserve
- 🍑 resize
- 🍑 empty
- 6. vector 的增删查改
- 🍑 push_back
- 🍑 pop_back
- 🍑 insert
- 🍑 erase
- 🍑 find
- 🍑 swap
- 🍑 operator[ ]
- 🍑 sort
- 7. vector 迭代器失效问题
- 🍑 失效场景
- 🍑 解决办法
- 8. 总结
1. vector的介绍
-
vector 是表示可变大小数组的序列容器。
-
vector 就像数组一样,采用连续的存储空间来存储元素。也就是意味着可以采用下标对 vector 的元素进行访问,和数组一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。
-
vector 使用动态分配数组来存储它的元素。当新元素插入时候,这个数组需要被重新分配大小为了增加存储空间。其做法是,分配一个新的数组,然后将全部元素移到这个数组。就时间而言,这是一个相对代价高的任务,因为每当一个新的元素加入到容器的时候,vector 并不会每次都重新分配大小。
-
vector 分配空间策略:vector 会分配一些额外的空间以适应可能的增长,因为存储空间比实际需要的存储空间更大。不同的库采用不同的策略权衡空间的使用和重新分配。但是无论如何,重新分配都应该是对数增长的间隔大小,以至于在末尾插入一个元素的时候是在常数时间的复杂度完成的。
-
vector 占用了更多的存储空间,为了获得管理存储空间的能力,并且以一种有效的方式动态增长。
-
与其它动态序列容器相比(deques、lists and forward_lists), vector 在访问元素的时候更加高效,在末尾添加和删除元素相对高效。对于其它不在末尾的删除和插入操作,效率更低。比起 lists 和 forward_lists 统一的迭代器和引用更好。
我们学习 vector 也和学习 string 一样,参考官方的文档:vector的文档介绍
2. vector的常见构造
这里主要有 4 中构造方式:
(1)无参构造一个空容器
vector<int> v1; //构造一个int类型的空容器
(2)构造并初始化 n 个 val 的容器
vector<int> v2(10, 5); //构造含有10个5的int类型容器
(3)拷贝构造某类型容器
vector<int> v3(v2); //拷贝构造int类型的v2容器
(4)使用迭代器进行初始化构造
vector<int> v4(v2.begin(), v2.end()); //使用迭代器拷贝构造v2容器的某一段内容
注意,vector 不只是能够用来构造 int 类型容器,还可以使用迭代器构造其他类型的容器
string s("hello world");
vector<char> v5(s.begin(), s.end()); //拷贝构造string对象的某一段内容
3. vector的遍历方式
vector 的遍历和 string 一样,也分为三种。
🍑 [ ] + 下标
vector 对 [ ] 运算符进行了重载,所以我们可以直接使用 [ ]+下标 访问对象中的元素。
还可以通过 [ ]+下标 修改对应位置的元素。
代码示例
int main()
{
vector<int> v; // 定义容器v1
// 尾插5个数据
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
// 使用下标访问数据
for (size_t i = 0; i < v.size(); ++i)
{
cout << v[i] << " ";
}
cout << endl;
// 使用下标修改数据
for (size_t i = 0; i < v.size(); ++i)
{
v[i] += 1;
cout << v[i] << " ";
}
cout << endl;
return 0;
}
运行结果
🍑 迭代器
begin 获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器
代码示例
int main()
{
vector<int> v; // 定义容器v1
// 尾插5个数据
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
// 使用迭代器访问数据
vector<int>::iterator it1 = v.begin();
while (it1 != v.end())
{
cout << *it1 << " ";
it1++;
}
cout << endl;
// 使用迭代器修改数据
vector<int>::iterator it2 = v.begin();
while (it2 != v.end())
{
*it2 += 1;
cout << *it2 << " ";
it2++;
}
return 0;
}
运行结果
🍑 范围for
和 string 一样,如果我们是通过范围 for 来修改对象的元素,那么接收元素的变量 e 的类型必须是引用类型,否则 e 只是对象元素的拷贝,对 e 的修改不会影响到对象的元素。
代码示例
int main()
{
vector<int> v; // 定义容器v1
// 尾插5个数据
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
// 使用范围for访问数据
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
// 使用范围for修改数据
for (auto& e : v)
{
e += 1;
cout << e << " ";
}
return 0;
}
运行结果
4. vector 迭代器使用
vector 的迭代器和 string 一样,也分为 正向迭代器 和 反向迭代器。
正向迭代器和 const正向迭代器:
反向迭代器 和 const 反向迭代器
它们的原理如下图所示(和 string 一样):
🍑 begin 和 end
通过 begin 函数可以得到容器中第一个元素的正向迭代器,通过 end 函数可以得到容器中最后一个元素的后一个位置的正向迭代器。
正向迭代器遍历容器:
int main()
{
//定义容器v
vector<int> v;
//使用push_back插入5个数据
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
//正向迭代器遍历容器
vector<int>::iterator it = v.begin();
while (it != v.end()) {
cout << *it << " ";
it++;
}
return 0;
}
调试运行
🍑 rbegin 和 rend
通过 rbegin 函数可以得到容器中最后一个元素的反向迭代器,通过 rend 函数可以得到容器中第一个元素的前一个位置的反向迭代器。
反向迭代器遍历容器:
int main()
{
//定义容器v
vector<int> v;
//使用push_back插入5个数据
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
//反向迭代器遍历容器
vector<int>::reverse_iterator rit = v.rbegin();
while (rit != v.rend()) {
cout << *rit << " ";
rit++;
}
return 0;
}
调试运行
注意:const 正向和反向这里就不演示了,和 string 的原理一样
5. vector 空间增长问题
主要学习以下几个函数
🍑 size
size 函数获取当前容器中的有效元素个数。
代码示例
int main()
{
//定义容器v并初始化为20个5
vector<int> v(20, 5);
cout << v.size() << endl; //获取当前容器中的有效元素个数
return 0;
}
运行结果
🍑 capacity
capacity 函数获取当前容器的最大容量。
代码示例
int main()
{
//定义容器v并初始化为30个5
vector<int> v(30, 5);
cout << v.capacity() << endl; //获取当前容器的最大容量
return 0;
}
运行结果
这里我们思考一个问题:capacity 是如何增容的呢?
可以用下面代码来验证一下
int main()
{
size_t sz;
vector<int> v;
sz = v.capacity();
cout << "making foo grow:\n";
for (int i = 0; i < 100; ++i) {
v.push_back(i);
if (sz != v.capacity()) {
sz = v.capacity();
cout << "capacity changed: " << sz << endl;
}
}
return 0;
}
运行结果
可以看到,在 VS2019 下,capacity 是按照大概 1.5 倍增长的。
那么 Linux 下呢?这里我也测试了一下,如图,大概是按照 2 倍增长的。
为什么 VS 和 Linux 下,capacity 增长的方式是不一样的呢?
很简单,因为早期的 STL 其实就是一个规范:
- VS 下用的是 PJ 版本,大概是按 1.5 倍进行增容的。
- Linux g++ 下是 SGI 版本,大概是按 2 倍进行增容的。
我们之前学数据结构知道,顺序表增容都是 2 倍的,所以不要固化的认为 vector 也是一样,具体增长多少是根据具体的需求定义的。
🍑 reserve
reserse 函数改变容器的最大容量。
(1)当所给值大于容器当前的 capacity 时,将 capacity 扩大到该值。
(2)当所给值小于容器当前的 capacity 时,什么也不做。
假设我事先知道要插入 100 个字符,那么我们可以使用 reserve 提前开好空间
int main()
{
size_t sz;
vector<int> v;
v.reserve(100); // 提前开好100空间
sz = v.capacity();
cout << "making foo grow:\n";
for (int i = 0; i < 100; ++i) {
v.push_back(i);
if (sz != v.capacity()) {
sz = v.capacity();
cout << "capacity changed: " << sz << endl;
}
}
return 0;
}
可以看到,当我们提前开好空间以后,容器并没有自己再去开辟空间
🍑 resize
resize 函数改变容器中的有效元素个数。
(1)当所给值大于容器当前的 size 时,将 size 扩大到该值,扩大的元素为第二个所给值,若未给出,则默认为 0。
(2)当所给值小于容器当前的 size 时,将 size 缩小到该值。
代码示例
int main()
{
size_t sz;
vector<int> v;
v.resize(100); // 开好100空间并全部初始化
sz = v.capacity();
cout << "making foo grow:\n";
for (int i = 0; i < 100; ++i) {
v.push_back(i);
if (sz != v.capacity()) {
sz = v.capacity();
cout << "capacity changed: " << sz << endl;
}
}
return 0;
}
可以看到,当我们使用 resize 的时候,除了进行开空间,还对容器进行了初始化
reserve 和 resize 综合演示
代码示例
int main()
{
//初始化容器
vector<int> v(10, 5);
//打印size和capacity
cout << v.size() << endl; //10
cout << v.capacity() << endl; //10
cout << endl;
//改变容器的capacity为20,size不变
v.reserve(20);
cout << v.size() << endl; //10
cout << v.capacity() << endl; //20
cout << endl;
//改变容器的size为15,capacity不变
v.resize(15);
cout << v.size() << endl; //15
cout << v.capacity() << endl; //20
return 0;
}
运行结果
注意:
-
reserve 只负责开辟空间,如果确定知道需要用多少空间,reserve 可以缓解 vector 增容的代价缺陷问题。
-
resize 在开空间的同时还会进行初始化,影响 size。
🍑 empty
empty 函数判断当前容器是否为空。
(1)如果容器为空,那么就输出 1
(2)如果容器不为空,那么就输出 0
代码示例
int main()
{
vector<int> v1(10, 5);
cout << v1.empty() << endl;
vector<int> v2;
cout << v2.empty() << endl;
return 0;
}
运行结果
6. vector 的增删查改
对于 vector 的增删查改主要是以下几个函数
🍑 push_back
push_back 函数对容器进行尾插。
代码示例
int main()
{
//定义容器v
vector<int> v;
//使用push_back插入5个数据
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
//打印
for (auto e : v) {
cout << e << " ";
}
return 0;
}
运行结果
🍑 pop_back
pop_back 函数对容器进行尾删
代码示例
int main()
{
//定义容器v
vector<int> v;
//使用push_back尾插5个数据
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
//使用pop_back尾删4个数据
v.pop_back();
v.pop_back();
v.pop_back();
v.pop_back();
//打印
for (auto e : v) {
cout << e << " ";
}
return 0;
}
运行结果
🍑 insert
insert 函数可以在所给迭代器位置插入一个或多个元素
(1)在 pos 位置插入一个值
代码示例
int main()
{
//定义容器v
vector<int> v;
//使用push_back尾插5个数据
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
//在容器头部插入0
v.insert(v.begin(), 10);
//打印
for (auto e : v) {
cout << e << " ";
}
return 0;
}
运行结果
(2)在 pos 位置插入 n 个值
代码示例
int main()
{
//定义容器v
vector<int> v;
//使用push_back尾插5个数据
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
//在容器头部插入5个10
v.insert(v.begin(), 5, 10);
//打印
for (auto e : v) {
cout << e << " ";
}
return 0;
}
运行结果
🍑 erase
erase 函数可以删除所给迭代器位置的元素,或删除所给迭代器区间内的所有元素(左闭右开)
(1)删除一个值
代码示例
int main()
{
//定义容器v
vector<int> v;
//使用push_back尾插5个数据
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
//头删
v.erase(v.begin());
//打印
for (auto e : v) {
cout << e << " ";
}
return 0;
}
运行结果
(2)删除一段区间
代码示例
int main()
{
//定义容器v
vector<int> v;
//使用push_back尾插8个数据
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
v.push_back(6);
v.push_back(7);
v.push_back(8);
//删除在该迭代器区间内的元素(左闭右开]
v.erase(v.begin(), v.begin() + 3);
//打印
for (auto e : v) {
cout << e << " ";
}
return 0;
}
运行结果
注意:以上都是按照特定的位置进行插入或删除元素的方式,若要按值进行插入或删除(在某一特定值位置进行插入或删除),则需要用到 find 函数。
🍑 find
vector 没有 find 函数,那么假设我要查找某个元素怎么办呢?
很简单,可以去调用算法库里面的一个函数接口:find
find 函数共三个参数,前两个参数确定一个迭代器区间(左闭右开),第三个参数确定所要寻找的值。
函数在所给迭代器区间寻找第一个匹配的元素,并返回它的迭代器,若未找到,则返回所给的第二个参数。
代码示例
int main()
{
//定义容器v
vector<int> v;
//使用push_back尾插6个数据
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
v.push_back(6);
// 假设我要3的前面插入300
//vector<int>::iterator pos = find(v.begin(), v.end(), 3);
auto pos = find(v.begin(), v.end(), 3);
if (pos != v.end())
{
cout << "找到了" << endl;
v.insert(pos, 300);
}
else
{
cout << "没有找到" << endl;
}
//打印
for (auto e : v) {
cout << e << " ";
}
return 0;
}
调试运行
注意:因为 find 函数是在算法库里面的,所以需要加头文件 #include<algorithm>
🍑 swap
通过 swap 函数可以交换两个容器的数据空间,实现两个容器的交换。
代码示例
int main()
{
//定义v1容器
vector<int> v1;
v1.push_back(1);
v1.push_back(1);
v1.push_back(1);
//定义v2容器
vector<int> v2;
v2.push_back(2);
v2.push_back(2);
v2.push_back(2);
//交换v1和v2的数据
v1.swap(v2);
//打印v1
for (auto e1 : v1) {
cout << e1 << " ";
}
//打印v2
for (auto e2 : v2) {
cout << e2 << " ";
}
return 0;
}
运行结果
🍑 operator[ ]
vector 中实现了 [ ]
操作符的重载,因此我们也可以通过 下标 + [ ] 的方式对容器当中的元素进行访问。
代码示例
int main()
{
//定义v1容器
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
v.push_back(6);
// 通过[]读写第0个位置。
cout << v[0] << endl;
// 通过[i]的方式遍历vector
for (size_t i = 0; i < v.size(); ++i) {
cout << v[i] << " ";
}
return 0;
}
运行结果
🍑 sort
sort 是算法库里面一个函数接口,它默认是排升序的,要传一段 左闭右开 的区间。
代码示例
int main()
{
//定义v1容器
vector<int> v;
v.push_back(9);
v.push_back(1);
v.push_back(5);
v.push_back(2);
v.push_back(0);
v.push_back(-1);
// 默认排升序
sort(v.begin(), v.end());
// 打印
for (auto e : v)
{
cout << e << " ";
}
return 0;
}
运行结果
如果想要排降序的话,需要使用仿函数,而且要带头文件 #include <functional>
代码示例
int main()
{
//定义v1容器
vector<int> v;
v.push_back(9);
v.push_back(1);
v.push_back(5);
v.push_back(2);
v.push_back(0);
v.push_back(-1);
// 默认排升序
sort(v.begin(), v.end(), greater<int>());
// 打印
for (auto e : v)
{
cout << e << " ";
}
return 0;
}
运行结果
7. vector 迭代器失效问题
我们上面学习了 insert 和 erase 函数,那么思考一个问题,为什么 insert 和 erase 的返回值是 iterator 迭代器呢?
迭代器的主要作用就是让算法能够不用关心底层数据结构,其底层实际就是一个指针,或者是对指针进行了封装,比如:vector 的迭代器就是原生态指针 T*
。
因此,迭代器失效,实际就是迭代器底层对应指针所指向的空间被销毁了,而使用一块已经被释放的空间,造成的后果是程序崩溃(即如果继续使用已经失效的迭代器,程序可能会崩溃)。
🍑 失效场景
(1)场景一
在下面代码中,我们在数组中 2 的位置插入一个 10,然后将 2 删除。
我们使用 find 获取的是指向 2 的指针以后,当我们在 2 的位置插入 10 后,该指针就指向了 10,所以我们之后删除的实际上是 10,而不是 2。
代码示例
int main()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
auto pos = find(v.begin(), v.end(), 3); //获取值为3的元素的迭代器
// 1 2 30 3 4 5
v.insert(pos, 10); // 在3的位置前插入30
// 1 2 3 4 5
v.erase(pos); // 删除pos位置的数据,导致pos迭代器失效。
//此处我们再访问pos位置的值,会导致非法访问
cout << *pos << endl;
return 0;
}
运行结果
erase 删除 pos 位置元素后,pos 位置之后的元素会往前搬移,没有导致底层空间的改变,理论上讲迭代器不应该会失效。
但是,如果 pos 刚好是最后一个元素,删完之后 pos 刚好是 end 的位置,而 end 位置是没有元素的,那么 pos 就失效了。
因此删除 vector 中任意位置上元素时,vs 就认为该位置迭代器失效了。
(1)场景二
把 vector 数组中的偶数全部删除。
代码示例
int main()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
auto it = v.begin();
while (it != v.end())
{
if (*it % 2 == 0) //删除容器当中的全部偶数
{
v.erase(it);
}
it++;
}
return 0;
}
运行结果
为什么运行起来会报错呢?因为迭代器访问到了不属于容器的内存空间,导致程序崩溃。
不仅如此,而且在迭代器遍历容器中的元素进行判断时,并没有对 1、3、5 元素进行判断。
以上操作,都有可能会导致 vector 扩容,也就是说 vector 底层原理旧空间被释放掉,而在打印时,it 还使用的是释放之间的旧空间,在对 it 迭代器操作时,实际操作的是一块已经被释放的空间,而引起代码运行时崩溃。
🍑 解决办法
vector 迭代器失效有 2 种:
- 扩容、缩容,导致野指针失效
- 迭代器指向的位置意义变了
所以我们要重新接收,重新处理,也就是说每次使用前,对迭代器进行重新赋值。
(1)场景一解决方案
对于实例一,我们在使用迭代器删除元素 2 时对其进行重新赋值便可以解决。
代码示例
int main()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
//获取值为2的元素的迭代器
auto pos = find(v.begin(), v.end(), 2);
//在值为2的元素的位置插入10
v.insert(pos, 10);
//重新获取值为2的元素的迭代器
pos = find(v.begin(), v.end(), 2);
//删除元素2
v.erase(pos);
// 打印
for (auto e : v)
{
cout << e << " ";
}
return 0;
}
运行结果
(2)场景二解决方案
对于实例二,我们可以接收 erase 函数的返回值(erase 函数返回删除元素的后一个元素的新位置)。
并且控制代码的逻辑是:当元素被删除后继续判断该位置的元素(因为该位置的元素已经更新,需要再次判断)。
代码示例
int main()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
v.push_back(6);
auto it = v.begin();
while (it != v.end())
{
if (*it % 2 == 0) // 删除容器当中的全部偶数
{
it = v.erase(it); // 删除后获取下一个元素的迭代器
}
else
{
it++; // 是奇数则it++
}
}
// 打印
for (auto e : v)
{
cout << e << " ";
}
return 0;
}
运行结果
8. 总结
通过上面的练习我们发现 vector 常用的接口更多是插入和遍历。
遍历更喜欢用数组 operator[i]
的形式访问,因为这样便捷。
大家可以多去刷一些 OJ 题以此来增强学习 vector 的使用。