引言:
上次我们学习了容器string
的使用以及底层实现,今天我们再来学习一个容器vector
。
一:vector
的介绍和使用
1. vector
的介绍:
vector
的文档介绍:
2. vector
的使用:
(1)vector
的定义
- vector()(重点) :无参构造。
- vector(size_type n, const value_type& val =value_type()) :构造并初始化n个
val
。 - vector (const vector& x); (重点) :拷贝构造。
- vector (InputIterator first, InputIterator last); :使用迭代器进行初始化构
造。
代码演示:
(2)vector iterator
的使用
- begin:获取第一个数据位置的
iterator/const_iterator
。 - end:获取最后一个数据的下一个位置的
iterator/const_iterator
。 - rbegin:获取最后一个数据位置的
reverse_iterator
。 - rend:获取第一个数据前一个位置的
reverse_iterator
。
画图理解:
代码演示:
(3)vector
的空间增长
- size:获取数据大小。
- capacity:获取容量大小。
- empty:判断容器是否为空。
- resize(重点):改变容器的
size
(顺带还可以赋值)。 - reserve(重点):改变容器的
capacity
(不能赋值)。
代码演示:
观察VS2022 编译环境下vector
的扩容规则:
注:通过简单测试可以看到在VS2022 的环境下,vector
一般是按照1.5
倍来扩容的,其实每次扩容都需要去堆上申请空间,频繁扩容的话会很影响效率,因此我们建议在知道需要多少空间的时候就可以先一次性开好,这样就可以避免频繁扩容。
(4) vector的增删查改
- push_back:尾插。
- pop_back:尾删。
- find:查找。(这个不是vector的接口,是算法库里面的)
- insert:在
position
之前插入val
。 - erase:删除
position
位置的数据。 - swap:交换两个
vector
里面的数据。 - operator[]:像数组一样访问
vector
。
代码演示:
push_back和erase:
push_back和pop_back:
swap交换两个vector的数据:
insert函数和find函数:
operator[]:
删除线格式
二:vector
模拟实现
1. 基本框架:
注:这里为契合泛型编程,因此这里将vector
实现成了类模板。
2. 构造函数
(1)不带参
(2)带参形式
注:这里的第二个参数val
,由于不知道具体类型,换到之前的话,内置类型是没有构造函数的,但是由于为了填补这里的不便,因此C++中对内置类型进行了升级,这里内置类型也能进行自动构造了,这里通过匿名对象来自动构造,在这里其实也表现出了匿名对象的方便。
(3) initializer list 形式构造(C++11之后支持)
(4)迭代器区间构造
注:由于STL中的容器都有迭代器,也就是说其他容器应该也是可以通过迭代器区间构造的,因此这里的迭代器区间构造写成了一个类模版的形式。
3. 拷贝构造函数:
注:这里拷贝构造函数的参数也是要用引用,避免发生无限递归。
4. 析构函数:
注:这里为了更严谨,在释放空间之前先判断指针是否为空。
5. capacity
函数(获取容量)
6. size
函数(获取数据个数)
7. 迭代器
(1)普通迭代器:
(2)const
类型迭代器:
8. empty
函数:
9. []
运算符重载:
10. push_back
函数:
(1) 分析:
凡是要插入数据,就必须先考虑空间是否足够,若空间不够的话要先扩容,然后再进行数据的插入,这样就分为两种情况:
- 空间足够的话,直接插入数据。
- 空间不足,先扩容之后再插入数据。
(2) 代码实现:
11. reserve
函数
注:这里特别要注意几个点:
- 在转移旧数据之前,先判断旧空间是否为空。
- 在进行拷贝的时候,由于可能会牵扯到深浅拷贝的问题,因此这里我们就不用
mencpy
了,因为memcpy
是逐个字节拷贝(浅拷贝),当面对string
这样的数据时就会引起析构时的错误,因此这里我们直接改为一个个赋值就能解决这些问题。
12. pop_back
函数:
13. insert 函数:
(1)分析:
(2) 代码实现:
注:这里注意几个点:
- 我们这里首先判断
pos
的值是否合法。 - 这里我们在扩容之前先计算了
pos
相对于起始点的相对距离,在扩容之后又更新了pos
的值,这里就是为了防止异地扩容导致的迭代器失效。(这个我们后面再讲解)
14. erase
函数:
(1)分析:
(2)代码实现:
15. resize
函数:
(1)分析:
这里的resize
分为两种情况:
- 增大空间,这时候只需要扩容,然后再根据传入的指定值进行初始化即可。
- 缩小空间,只需修改
_finish
即可(不改变_capacity
)。
(2)代码实现:
16. swap
函数:
这里还是和之前的string
类一样,为了减少代价,因此我们来自己实现swap
函数。
17. =
运算符重载:
这里我们就直接复用前面实现好的拷贝构造即可。
注:这里的传值传参会调用拷贝构造,拷贝构造出一个临时对象v
,然后将这个临时对象和*this
交换,这样v
就会拿到*this
的地址,由于v
是临时对象,出作用域就会自动析构,也就不需要我们自己去释放*this
的空间。(极限的压榨)
三:关于迭代器失效问题探讨:
1. insert
引起的迭代器失效
insert
造成的迭代器失效,归根结底还是因为扩容的时候如果异地扩容的话就会导致迭代器的失效。
但是我们这样还是不能从根源上解决问题,就比如VS2022环境下的编译器是非常严格的,这时候只要异地扩容了,编译器就会给POS
打上标记,你访问POS
就会出错。
看下面这个场景:
这里就直接崩掉了。
那么怎么从根源上解决问题呢? 这时候就需要对迭代器进行重新赋值,让insert
函数返回POS
更新后的值即可。
在对迭代器POS
进行重新赋值之后就可访问了。
2. erase 缩容引起的迭代器失效
既然扩容时的异地扩容会引起迭代器的失效,那么erase
在删除数据时,当数据很少,不需要那么大的空间时,它是有可能会异地开辟一段空间进行异地缩容的,因此这里也会引起迭代器的失效。
为了能显示出这个效果,这里我们用到的是标准库里面的erase
函数。
给出下面这个场景:
注:这里在对it
位置进行erase
之后,编译器就会认为迭代器it
就失效了,你要是再访问的话就会出错。
那么怎么解决这个问题呢?还是对迭代器重新赋值。
我们可以来看一下标准库里面的erase
是怎么实现的:
可以看到标准库里面的erase
函数是有返回值的,这也是为了解决迭代器失效的问题。
当我们对迭代器进行重新赋值后就没问题了:
因此我们模拟实现的erase
函数最好也是和标准库里面的保持一致:
3. 小结:
通过上面两个案例,想必大家已经有点感觉了,凡是牵扯到的异地的空间改变,都是有可能导致迭代器的失效的,因此在使用迭代器的时候就要谨慎一些。
四:完结!!!
下次我们就是来学习下一个容器list
啦!(期待)