目录
vector介绍
vector的常用接口介绍
constructor构造函数
vector()
vector(size_type n, const value_type& val = value_type())
vector(const vector& x)
vector(InputIterator first, InputIterator last)
iterator
begin
end
rbegin
rend
vector 空间增长问题
size
capacity
empty
resize
reserve
reserve不同环境在的增容规则
vector增删查改
push_back
pop_back
find
insert
erase
swap
operator[]
迭代器失效问题
因为扩容导致迭代器失效
解决方式
因为删除数据导致迭代器失效
解决方法
以下代码的功能是删除vector中所有的偶数,请问那个代码是正确的,为什么?
1、vector是表示可变大小数组的序列容器。
2、就像数组一样,vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问,和数组一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。
3、本质讲,vector使用动态分配数组来存储它的元素。当新元素插入时候,这个数组需要被重新分配大小为了增加存储空间。其做法是,分配一个新的数组,然后将全部元素移到这个数组。就时间而言,这是一个相对代价高的任务,因为每当一个新的元素加入到容器的时候,vector并不会每次都重新分配大小。
4、 vector分配空间策略:vector会分配一些额外的空间以适应可能的增长,因为存储空间比实际需要的存储空间更大。不同的库采用不同的策略权衡空间的使用和重新分配。但是无论如何,重新分配都应该是对数增长的间隔大小,以至于在末尾插入一个元素的时候是在常数时间的复杂度完成的。
5、 因此,vector占用了更多的存储空间,为了获得管理存储空间的能力,并且以一种有效的方式动态增长。
6、 与其它动态序列容器相比(deques, lists and forward_lists), vector在访问元素的时候更加高效,在末尾添加和删除元素相对高效。对于其它不在末尾的删除和插入操作,效率更低。比起list和forward_lists统一的迭代器和引用更好。
vector是非常常见的一个容器,在这里,就介绍一些常用的接口。其它一些接口如果用到时,查文档再了解学习就好了,毕竟一个人的学习精力有限嘛。老规矩,和string一样,在下一篇我会写一篇vector的模拟实现,希望大家支持一下博主~
无参构造。
const allocator_type& alloc = allocator_type() :这个是空间适配器,在这里我们不用去管它,只用知道它是用来申请空间用的,可以提高申请空间效率就足够了。(下面的类似,我就不在这里介绍了)
举个栗子:
int main()
{
vector v;
return 0;
}
vector
v: 我们在使用vector时要用< 类型 >指定一下,它和string是有区别的。在这里我们指定了int,说明这个容器里面放的数据是int类型。
我们调试一下看看这个构造函数做了什么:
我们发现无参的构造函数size和capacity是0,说明里面是没有数据的。
size_type n:要构造指定类型数据的个数。
const value_type& val = value_type():要初始化构造的vector里面存放的数据。
举个栗子:
int main()
{
vector v(5, 10);
vector> vv(5, vector(5, 100)); //二维数组
return 0;
}
vector
v(5, 10): 初始化的vector里面存放了5个10
vector> vv(5, vector vector里面放了5个vector(5, 100)): (5,100),vector里面又放了一个vector,相当于二维数组的功能。vector (5,100)中vector初始化(存放)了5个100。
我们调试一下代码看看情况:
拷贝构造,用一个已经初始化好的对象来拷贝构造一个没有初始化的对象。
举个栗子:
int main()
{
vector v1(5, 10);
vector v2(v1);
for (auto e : v2)
{
cout << e << " ";
}
cout << endl;
return 0;
}
运行结果:
我们可以看到v1的内容拷贝构造出了v2,所以v2和v1里面存放的数据一样 。
使用迭代器区间构造初始化对象
举个栗子:
int main()
{
vector v1(5, 10);
vector v2(v1.begin(), v1.end());
for (auto e : v2)
{
cout << e << " ";
}
cout << endl;
vector v3(v1.begin(), v1.begin() + 2);
for (auto e : v3)
{
cout << e << " ";
}
cout << endl;
return 0;
}
运行结果:
我们可以看到可以用一个相同类型的vector对象区间来构造初始化一个新对象,这个区间我们可以合理控制大小。
注意:这个迭代器区间是左闭右开的,也就是说last指的是最后一个数据的下一个坐标的位置。
获取第一个数据位置的迭代器。
举个栗子:
int main()
{
vector v(5, 10);
vector::iterator it = v.begin();
vector::const_iterator it2 = v.cbegin();
cout << *it << endl;
cout << *it2 << endl;
return 0;
}
运行结果:
begin:可以修改迭代器位置的值
cbegin: 不可以修改迭代器位置的值
获取最后一个数据的下一个位置的迭代器。
举个栗子:
int main()
{
vector v(5, 10);
vector::iterator it = v.end();
cout << *(--it) << endl;
*it = 100; //it刚才已经 -- 了,已经表示最后一个数据的迭代器了
vector::const_iterator it2 = v.cend();
cout << *(--it2) << endl;
return 0;
}
运行结果:
*(--it):--it代表最后一个数据的迭代器,解引用之后拿到最后一个数据
*it = 100:解引用之后再修改最后一个数据的值为100,这里不要 再 -- 了,前面已经--过了
注意:it2是const类型的迭代器,它指向的内容解引用之后不可以被修改
获取最后一个位置的迭代器。
crbegin:const版本,通过const拿到的crbegin的迭代器,它指向的内容不可以被修改,和上面一样,下面就不再演示了。
举个栗子:
int main()
{
vector v(5, 10);
vector::reverse_iterator it = v.rbegin();
cout << *it << endl;
*it = 100;
vector::const_reverse_iterator it2 = v.crbegin();
cout << *it2 << endl;
return 0;
}
运行结果:
我们发现rbegin/crbegin指向的是最后一个数据的迭代器。
反向迭代器:我们用迭代器遍历的时候,我们++,底层实际上是在--往后走,与正向迭代器用法一样,但是意思是反的。
举个栗子:
int main()
{
vector v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
vector::reverse_iterator it = v.rbegin();
while (it != v.rend())
{
cout << *it << " ";
it++;
}
cout << endl;
return 0;
}
v.push_back(),待会会介绍,我们为了在这里更好演示效果就先使用这个接口~
我们暂时只用明白上面5个push_back后,vector里面存放的数据时:1 2 3 4 5
运行结果:
我们发现使用反向迭代器之后数据是反着打印的。
上面我们使用了rend,可能有的码友没见过,我们在下面来介绍一下就明白刚才的代码了:
获取第一个数据的前一个位置的迭代器。
在刚才展示的例子中我们在这里画个图来解释一下:
获取数据个数。
举个栗子:
int main()
{
vector 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);
cout << v.size() << endl;
return 0;
}
在这里我们还是要借助push_back()接口,意思是往vector里面插入数据。(待会详细介绍~)
运行结果:
我们可以看到通过v.size(),就拿到了vector里面数据的个数。
获取vector容量的大小。
举个栗子:
int main()
{
vector 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);
cout << v.capacity() << endl;
return 0;
}
运行结果:
判断是否为空,为空就返回true;否则返回false。
举个栗子:
int main()
{
vector v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
cout << v.empty() << endl;
vector v2;//无参的构造函数初始化时,v2里面是没有数据的,size为0
cout << v2.empty() << endl;
return 0;
}
运行结果:
我们发现v不为空,则返回false(0); v2为空,则返回true(1)。
改变vector的数据个数(size)。
size_type n:要改变vector里面size的个数到n
const value_type& val:如果vector里面当前的size小于n,则往后用值val初始化到个数n。
举个栗子:
int main()
{
//使用默认的val
vector v;
v.resize(5);
//指定val
//1、当 v2.size() < n时
vector v2;
v2.resize(5, 10);
//1、当 v2.size() > n时
vector v3(v2);
v3.resize(2, 50);
return 0;
}
运行结果:
v.resize(5):size修改到5,使用默认的val,int对应的是0。
v2.resize(5,10):v2.size < 5,size刚开始为0,所以size往后追加到5,用10来初始化
v3.resize(2,50):刚开始v3.size() > 2,所以size直接变成2,并且不会用50来初始化
扩容,改变vector的capacity(容量),实际上这个接口用的没有resize多。
举个栗子:
int main()
{
vector v;
cout << v.capacity() << endl;
v.reserve(20);
cout << v.capacity() << endl;
return 0;
}
运行结果:
对于下面的一段代码,在VS和Linux环境下来验证它们的增容规则:
int main()
{
size_t sz;
std::vector foo;
sz = foo.capacity();
std::cout << "making foo grow:\n";
for (int i = 0; i < 100; ++i) {
foo.push_back(i);
if (sz != foo.capacity()) {
sz = foo.capacity();
std::cout << "capacity changed: " << sz << '\n';
}
}
}
VS:
Linux:
我们可以从上面的结果发现。
VS的capacity增长方式大概是按1.5倍方式增长。
Linux的capacity增长方式是按照2倍方式扩容的。
总结:
1、capacity的代码在vs和g++下分别运行会发现,vs下capacity是按1.5倍增长的,g++是按2倍增长的。这个问题经常会考察,不要固化的认为,顺序表增容都是2倍,具体增长多少是根据具体的需求定义的。vs是PJ版本STL,g++是SGI版本STL。
2、reserve只负责开辟空间,如果确定知道需要用多少空间,reserve可以缓解vector增容的代价缺陷问题。(reservr扩容直接到指定的capacity大小,不是至少扩大指定的大小)
3、resize在开空间的同时还会进行初始化,影响size。
在vector尾部插入数据。
举个栗子:
int main()
{
vector v;
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 << " ";
}
cout << endl;
return 0;
}
运行结果:
通过遍历push_back后的v,我们发现每次push_back都是在尾部插入数据。
删除尾部的数据。
举个栗子:
int main()
{
vector v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.pop_back();
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
return 0;
}
运行结果:
v刚开始尾插了1 2 3 ,再经过pop_back()之后,尾部数据3被删除了,这时候再遍历v,就只有数据1 2。
find: 算法库里面提供的(string里面的find是string自己的),不是vector成员的接口。在使用时要传迭代器区间。
举个栗子:
int main()
{
vector v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
vector::iterator it = find(v.begin(), v.end(), 3);
cout << *it << endl;
return 0;
}
运行结果:
vector
::iterator it = find(v.begin(), v.end(), 3): int是传给模板类型T;v.begin(),v.end()是传给迭代器区间InputIterator first,InputIterator last;3是要查找的内容vector::iterator是返回类型。
在指定的位置前插入数据val。(指定的位置都是传对应的迭代器)
vector我们发现库里面是没有提供头插头删的,因为vector是连续的空间,头插头删效率太低。所以接口是不提供了。
但是如果想要头插,insert可以实现其功能(尽量少用)。
举个栗子:
int main()
{
vector v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
v.insert(v.begin(), 10);
vector::iterator it = find(v.begin(), v.end(), 3);
v.insert(it, 30);
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
return 0;
}
运行结果:
我们在使用insert时,传的position必须是迭代器。(这和string不同,string可以传下标或迭代器)
删除position(迭代器)位置的值。
举个栗子:
int main()
{
vector v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
//iterator erase(iterator position);
v.erase(v.begin());
vector::iterator it = find(v.begin(), v.end(), 3);
v.erase(it);
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
//iterator erase (iterator first, iterator last);
v.erase(v.begin(), v.end());
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
return 0;
}
运行结果:
v.erase(v.begin()):v.begin()表示第一个数据的迭代器,删除 v.begin()
v.erase(it):删除it位置迭代器对应的数据
v.erase(v.begin(), v.end()):删除指定迭代器区间的数据
交换两个vector的数据空间。(底层实际上交换两个指针的指向)
举个栗子:
int main()
{
vector v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
vector v2;
v2.push_back(10);
v2.push_back(20);
v2.push_back(30);
v2.push_back(40);
v2.push_back(50);
v1.swap(v2);
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
for (auto e : v2)
{
cout << e << " ";
}
cout << endl;
return 0;
}
运行结果:
v1.swap(v2):我们可以看出来swap是成员函数。之后v1和v2两个vector里面的数据内容就交换成功了。
像数组一样访问。
vector常用的接口更多是插入和遍历。遍历更喜欢用数组operator[i]的形式访问,因为这样便捷。
举个栗子:
int main()
{
vector v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
//像数组一样访问数据
cout << v[2] << endl;
//修改数据(非const)
int& tmp = v[3];
tmp = 30;
cout << v[3] << endl;
return 0;
}
运行结果:
对于const版本就不能用引用修改其值了。
举个栗子:
迭代器的主要作用就是让算法能够不用关心底层数据结构,其底层实际就是一个指针,或者是对指针进行了封装,比如:vector的迭代器就是原生态指针T*。因此迭代器失效,实际就是迭代器底层对应指针所指向的空间被销毁了,而使用一块已经被释放的空间,造成的后果是程序崩溃(即如果继续使用已经失效的迭代器,程序可能会崩溃)
会引起其底层空间改变的操作,都有可能是迭代器失效,比如:resize、reserve、insert、assign、push_back等
我们在不断往vector里面插入数据时,可能存在扩容的情况。如果扩容方式是异地扩容,那么原来迭代器的位置指向原来的旧空间(异地扩容后就销毁了)。
我们举个栗子:
int main()
{
vector v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
vector::iterator it = find(v.begin(), v.end(), 2);
cout << "插入前:" << *it << endl;
v.insert(it, 10);
cout << "插入后:" << *it << endl;
return 0;
}
运行结果:
我们发现插入后,再打印*it程序就崩溃了。我用的是vs2019版本的编译器,如果每次insert后再使用该迭代器就会报错,但是在其他编译器就不一定了。实际上v.insert(it,10)后,it指向的内容时10。
为什么会出现这种情况?我们来画图分析一下底层:
我们可能会思考,如果pos传引用不就解决了这类迭代器失效的问题了,实际上并不是,就算传引用解决了这个问题,实际上在其他场景还会出现问题,所以这样做并不合适。
在以上操作完成之后,如果想要继续通过迭代器操作vector中的元素,只需给it重新赋值即可。(返回值接收也行,及时更新it的位置)
指定位置元素的删除操作--erase
举个栗子:
int main()
{
vector v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
vector::iterator it = find(v.begin(), v.end(), 3);
v.erase(it);
cout << *it << endl;
return 0;
}
运行一下:
我们发现程序崩溃了。 我用的是vs2019版本的编译器,如果每次erase后再使用该迭代器就会报错,但是在其他编译器就不一定了。实际上v.erase(it)后,it指向的内容时4。(指向要删除位置的迭代器的下一个迭代器)
调试窗口来看:
erase删除pos位置元素后,pos位置之后的元素会往前搬移,没有导致底层空间的改变,理论上讲迭代器不应该会失效,但是:如果pos刚好是最后一个元素,删完之后pos刚好是end的位置,而end位置是没有元素的,那么pos就失效了。因此删除vector中任意位置上元素时,vs就认为该位置迭代器失效了。
在使用前,对迭代器重新赋值即可
//1、
int main()
{
vector v = { 1, 2, 3, 4 };
auto it = v.begin();
while (it != v.end())
{
if (*it % 2 == 0)
{
v.erase(it);
}
++it;
}
return 0;
}
//2、
int main()
{
vector v{ 1, 2, 3, 4 };
auto it = v.begin();
while (it != v.end())
{
if (*it % 2 == 0)
{
it = v.erase(it);
}
else
{
++it;
}
}
return 0;
}
答案是 2 中的代码是正确的。
因为每次erase之后it都会指向删除元素的下一个位置的迭代器。
对于这种情况:
while (it != v.end())
{
if (*it % 2 == 0)
{
v.erase(it);
}
++it;
}删除元素后it在删除后it已经往后推了一个迭代器,但是下面还有++it,说明刚才指向的位置还没有经过 if(*it%2 == 0)的判断就被++跳过去了。
由此可知,当erase后下面就不用++it了。如果没有erase就++it。
// 1 2 3 4 5 -->最后一个数是奇数&没有连续的偶数 正常
//1 2 4 5 -->有连续的偶数&最后一个数是奇数 没删除完偶数
//1 2 3 4 -->最后一个数是偶数崩溃
由于在VS2019下不好演示,我就在Linux下来展示这个现象:
运行一下: