当前位置: 首页 > 资讯 > > 内容页

C++STL学习之[vector的使用]

发布时间:2023-03-12 23:49:33 来源:阿里云

✨个人主页:Yohifo

????所属专栏:C++修行之路


【资料图】

????每篇一句:图片来源

Thepowerofimaginationmakesusinfinite.

想象力的力量使我们无限。

????前言

vector是表示可变大小数组的序列容器,其使用的是一块连续的空间,因为是动态增长的数组,所以vector在空间不够时会扩容;vector优点之一是支持下标的随机访问,缺点也很明显,头插或中部插入效率很低,这和我们之前学过的顺序表性质很像,不过在结构设计上,两者是截然不同的

????正文

本文介绍的是vector部分常用接口

1、默认成员函数

vector的成员变量如上图所示,就是三个指针,分别指向:

_start指向空间起始位置,即begin

_finish指向最后一个有效元素的下一个位置,相当于end

_end_of_storage指向已开辟空间的终止位置

1.1、默认构造

vector支持三种默认构造方式

默认构造大小为0的对象

构造n个元素值为val的对象

通过迭代器区间构造,此时元素为自定义类型,如string、vector、Date等

intmain

{

vectorv1;//构造元素值为int的对象

vectorv2(10,"x");//构造10个值为"x"的对象

strings="abcedfg";

vectorv3(s.begin,s.end);//构造s区间内的元素对象

return0;

}

注:也可以直接通过vectorv4={1,2,3}的方式构造对象,不过此时调用了拷贝构造函数

vectorv4={1,2,3};//这种构造方式比较常用,有点像数组赋初始值

1.2、拷贝构造

拷贝构造:将对象x拷贝、构造出新对象v,拷贝构造函数的使用方法很简单,利用一个已经存在的vector对象,创建出一个值相同的对象

vectorx={1,2,3,4,5};

vectorv(x);//利用对象x构造出v

可以看到,对象v和对象x的值是一样的(copy)

注意:调用拷贝构造时,两个对象类型需匹配,且被复制对象需存在

拷贝构造和赋值重载有深度拷贝的讲究,在模拟实现vector时演示

1.3、析构函数

析构函数,释放动态开辟的空间,因为vector使用的空间是连续的,所以释放时直接通过delete[]_start释放即可

析构函数会在对象生命周期结束时自动调用,平常在使用时无需关心

//~vector函数内部

delete[]_start;

_start=_finish=_end_of_storage=nullptr;

1.4、赋值重载

拷贝构造的目的是创建一个新对象,赋值重载则是对一个老对象的值进行==改写==

intarr[]={6,6,8};

vectorv1(arr,arr+(sizeof(arr)/sizeof(arr[0])));//迭代器区间构造

vectorv2;//创建一个空对象

v2=v1;//将v1的值赋给老对象v2

注意:v1对象赋值给v2对象后,v1本身并不受任何影响,改变的只是v2

赋值重载函数有返回值,适用于多次赋值的情况

vectorv3={9,9,9};

v2=v1=v3;//这样也是合法的,最终v1、v2都会受到影响

2、迭代器

迭代器是一个天才设计,它的出现使得各种各样的容器都能以同一种方式进行访问、遍历数据

vector支持下标随机访问,所以大多数情况下访问数据都是使用下标,但迭代器相关接口它还是有的

vector和string的迭代器本质上就是原生指针,比较简单,但后续容器的迭代器就比较复杂了

复杂归复杂,但每种容器的迭代器使用方法都差不多,这就是迭代器设计的绝妙之处

注:string和vector的迭代器都是随机迭代器(RandomAccessIterator),可以随意走动,支持全局排序函数sort

2.1、正向迭代器

正向迭代器即从前往后遍历的迭代器

利用迭代器正向遍历vector对象

constchar*ps="HelloIterator!";

vectorv(ps,ps+strlen(ps));//迭代器构造

vector::iteratorit=v.begin;//创建该类型的迭代器

while(it!=v.end)

{

cout<<*it;

it++;

}

cout<

注意:

迭代器在创建时,一定要先写出对应的类型,如vector,嫌麻烦可以直接用auto推导

在使用迭代器遍历时,结束条件为it!=v.end不能写成<,因为对于后续容器来说,它们的空间不是连续的,判断小于无意义

begin为第一个有效元素地址,end为最后一个有效元素的下一个地址

vector是随机迭代器,也支持这样玩

//auto根据后面的类型,自动推导迭代器类型

autoit=v.begin+3;//这是随机的含义

2.2、反向迭代器

反向迭代器常用来反向遍历(从后往前)容器

反向遍历vector对象

constchar*ps="HelloReverseIterator!";

vectorv(ps,ps+strlen(ps));

vector::reverse_iteratorit=v.rbegin;//创建该类型的迭代器

while(it!=v.rend)

{

cout<<*it;

it++;

}

cout<

反向迭代器的注意点与正向迭代器一致,值得注意的是rbegin和rend

begin和end适用于正向迭代器

begin为对象中的首个有效元素地址

end为对象中最后一个有效元素的下一个地址

rbegin和rend适用于反向迭代器

rbegin为对象中最后一个有效元素地址

rend为对象中首个有效元素的上一个地址

注意:begin不能和rend混用

上述迭代器都是用于正常可修改的对象,对于const对象,还有cbegin、cend和crbegin、crend,当然这些都是C++11中新增的语法

注:对于const对象,存在重载版本,如beginconst,也就是说,const修饰的对象也能正常使用begin、end、rbegin和rend;C++11中的这个新语法完全没必要,可以不用,但不能看不懂

3、容量相关

下面来看看vector容量相关函数和扩容机制

3.1、大小、容量、判空

大小size

容量capacity

判空empty

这些函数对于我们太熟悉了,和顺序表的一模一样

直接拿来用一用

vectorv={1,2,3,4,5};

cout<<"size:"<

cout<<"capacity:"<

cout<<"empty:"<

这几个函数都是直接拿来用的,没什么值得注意的地方

3.2、空间扩容

连续空间可扩容,像string一样,vector也有一个提前扩容的函数:reserve

输入指定容量即可扩容,常用来提前扩容,避免因频繁扩容而导致的内存碎片

下面来通过一个小程序先来简单看看PJ版和SGI版的默认扩容机制

vectorv;

size_tcapacity=v.capacity;

cout<<"Defaultcapacity:"<

inti=0;

while(i<100)

{

v.push_back(i);//尾插元素i

//如果不相等,证明出现扩容

if(capacity!=v.capacity)

{

capacity=v.capacity;

cout<<"Newcapacity:"<

}

i++;

}

可以看出,PJ版采用的是1.5倍扩容法,而SGI版直接采用2倍扩容法,待扩容量较小时,PJ版会扩容更多次,浪费更多空间;但待扩容量越大时,变成SGI版浪费更多空间,总的来说,两种扩容方式各有各的优点

如果我们提前知道待扩容空间大小n,可以直接使用reserve(n)的方式进行提前扩容,这样一来,无论是哪种版本,最终容量大小都是一致的,且不会造成空间浪费

v.reserve(100);//提前开辟空间

此时是非常节约空间的,而且不会造成很多的内存碎片

注意:当n小于等于capacity时,reserve函数不会进行操作

3.3、大小调整

与提前扩容相似的大小调整,主要调整的是_finish

在扩容的同时对新空间进行初始化,参数2val为缺省值,缺省为对应对象的默认构造值

自定义类型也有默认构造函数,如int,构造后为0

这种构造方法称为匿名构造,后续会经常简单(很方便)

vectorv1;

v1.resize(10);//使用缺省值

vectorv2;

v2.resize(10,6);//使用指定值

区别在于:是否指定初始化值

resize和reserve:

两者的共同点是都能起到扩容的效果

resize扩容的同时还能进行初始化,reserve则不能

resize会改变_finish,而reserve不会

对于两者来说,当n小于等于capacity时,都不进行扩容操作

resize此时会初始化size至capacity这段空间

3.4、缩容

vector中还提供一个了缩容函数,将原有容量缩小,但这完全没必要,以下是缩容步骤:

开辟新空间(比原空间更小的空间)

用原空间中的数据将新空间填满,超出部分丢弃

释放原空间,完成缩容

为了一个缩容而导致的是代价是很大的,因此不推荐缩容,想要改变size时,可以使用resize函数

这里就不演示这个函数了,就连官方文档上都有一个警告标志

4、数据访问相关

连续空间数据访问时,可以通过迭代器,也可以通过下标,这里还是更推荐使用下标,因为很方便;作为“顺序表”,当然也支持访问首尾元素

4.1、下标随机访问

下标访问是通过operator[]运算符重载实现的

库中提供了两个重载版本,用以匹配普通对象和const对象

constchar*ps="Hello";

vectorv(ps,ps+strlen(ps));//迭代器区间构造

constvectorcv(ps,ps+strlen(ps));//迭代器区间构造

size_tpos=0;//下标

while(pos

{

cout<

cout<

pos++;

}

cout<

除了operator[]以外,库中还提供了一个at函数,实际就是对operator[]的封装

v.at(0);

v[0]//两者是完全等价的

注意:因为是下标随机访问,所以要小心,不要出现越界行为

4.2、首尾元素

front获取首元素,back获取尾元素

vectorv={1,1,1,0,0,0};

cout<<"Front:"<

cout<<"Back:"<

实际上,front就是返回*_start,back则是返回*_finish

5、数据修改相关

vector也可以随意修改其中的数据,比如尾部操作,也支持任意位置操作,除此之外,还能交换两个对象,亦或是清除对象中的有效元素

5.1、尾插尾删

push_back和pop_back算是老相识了,两个都是直接在_finish上进行操作

这两个函数操作都很简单,不再演示

注意:如果对象为空,是不能尾删数据的

对于已有对象数据的修改,除了赋值重载外,还有一个函数assign,可以重写指定对象中的内容,使用方法很像默认构造函数,但其本质又和赋值重载一样

第一种方式是通过迭代器区间赋值,第二种是指定元素数和元素值赋值

5.2、任意位置插入删除

任意位置插入删除是使用vector的重点,因为这里会涉及一个问题:迭代器失效,这个问题很经典,具体什么原因和如何解决,将在模拟实现vector中解答

简单演示一下用法:

intarr[]={6,6,6};

vectorv={1,0};

//在指定位置插入一个值

v.insert(find(v.begin,v.end,1),10);//10,1,0

//在指定位置插入n个值

v.insert(find(v.begin,v.end,0),2,8);//10,1,8,8,0

//在指定位置插入一段迭代器区间

v.insert(find(v.begin,v.end,8),arr,arr+(sizeof(arr)/sizeof(arr[0])));//10,1,6,6,6,8,8,0

//删除指定位置的元素

v.erase(find(v.begin,v.end,10));//1,6,6,6,8,8,0

//删除一段区间

v.erase(v.begin+1,v.end);//1

先浅浅演示一下迭代器失效的场景

vectorv={1,2,3};

推荐阅读