【C++】编程必备:深入解析vector容器的使用

文章目录
一、vector简介
vector与我们之前学习过的string类相似,只是string类存储的数据类型已经确定是char,不是一个模板,但是学习string也能让我们快速学习以后这些容器的使用,所以推荐大家可以先学习string,链接如下:
【C++】STL全面简介与string类的使用(万字解析)
我们常说的vector是一个类似于顺序表的类模板,它的类型是由使用者确定的,所以vector相对于更加灵活,并且属于STL库,它的接口看起来更加规范,不像string那样混乱,所以如果学懂了string,那么我们学习vector也就不会难了,接下来让我们正式开始学习vector
二、vector的默认成员函数重载
默认构造
我们之前也提到过vector就类似于顺序表,只不过vector是一个模板,所以它的底层也是一个数组,那么默认构造时不给这个数组初始化空间就可以了,我们写个代码试试,如下:
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> v;
//查看vector对象v的容量,和string接口类似
cout << v.capacity() << endl;
//查看有效数据个数
cout << v.size() << endl;
return 0;
}
我们来看看代码运行结果:
可以看到确实按照我们所料,vector的默认构造不会给底层的数组开辟空间,也不需要放什么\0,那是string类确定是char类型才加的
普通构造
vector的普通构造我们就掌握两个,其中一个就是n个value的构造,如图:
接下来我们简单用一用就好了,因为这个接口类似于我们之前讲的string类的n个字符c的构造,有了string类的基础我们讲vector的使用就轻松很多,如下:
int main()
{
//构造出有两个5的vector
vector<int> v(2, 5);
return 0;
}
我们来看看代码调试结果:
这就是vector的n个value的构造,可以插入n个value值,接下来我们来学习下一个构造,这个构造是C++11才支持的,因为C++11引入了initializer_list,让编译器能够在一些特定场景将花括号中的元素构造成initializer_list,这个我们在实现部分才能讲清
这里我们主要会用就行,说白了就是我们可以像数组一样使用一个花括号去初始化vector,这个花括号里面的元素未来就会被拿去初始化initializer_list,如下:
int main()
{
//使用花括号初始化,也可以写成:
//vector<int> v{1, 2, 3, 4};
vector<int> v = { 1, 2, 3, 4 };
return 0;
}
我们来调试看看vector是否按照花括号中的元素初始化了,如下:
可以看到成功用花括号中的元素初始化了vector
拷贝构造和赋值重载
vector的拷贝构造和赋值重载采用的是深拷贝,如果不知道深拷贝的同学可以看这篇文章,里面详细讲到了深浅拷贝:【C++】揭秘类与对象的内在机制(核心卷之深浅拷贝与拷贝构造函数的奥秘)
接下来我们来演示一下vector的拷贝构造和赋值重载的使用,如下:
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> v1(2, 3);
//拷贝构造
vector<int> v2 = v1;
vector<int> v3;
//赋值重载
v3 = v1;
return 0;
}
我们来看看调试结果,看看是不是深拷贝,如下:
可以看到确实实现了深拷贝,它们底层的数组地址不同
析构
vector的析构比较简单,就是直接释放底层的数组即可,只有后面list和map等数据结构的释放才会稍微复杂一点,这里就不做演示了
三、元素访问接口
我们来看看vector的元素访问接口有哪些:
可以看到跟string类的元素访问接口差不多,因为string和vector的底层都是数组,所以都重载了[]运算符,其中at也相当于就是[],而front和back就分别是取第一个元素和最后一个元素,我们演示一下:
int main()
{
vector<int> v = { 1, 2, 3, 4 };
cout << v[2] << endl;
cout << v.at(2) <<endl;
cout << v.front() << endl;
cout << v.back() << endl;
return 0;
}
运行结果如下:
四、容量相关接口
我们来看看vector容量相关的接口:
可以发现还是和string类高度相似,size和capacity分别是求有效数据个数和容量大小,resize是改变size的大小,empty是判空,reserve是扩容接口,我们来简单使用一下:
int main()
{
vector<int> v = { 1, 2, 3, 4 };
cout << v.size() << endl;
cout << v.capacity() << endl;
cout << v.empty() << endl;
v.resize(20);
v.reserve(35);
cout << v.size() << endl;
cout << v.capacity() << endl;
return 0;
}
代码运行结果如下:
五、迭代器
vector和string迭代器的使用方法相同,因为我们之前就讲过,迭代器的出现就是为了给每个容器一个统一的遍历和访问的方式,所以自然每个容器的迭代器的使用方法相同了,如下:
int main()
{
vector<int> v = { 1, 2, 3, 4 };
vector<int>::iterator it = v.begin();
while (it != v.end())
{
cout << *it << " ";
it++;
}
cout << endl;
return 0;
}
代码运行结果如下:

接下来我们再试试范围for,如下:
int main()
{
vector<int> v = { 1, 2, 3, 4 };
for (auto& e : v)
{
cout << e << " ";
}
cout << endl;
return 0;
}
代码运行结果如下:
可以看到vector迭代器和范围for的使用和string几乎没有差别
六、vector的修改接口
vector的修改接口非常简单,大部分和string的差不多,还比string的少,如下:
push_back与pop_back
在vector中就没有append之类的来干扰我们,我们还是一个一个简单使用一下,首先是push_back以及pop_back,分别是尾插和尾删,如下:
insert
接下来我们来看看insert,它的接口如图:
可以看到vector的插入和string略微有点不同了,因为vector不再是指定下标去插入元素了,而是指定一个迭代器的位置,将元素插入到这个迭代器的位置上,我们来演示一下,如下:
int main()
{
vector<int> v = { 1, 2, 3, 4 };
vector<int>::iterator it = v.begin();
while (it != v.end())
{
if (*it == 2)
v.insert(it, -1);
it++;
}
for (auto e : v)
cout << e << " ";
return 0;
}
我们的预期是在元素2的位置上插入一个元素-1,我们来看看代码运行结果,看看是否如我们所料,如图:

我们惊讶地发现,代码居然报错了,这是为什么呢?这就涉及到迭代器失效的问题了,所以我们先来学习迭代器失效是怎么一回事,然后再迭代器失效部分我们再来演示一下insert的使用
迭代器失效问题
由于我们vector的insert和erase都是通过迭代器进行插入和删除元素的操作,所以可能导致迭代器失效,也就是迭代器经过插入或者删除后失去效果,没用了,比如当我们插入一个元素后,原数组可能会进行扩容,而扩容的本质就是释放当前空间然后再开一段空间,那么原本指向vector对象元素的迭代器就失效了,删除元素也是同理,被删除后,那个元素的迭代器就失效了,如图:

在上面的图中我们演示了在元素2位置上插入一个元素的流程图,不知道把这个图画完整之后你有没有发现什么异常,这里我们就直接说了,就是那个it现在指向的是新数组的第二个元素,而原来那段空间释放了,外部的it还指向原来的那段空间,所以外部的it就失效了
当然上面的扩容可能并不存在,就是我们插入-1也可能不会扩容,但是VS一定会报错,因为它认为只要删除或者插入了元素,那么这个迭代器就一定失效了,只要继续使用就报错,迭代器失效的解决方法也很简单,就是接收新空间的迭代器
那么我们怎么接收新空间的迭代器呢?其实我们可以从insert和erase的返回值看出,这两个函数的返回值就是迭代器,返回的就是新空间的迭代器(可能不是新空间,就是因为不确定所以才要接收),接下来我们接收新空间的迭代器试试,看看有没有问题,如下代码:
int main()
{
vector<int> v = { 1, 2, 3, 4 };
vector<int>::iterator it = v.begin();
while (it != v.end())
{
if (*it == 2)
{
//插入-1之后接收新的迭代器(可能是新空间,可能不是)
//由于是否扩容程序员也不知道,所以一定要接收
it = v.insert(it, -1);
//注意此时it指向的是-1,如果后面只++一次就还是2
//然后又要进行插入,就死循环了,所以这里往后走一步
it++;
}
it++;
}
for (auto e : v)
cout << e << " ";
return 0;
}
我们来看看代码运行结果:
可以看到代码没有问题,果然就跟我们说的一样,这就是insert的使用,一定要记得接收insert返回过来的迭代器
erase
我们来看看erase的接口:
这两个接口分别是删除一个迭代器位置上的元素以及删除一段迭代器区间,我们就以第一个接口为例讲解,我们还是简单写一下,不接收返回值会怎么样,如下代码:
int main()
{
vector<int> v = { 1, 2, 3,4 };
auto it = v.begin();
while (it != v.end())
{
if (*it == 2)
{
v.erase(it);
}
it++;
}
for (auto e : v)
cout << e << " ";
return 0;
}
我们来看看代码是否会出错,如下:
可以看到代码确实有问题,同样也是迭代器失效的问题,但是这时候我们就需要分析一下了,删除并不会导致扩容,为什么也会造成迭代器失效呢?其实正常情况下确实不会造成迭代器失效,但是如果是最后一个元素的迭代器呢?
当我们把最后一个元素删除之后,不会发生数据的挪动,只会让size- -,那么此时最后一个元素我们不能访问,而原本的迭代器还指向最后一个元素,此时我们如果操作迭代器就会产生很大问题,但是可能有同学还是会问,上面的代码我们删除的不是最后一个元素啊,为什么还是会报错?
其实之前我们就已经做了解答了,这是因为VS的检查机制,为了确保安全,只要有可能会出现迭代器失效,VS就认为一定会失效,所以就算我们没有删除最后一个元素VS还是会报错,所以为了避免出现迭代器失效的问题,我们最好都要接收返回值,如下:
int main()
{
vector<int> v = { 1, 2, 3,4 };
auto it = v.begin();
while (it != v.end())
{
if (*it == 2)
{
it = v.erase(it);
}
it++;
}
for (auto e : v)
cout << e << " ";
return 0;
}
代码运行结果如图:
可以看到确实是迭代器失效的问题,可能大家对它还是不太熟悉,等到后面我们讲了vector的实现后大家才能理解透彻
那么关于vector的使用我们今天就讲到这里,如果大家有什么疑问欢迎私信我,下一篇文章我们就开始将vector的实现了,敬请期待吧
bye~
更多推荐


所有评论(0)