c++stl vector,stl vector用法
Yyds干货库存
在写作之前,我们已经了解了字符串。现在我们开始学习向量。我们在这里会发现,STL的设计非常相似。我们学了一个,很简单。我们先简单看一下vector的用法。今天重点讲迭代器失效和深层次复制,这是我们的难点。
Vector使用vector可以理解为我们之前的动态数组,也就是序列表。它也是一个类模板。我们先来看看它的简单用法。它的第一个参数模板是要存储的数据类型,第二个是为内存池打开空间。如果你觉得库中的内存池不够高效,你也可以自己写一个传过去,但这不是我们想说的。
如果构造函数向量包含复制构造,则有四个构造函数。下面就简单介绍一下。
函数名
解释
向量();
无参数结构
vector (size_type n,const value _ type val=value _ type());
n的构造值的元素
vector (InputIterator第一,InputIterator最后);
迭代区间构造
向量(常数向量x);
复制结构
这里我们还是自测两个常用的。
int main()
{
向量int v1
向量char v2(10, a );
返回0;
}
Size() capacity()也是我们经常用到的函数,没什么好解释的。
int main()
{
向量int v1(20,15);
cout size: v1 . size()endl;
cout capacity: v1 . capacity()endl;
返回0;
}
函数reserve()是为了开辟空间,和string一样,规则还是一样的。
int main()
{
向量int v1(20,15);
v1 .储备(10);
cout capacity: v1 . capacity()endl;
v1 .储备(100);
cout capacity: v1 . capacity()endl;
返回0;
}
Resize()并不是什么新东西。还是和以前一样。
int main()
{
向量int v1(20,15);
v1.resize(10,0);
cout size: v1 . size()endl;
v1.resize(100,0);
cout size: v1 . size()endl;
返回0;
}
运算符[]的底层是一个连续数组,所以我们这里最好支持下标访问。
int main()
{
向量int v1(20,15);
for(int I=0;I v1 . size();我)
{
cout v1[I]“”;
}
cout endl
返回0;
}
迭代器这里的迭代器分为正向迭代器和反向迭代器,每一个都用const修改。这里不做过多解释,用法和之前一样。
int main()
{
向量int v1
v1 . push _ back(1);
v1 . push _ back(2);
v1 . push _ back(3);
v1 . push _ back(4);
v1 . push _ back(5);
vector int:iterator it=v1 . begin();
而(它!=v1.end())
{
cout * it“”;
它;
}
cout endl
返回0;
}
Push_back()在末尾插入一个数据,在末尾插入一个数据。
int main()
{
向量int v1
v1 . push _ back(1);
v1 . push _ back(2);
v1 . push _ back(3);
v1 . push _ back(4);
v1 . push _ back(5);
for(int I=0;I v1 . size();我)
{
cout v1[I]“”;
}
cout endl
返回0;
}
Vector实现了,终于过了上面的认知阶段。最主要的原因是我们无话可说,和string几乎一模一样。不用说?我们现在想看的是他们的实现,这很有意思。
我们先推一波向量的底层。应该有一个大小和容量来记录有效的数据和容量,还应该有一个数组来存储数据。我认为这一点不应该有任何疑问。我们来看看SGI版本的实现,里面存储了三个指针(类型化def)。这个方法也是可以的,而且比我们的简单。我们就用这个来实现吧。
让我们画出它的物理图,以便大家更好地理解它。
现在我们可以建立框架,让我们建立一个简单的。
模板类T
分类向量
{
公共:
typedef T*迭代器;//本机指针
typedef const T * const _ iterator
向量()
:_start(nullptr)
,_finish(nullptr)
,_endOfStoage(nullptr)
{
}
私人:
iterator _ start
iterator _ finish
iterator _ endOfStoage
}迭代器vector和string是一样的,都是原生指针,我们可以直接使用。
迭代器begin()
{
返回_开始;
}
const_iterator begin() const
{
返回_开始;
}
迭代器结束()
{
返回_完成;
}
const_iterator end() const
{
返回_完成;
}size() capacity()的实现就更简单了。我们知道,数据的个数可以通过指针减去指针得到,这里也适用。这里直接用吧。
size_t size()常量
{
return _ finish-_ start;
}
//计算容量
size_t容量()常量
{
return _ endOfStoage-_ start;
}operator[]既然vector的物理地址是连续的,那我们最好支持operator[]的随机访问,太简单了。
测试运算符[](size_t pos)
{
assert(pos=0 pos size());
return *(_开始位置);
}
常量T运算符[](size_t pos)常量
{
assert(pos=0 pos size());
return *(_开始位置);
}reserve()这是扩展和释放热量的功能,但是这里面可以有一个很重要的问题,深浅复制的深层次问题,这是我们今天博客最重要的内容之一。我们稍后将分享它们。
空预留(size_t n=0)
{
size _ t oldsize=size();
if(n capacity())
{
//打开一个空间
T * tmp=new T[n];
//确定原始空间中是否有数据。
if(_start)
{
memcpy(tmp,_start,size()* sizeof(T));
delete[]_ start;
}
_ start=tmp
_ finish=tmp oldsize
_ endOfStoage=tmp n;
}
}resize()是调整_size大小的函数。这里我们仍然只实现一种情况。
void resize(size_t n,const T val=T())
{
//三种情况
储备(n);
if(n size())
{
while(size() n)
{
//
* _ finish=val
_完成;
}
}
其他
{
_ finish=_ start n;
}
}insert()这是在一个地址插入一个元素,我们默认插入在元素前面,有迭代器失效的问题。
迭代器插入(迭代器位置,常数T x)
{
assert(pos=_ start pos=_ finish);
if(_finish==_endOfStoage)
{
size _ t len=pos-_ start;//记录以防止无效
size _ t new cap=_ endOfStoage-_ start==0?4 : 2 *容量();
储备(new cap);
//更新pos解决了部分迭代器失败问题。
pos=_ start len
}
//开始插入数据
迭代器it=_ finish
而(它!=位置)
{
* it=*(it-1);
它-;
}
* pos=x;
_完成;
退货位置;
}push_back()复用插入函数。
void push_back(常量值)
{
插入(_finish,val);
}erase()这次复杂度达到了O(N),确实有点高,但是我们也无能为力。
迭代器擦除(迭代器位置)
{
assert(pos=_ start pos _ finish);
迭代器it=pos 1;
而(它!=_完成)
{
*(it-1)=* it;
它;
}
_完成-;
退货位置;
}pop_back()尾部删除的时间复杂度O(1)。
void pop_back()
{
if(size()!=0) //在这里,最好不要用空的空间来判断清的恐惧。
{
-_完成;
}
}swap的主要功能是复制结构等。我们只需要交换三个指针。
无效交换(向量转换)
{
std:swap(_start,v . _ start);
std:swap(_finish,v . _ finish);
std:swap(_endOfStoage,v . _ endOfStoage);
}迭代器失败现在来说说向量中最难理解的问题。迭代器失败,我们需要控制向量的使用以避免错误。向量的迭代器失效大致可以分为三种类型。我们一个一个来举例。
先来看看标准库,还是会有野指针的问题。
#包括iostream
#包含矢量
使用命名空间std
int main()
{
向量int v(10,1);
vector int:iterator pos=v . begin();
int I=0;
而(i 100)
{
v .插入(位置2);
我;
}
返回0;
}
让我们看看问题发生在哪个步骤,并将其打印出来。我们发现它出现在我等于1的时候。
我们需要看看第一次插入后pos是否失败。VS有点严格。
int main()
{
向量int v(10,1);
vector int:iterator pos=v . begin();
printf(pos : %p\n ,pos);
int I=0;
而(i 100)
{
如果(i==1)
{
printf(pos : %p\n ,pos);
Cout test endl
}
v .插入(位置2);
我;
}
返回0;
}
所以我们要看看Linux环境,不同的编译器有不同的处理机制,这一点我们之前已经知道了。
从这里我们可以看到,只有在我们插入一些数据的时候才会出现错误,而且是在i=10的时候。这个现象可以解释迭代器失败的原因之一。先调试一下吧。
先说原因。当我们插入数据时,我们使用迭代器,准确地说,是原生指针。如果我们扩展hungry oh中的原始指针,那么现在发生的事情就会发生,迭代器将无法指向内容。
我们可以用某种方式解决这个插入问题。我们可以记录pos和_start之间的相对距离,扩展后做相应的改变。
迭代器插入(迭代器位置,常数T x)
{
assert(pos=_ start pos=_ finish);
if(_finish==_endOfStoage)
{
size _ t len=pos-_ start;//记录以防止无效
size _ t new cap=_ endOfStoage-_ start==0?4 : 2 *容量();
储备(new cap);
//更新pos解决了部分迭代器失败问题。
pos=_ start len
}
//开始插入数据
//.
退货位置;
}对不起,你觉得你现在写的是对的吗?看一下下面的代码。我们想在它前面加上这个偶数的十倍。怎么样?
# include iostream # include vector . HPP
使用STD:cout;
使用STD:CIN;
使用STD:endl;
int main()
{
bit:Vector int v;
五. push _ back(1);
五. push _ back(2);
五. push _ back(3);
五. push _ back(4);
五. push _ back(5);
五. push _ back(6);
bit:Vector int:iterator it=v . begin();
而(它!=v.end())
{
if (*it % 2==0)
{
int ret=* it * 10
v.insert(it,ret);
}
它;
}
for (int val : v)
{
cout val“”;
}
返回0;
}
好吧,还有一个问题。看这里,我们可以看到迭代器失败的另一个问题。我们不是更新了pos吗?这里面还是有一些问题的。我们引入形式参数,改变形式参数不会影响实际参数。那么,我们可以介绍推荐人吗?是的,我们能,但是我们不能在标准图书馆里做它。
我们以返回值的形式解决这个问题。
迭代器插入(迭代器位置,常数T x)
{
assert(pos=_ start pos=_ finish);
//更新pos解决了部分迭代器失败问题。
//开始插入数据
//.
退货位置;
}
还有一个原因,为什么我们不必引用这个。要知道,insert支持以下用法。临时变量有恒定性,所以我们要用const来修改。
导致返回值迭代器失败。如果你认为我们的代码现在可以正常运行,那你就太天真了。运行之后你会发现还是有问题。
# include iostream # include vector . HPP
使用STD:cout;
使用STD:CIN;
使用STD:endl;
int main()
{
bit:Vector int v;
五. push _ back(1);
五. push _ back(2);
五. push _ back(3);
五. push _ back(4);
五. push _ back(5);
五. push _ back(6);
bit:Vector int:iterator it=v . begin();
而(它!=v.end())
{
if (*it % 2==0)
{
int ret=* it * 10
v.insert(it,ret);
}
它;
}
for (int val : v)
{
cout val“”;
}
返回0;
}
现在我们用标准库看看这种情况是否也存在。
#包括iostream
#包含矢量
使用命名空间std
int main()
{
向量int v;
五. push _ back(1);
五. push _ back(2);
五. push _ back(3);
五. push _ back(4);
五. push _ back(5);
五. push _ back(6);
vector int:iterator it=v . begin();
而(它!=v.end())
{
if (*it % 2==0)
{
int ret=* it * 10
v.insert(it,ret);
}
它;
}
for (int val : v)
{
cout val“”;
}
返回0;
}
这个也崩溃了,说明至少我们的实现没有错误。这样做的原因是返回值。让我们看一看。
现在你应该有线索了。我们的返回值是新插入的迭代器。检查一下。
int main()
{
向量int v;
五. push _ back(1);
五. push _ back(2);
五. push _ back(3);
vector int:iterator it=v . begin();
it=v.insert(it,10);
返回0;
}
也就是说,我们在插入元素后需要移动迭代器,移动与否由你决定。
#包括iostream
#包含矢量
使用命名空间std
int main()
{
向量int v;
五. push _ back(1);
五. push _ back(2);
五. push _ back(3);
五. push _ back(4);
五. push _ back(5);
五. push _ back(6);
vector int:iterator it=v . begin();
而(它!=v.end())
{
if (*it % 2==0)
{
int ret=* it * 10
v.insert(it,ret);
它;
}
它;
}
for (int val : v)
{
cout val“”;
}
返回0;
}
擦除导致迭代器失败。vector还有第三个迭代器失败,主要是保留的情况。我们先来看看Erase在图书馆的大致情况。一般这种情况是不可以遇到的,因为无论是VS编译器还是G编译器似乎都不会收缩,但是我们需要知道这种情况。
使用命名空间std
int main()
{
向量int v;
五. push _ back(1);
五. push _ back(2);
五. push _ back(3);
五. push _ back(4);
五. push _ back(5);
五. push _ back(6);
vector int:iterator it=v . begin();
而(它!=v.end())
{
if (*it % 2==0)
{
it=v . erase(it);//注意it验收就行了。
}
其他
{
它;
}
}
for (int val : v)
{
cout val“”;
}
返回0;
}
总结vector的迭代器故障主要有两种类型。
由于扩展(插入)或收缩(擦除),它变成了一个通配符指针。因为指向的意义改变了,就像erase删除元素的下一个位置,insert也是插入元素的下一个位置,这就导致了迭代器的自增或自减的合理使用。不同编译器对迭代器失败的处理机制还是有很大差别的。这里重点讲一下VS和g,这个VS是严格的。只要插入或删除一次,无论是扩展还是收缩,都无法访问当前地址。
#包括iostream
#包含矢量
使用命名空间std
int main()
{
向量int v(10,1);
vector int:iterator it=v . begin();
v .插入(it,2);
cout v . capacity()endl;
* it
返回0;
}
int main()
{
向量int v(10,1);
vector int:iterator it=v . begin();
v .擦除(它);
cout v . capacity()endl;
* it
返回0;
}
但是按照G中的佛教体系,一般是可以访问的。如果遇到通配符指针,将单独报告一个错误。
#包括iostream
#包含矢量
使用命名空间std
int main()
{
向量int v(10,1);
vector int:iterator it=v . begin();
v .插入(it,2);
cout v . capacity()endl;
* it
返回0;
}
int main()
{
向量int v(10,1);
vector int:iterator it=v . begin();
v .擦除(它);
cout v . capacity()endl;
* it
返回0;
}
完善vector这里需要完善vector,包括复制构造,赋值重载等。这些都比较简单。先抄施工吧。
文案建设这里我们需要写现代文,还是比较简单的。
Vector(const Vector T v) //这里建议这样写。当然,类中的const Vector v也可以这样做,但不推荐。
:_start(nullptr)
,_finish(nullptr)
,_endOfStoage(nullptr)
{
//现代写作
向量T vv
for(常量值:v)
{
vv . push _ back(val);
}
//我让你帮我写,你要把变量vv解构。
互换(vv);
}赋值重载更简单,可以说是厉害的一批。
向量T算子=(向量T v)
{
//这里我们不传入引用,这里编译器自动调用copy构造,我们可以直接传入。
互换(五);
返回* this
析构函数在这里写析构函数可以避免内存泄漏。
~向量()
{
if(_start)
{
delete[]_ start;
_ finish=nullptr
_ endOfStoage=nullptr
}
}多值构造这里,我们把这个构造过程中一次初始化多个值的构造函数写下来。在这里,您可以重用尾部插入。
Vector(size_t n,const T value=T())
:_start(nullptr)
,_finish(nullptr)
,_endOfStoage(nullptr)
{
向量T vv
for(size _ t I=0;I n;我)
{
vv.push_back(值);
}
互换(vv);
}区间构造有时候我们可能会用区间构造,也有一个问题,主要是上面的多值构造有问题。你会知道什么时候轮到你叫牌。
模板类输入运算符
Vector(先输入运算符,后输入运算符)
:_start(nullptr)
,_endOfStoage(nullptr)
,_finish(nullptr)
{
向量T vv
而(先!=最后一个)
{
vv . push _ back(* first);
第一;
}
互换(vv);
}这里先测试一下,主要是看这个错误信息。
int main()
{
//排除方法
向量int v1(10,2);
用于(自动e : v1)
{
cout e“”;
}
cout endl
向量字符v2(10, x );
用于(自动e : v2)
{
cout e“”;
}
cout endl
返回0;
}
先排除第一个,你会发现第一个构造函数是错的。我们需要分析它。
int main()
{
向量字符v2(10, x );
用于(自动e : v2)
{
cout e“”;
}
cout endl
返回0;
}
先说明一下这个情况。对于编译器,我们会自动推导出参数的类型。我们发现都是int,所以编译器有理由相信我们调用的是区间构造。由于多值构造的两个参数不同,编译器必须优先考虑区间构造,这就是报错的原因。
源代码中对此的解决方案是将第一个参数的类型改为int。
向量(int n,const T value=T())
:_start(nullptr)
,_finish(nullptr)
,_endOfStoage(nullptr)
{
向量T vv
for(int I=0;I n;我)
{
vv.push_back(值);
}
互换(vv);
}更深更深复制到这里,剩下vector最后一大内容。这个模块有点难,需要我们很好的理解。这里,我先举个简单的例子。在标准库中,我们可以嵌套vector,就像下面这样。
#包括iostream
#包含矢量
使用命名空间std
int main()
{
//排除方法
vector向量int vv
向量int v1(10,1);
向量int v2(10,2);
向量int v3(10,3);
向量int v4(10,4);
向量int v5(10,5);
vv . push _ back(v1);
vv . push _ back(v2);
vv . push _ back(v3);
vv . push _ back(v4);
vv . push _ back(V5);
for(向量整数值:vv)
{
for(整数:整数)
{
cout e“”;
}
cout endl
}
返回0;
}
但是我们不能为了自己的写作而这样做。我们先运行一下,你就知道了。
#包括iostream
#包含“Vector.hpp”
使用命名空间std
int main()
{
bit:Vector bit:Vector int vv;
bit:Vector int v1(10,1);
bit:Vector int v2(10,2);
bit:vector int v3(10,3);
bit:vector int v4(10,4);
bit:vector int v5(10,5);
vv . push _ back(v1);
vv . push _ back(v2);
vv . push _ back(v3);
vv . push _ back(v4);
vv . push _ back(V5);
for(位:向量int val : vv)
{
for(整数:整数)
{
cout e“”;
}
cout endl
}
返回0;
}
这个报告有错误,这里就不让你想了。这里的主要问题在于插入和保留函数。我们先来解释一下向量嵌套向量的性质。
当vector\中的T是自定义类型的数组时,通常会出现浅拷贝的问题。我们在这里的实现在于memcpy函数。对于一般的整数数组,它复制数组中的值,这是一种深度复制。但是T这里是自定义类型的数组,也就是我们的vector存储的是地址,所以这个时候地址是复制的。如果你知道我们以后还需要删除,你会为自定义类型调用自己的析构函数,这会导致原指针变成野指针。我们先过一遍原码,这样你会有一点了解。这里的赋值重载是深度复制,没有问题。
不扩展容量时,插入数据。步骤如下。vector的数组被复制到vector的数组中,数组中的每个元素都会被调用赋值重载。这是一份深度拷贝。
插入的时候会有问题。我们使用memcpy函数,它将原始数组的所有元素复制到tmp,知道复制的指针,
下一个关键步骤是我们把original _start给删除了。要知道,自定义类型数组的所有指针都存储在里面,编译器会逐个释放空间。我们的tmp变成了野指针。
解决这个问题是个好主意。我们不用memcpy函数,一个一个赋值。内置类型也一样,我们会为自定义类型调用自己的赋值重载,所以没问题。
空预留(size_t n=0)
{
size _ t oldsize=size();
if (n capacity())
{
//打开一个空间
T * tmp=new T[n];
//确定原始空间中是否有数据。
if (_start)
{
//在这里,直接赋值会自动调用自定义类型的赋值重载-深度复制。
for(int I=0;I size();我)
{
tmp[I]=_ start[I];
}
//memcpy(tmp,_start,size()* sizeof(T));
delete[]_ start;
}
_ start=tmp
_ finish=tmp oldsize
_ endOfStoage=tmp n;
}
}
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。