《STL--- vector的使用及其底层实现》
引言:
上次我们学习了容器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啦!(期待)
