✨个人主页: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
{
vector
vector
strings="abcedfg";
vector
return0;
}
注:也可以直接通过vector
vector
1.2、拷贝构造
拷贝构造:将对象x拷贝、构造出新对象v,拷贝构造函数的使用方法很简单,利用一个已经存在的vector对象,创建出一个值相同的对象
vector
vector
可以看到,对象v和对象x的值是一样的(copy)
注意:调用拷贝构造时,两个对象类型需匹配,且被复制对象需存在
拷贝构造和赋值重载有深度拷贝的讲究,在模拟实现vector时演示
1.3、析构函数
析构函数,释放动态开辟的空间,因为vector使用的空间是连续的,所以释放时直接通过delete[]_start释放即可
析构函数会在对象生命周期结束时自动调用,平常在使用时无需关心
//~vector函数内部
delete[]_start;
_start=_finish=_end_of_storage=nullptr;
1.4、赋值重载
拷贝构造的目的是创建一个新对象,赋值重载则是对一个老对象的值进行==改写==
intarr[]={6,6,8};
vector
vector
v2=v1;//将v1的值赋给老对象v2
注意:v1对象赋值给v2对象后,v1本身并不受任何影响,改变的只是v2
赋值重载函数有返回值,适用于多次赋值的情况
vector
v2=v1=v3;//这样也是合法的,最终v1、v2都会受到影响
2、迭代器
迭代器是一个天才设计,它的出现使得各种各样的容器都能以同一种方式进行访问、遍历数据
vector支持下标随机访问,所以大多数情况下访问数据都是使用下标,但迭代器相关接口它还是有的
vector和string的迭代器本质上就是原生指针,比较简单,但后续容器的迭代器就比较复杂了
复杂归复杂,但每种容器的迭代器使用方法都差不多,这就是迭代器设计的绝妙之处
注:string和vector的迭代器都是随机迭代器(RandomAccessIterator),可以随意走动,支持全局排序函数sort
2.1、正向迭代器
正向迭代器即从前往后遍历的迭代器
利用迭代器正向遍历vector对象
constchar*ps="HelloIterator!";
vector
vector
while(it!=v.end)
{
cout<<*it;
it++;
}
cout<
注意:
迭代器在创建时,一定要先写出对应的类型,如vector
在使用迭代器遍历时,结束条件为it!=v.end不能写成<,因为对于后续容器来说,它们的空间不是连续的,判断小于无意义
begin为第一个有效元素地址,end为最后一个有效元素的下一个地址
vector是随机迭代器,也支持这样玩
//auto根据后面的类型,自动推导迭代器类型
autoit=v.begin+3;//这是随机的含义
2.2、反向迭代器
反向迭代器常用来反向遍历(从后往前)容器
反向遍历vector对象
constchar*ps="HelloReverseIterator!";
vector
vector
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
这些函数对于我们太熟悉了,和顺序表的一模一样
直接拿来用一用
vector
cout<<"size:"< cout<<"capacity:"< cout<<"empty:"< 这几个函数都是直接拿来用的,没什么值得注意的地方 3.2、空间扩容 连续空间可扩容,像string一样,vector也有一个提前扩容的函数:reserve 输入指定容量即可扩容,常用来提前扩容,避免因频繁扩容而导致的内存碎片 下面来通过一个小程序先来简单看看PJ版和SGI版的默认扩容机制 vector 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 这种构造方法称为匿名构造,后续会经常简单(很方便) vector v1.resize(10);//使用缺省值 vector 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";
vector
constvector
size_tpos=0;//下标
while(pos { cout< cout< pos++; } cout< 除了operator[]以外,库中还提供了一个at函数,实际就是对operator[]的封装 v.at(0); v[0]//两者是完全等价的 注意:因为是下标随机访问,所以要小心,不要出现越界行为 4.2、首尾元素 front获取首元素,back获取尾元素 vector 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}; vector //在指定位置插入一个值 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 先浅浅演示一下迭代器失效的场景 vector