STL的第二站,便是vector了。
对于学习STL,有一个非常大的好处便是,它们有很多函数都是相通的!这也是面向对象的一大好处:背后的函数实现可能不同,但是使用方式相同。
1.简单了解vector
老样子,打开我们的cplusplus——然后惊奇的发现,它换UI了!终于不再是那个2000年初的模样了(虽然这和我们的使用没啥关系)
不摸鱼了,来看看vector
究竟是何方神圣——其实他就是一个顺序表
和string不同的是,vector
有模板参数,可以存储任何类型的内容。int、double、char、甚至string。
这一点在cplusplus网站上的第一行便告诉了我们
1 | template < class T, class Alloc = allocator<T> > class vector; // generic template |
你还可以看到,标题下方给了一个这样的类模板定义,其中T代表的是存放数据类型,allocator<T>
也是STL库中的一部分,用于向堆申请空间(类似一个内存池)可以提高访问的效率
前期的学习我们不需要知道allocator<T>
是怎么实现的,只要知道它有这个功能就ok了。
1.1函数接口
往下滑,你会发现vector的函数接口,我们很多都已经学过了!
这些函数的操作和string
类型都是一样的,这里就不再讲解了!
对比发现,其实vector多出来的函数并不多,这便是学习STL的好处!
2.使用vector
因为大部分函数的功能、操作和string一样,有问题可以去看看我之前string的博客
2.1构造
对于vector而言,比较常用的构造函数是下面这几个
这里说说最后一个,利用迭代器进行初始化,它的使用如下
1 | std::string s1("hello"); |
当我们已经有一个其他的支持迭代器的容器之后,便可以使用该容器的迭代器初始化一个vector
这里的区间可以自行控制,对应的初始化的内容也会有所不同
2.2迭代器
vector的迭代器和string基本相同,都拥有正向和反向两种方式
迭代器 | 说明 |
---|---|
begin+end | 获取第一个数据位置iterator/const_iterator, 获取最后一个数据的下一个位置iterator/const_iterator |
rbegin+rend | 获取最后一个数据位置的reverse_iterator,获取第一个数据前一个位置的 reverse_iterator |
1 | vector<int>::iterator it = v.begin(); |
利用迭代器,我们可以打印vector的内容
2.3迭代器失效问题
在vector中的一些操作会导致迭代器失效,即迭代器指向的内容含义变了。包括但不限于:
- 因为erase导致的数据擦除
- 扩容导致的原有空间被释放(此时迭代器依旧指向原有空间)
- 所有会导致扩容的操作
这时候我们需要借助函数的返回值对it进行重新赋值,比如下图是erase
和insert
的返回参数
我们只需要在使用迭代器遍历的时候用it来接受返回值(更新迭代器)就可以规避这个问题
1 | void test02() |
库里面的其他函数就不进行讲解啦,有问题的可以去看看之前string的博客。当然最好的学习办法就是依据cplusplus上面的函数定义,尝试自己弄明白这些库函数的使用方法。
3.模拟实现
代码详见我的gitee仓库【链接】STL-Vector的源码也在里面!
和string
类不同的是,在vector里面并不是用size和capacity这两个成员变量来确认有效长度和容量的。
通过查看stl源码可以看到,其使用了3个迭代器进行标识
1 | private: |
了解结构后,便可以先把无参构造和析构函数写出来待用
1 | //无参构造函数 |
那么要怎么知道size和capacity的大小呢?很简单,通过指针相减即可得出结果
1 | size_t size()const |
3.0 迭代器
在模拟实现一开始,我们就要用typdef
定义两个迭代器,方便后面的使用
1 | typedef T* iterator; |
3.1 reserve/resize
现在我们的vector内容还是空空的,我们需要先实现一下push_back
函数,可以基本操作一下我们的数据。但没有构造函数去申请空间,哪来的目标给我们操作呢?
- 这时候就需要先实现
reserve
来扩容和获取空间了!
在扩容之前,我们需要获取原有数据的长度,再new将空间整出来,并给_start
。操作结束后,我们需要修改_finish
和_endofstorage
,否则这两个迭代器指向的还是原有空间,出现了迭代器失效问题
1 | void reserve(size_t n)//扩容 |
关于为何拷贝内容的时候需要用=而不能直接memcpy
,后文会提到。
resize和reserve唯一不同的一点,就是reserve会修改原有空间的数据,其余的实现是一样的。所以这里面我们直接复用reserve进行扩容即可
1 | //在C++中,内置类型也有一个“构造函数” |
注意,T()
的含义是用模板参数的构造函数作为缺省值。在C++中,内置类型也可以进行这个操作
当你不进行传值的时候,int类型默认是0
3.2插入/删除
空间拿到了,现在就可以对我们的数据进行插入操作了
1 | void push_back(const T& x) |
简单测试一下插入和删除功能,没啥问题
更好的办法,其实是写好insert和earse,然后尾插尾删的时候直接复用即可
1 | //pos位置插入val |
因为这里我们只需要操作一个元素,所以就不需要担心移动多个数据可能导致的问题,事情也变得简单多了
1 | void push_back(const T& x) |
尾插尾删操作直接复用源码即可
3.3重载[ ]操作符
因为vector本身只是个顺序表,所以我们需要对它的对象重载一下[]
操作符,这样就能和数组一样去访问vector的数据
1 | //重载[]操作符 |
关于构造函数,这里重点介绍一下迭代器区间的构造函数,因为后面的拷贝构造我们会用上它。
3.4迭代器区间构造/拷贝/赋值
库里面关于该构造函数的定义如下
1 | template <class InputIterator> |
因为这时候我的水平还很垃,就暂且不管这个allocator
了
该函数需要我们传参两个迭代器,因为我们底层就是用指针实现的,所以直接解引用然后将内容尾插即可(如果没有空间,尾插会调用reserve来获取空间/扩容)
1 | template <class InputIterator> |
有了这个构造函数后,拷贝构造就很容易了!
1 | void swap(vector<T>& v) |
利用迭代器区间构造出来一个tmp对象,再将该对象和我们本身进行交换即可!
- 你可能会问:欸你这样交换了,原本的内容岂不是没人管了,有内存泄漏啊!
并不是这样的,我们使用库函数里面的swap
交换了二者的成员变量,在拷贝构造函数完成之后,tmp
生命周期结束,会自动调用构造函数处理掉我们的数据!
这就是让别人帮你干活😂
赋值重载就跟简单了,我们连tmp变量都不需要弄,直接使用传值(会自动调用拷贝构造一个临时变量)就可以啦!
1 | //赋值重载(v没有引用,拷贝构造一个新的传参过来,所以不需要手动再构建一个tmp) |
3.5用n个value进行构造
在库中的vector还有一个这样的构造函数,用n个val进行初始化
因为我们之前已经实现了一个相关功能的函数resize
,我们只需要调用resize
就可以完成这个操作!
1 | //利用n个val进行构造 |
只实现一个vector(size_t n, const T& val = T())
的版本是不可以的,因为我们忽略了一种情况
1 | vector<char> v1(10,'x')//可以成功调用 |
为啥第二种情况不行呢?再来看看另外一个构造函数,我们之前实现过的迭代器区间构造
1 | template <class InputIterator> |
你会发现,当我们传的两个参数都是int类型的时候,编译器会直接调用这个模板函数!因为first
和last
就是两个相同类型的参数!
通过调试也可以看到在进行构造的时候,调用了这个迭代器区间构造函数
为了避免这种情况,我们需要对int类型单独处理一下,修改一下传参即可
1 | //如果不单独重载一个int类型版本,就会被识别成上面的模板函数 |
- 这个问题是怎么发现并解决的?
我们可以直接查看stl源码,瞅瞅库里面是怎么实现的
可以看到库里面不但重载了一个int类型的版本,还重载了long
长整型的情况
到这里,我们的模拟实现就基本完成了!
3.6关于自定义类型的拷贝问题
为什么在3.1
的reserve实现中,我使用了赋值重载,而没有使用memcpy
?
来看看下面这个OJ题目杨辉三角的代码
leetcode118杨辉三角:https://leetcode.cn/problems/pascals-triangle/submissions/
1 | class Solution { |
在这里面,我们有两层vector进行嵌套使用,外层的vector存放的是内层vector的对象。
它的结构如下图
这时候如果只用memcpy
进行浅拷贝,相当于新的空间里面存放的是的确是新的vector对象,但这些vector对象存放的却是原有数据的指针,而原本的数据已经被我们delete掉了,出现了野指针问题!
反应到代码上,当我们在Solution
内部进行一次打印,再在外部进行一次打印,结果就会完全不同
1 | void test05() |
使用赋值重载,就可以进行深拷贝,每一个vector对象都能获得新的值,也就不会出现这个问题!
1 | //reserve代码的一部分 |
结语
到这里,vector的模拟实现就完成啦!通过模拟实现可以让我们更好的理解vector容器的结构,也方便后续的使用!
有任何问题,都可以在评论区提出哦!
- 本文标题:【C++】STL:vector
- 创建时间:2022-07-15 13:20:46
- 本文链接:posts/3664653329/
- 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!