迭代器主要的作用就是为了可以像数组那样实现指针向后移动到下一个数据。同时迭代器统一了所有容器,让所有容器可以通过迭代器互通数据。
那么下面我们来看看迭代器
数组的优势
我们数组的优势就是内存连续,那么我们将首地址的地址进行加减就可以访问上一个数据和下一个数据:
大概就是得到开头的指针,然后得到最后一个数据的下一个指针,然后我们就可以用这种方法逐一操作数据了。
迭代器
此时我们稍微将这个指针包装一下:
这大概就是迭代器的大概意思。
iterator就是迭代器的名字,所用容器的迭代器都叫这个名字
迭代器将所有的容器都视为数组的“连续内存形式”,这样我们就可以使用指针的加减操作。
各种容器迭代器的使用方法
容器名::iterator
定义迭代器要指定是哪个容器的迭代器
因为不同容器的内存形式是不同的,像链表就是碎片化的内存,那么我们的迭代器为了实现“数组的形式”,就要实现一个类来重载++、--这些操作,就是模拟指向下一个节点,上一个节点。所以每个容器的迭代器底层是不同的,所以为了保证迭代器名字相同就要typedef 一下。所以迭代器是名字一样,实现目的一样,但是实现的过程不同。
这些不同底层的迭代器都是在每个容器的类里面定义的,所以要加上空间限制符来指定你现在是用的哪个类的迭代器
头尾迭代器的调用
迭代器的开始就是类.begin(),尾就是类.end()
举例:
string:
vector:
list:
这里面的迭代器都是指向内容的地址指针,解引用后才是所要的数据。
既然是获取到它的地址,那么我们可以用迭代器做很多事情。改变数据,获取数据都是很方便的。
迭代器的种类
每个容器内部迭代器分类
每个容器实现的迭代器作用是基本相同的。我们有时候不想要可以改变值的迭代器,所以就有了const_iterator类的迭代器,同时我们想要反向遍历的迭代器,所以有了reserve_iterator
iterator
上面做了详细的讲解,就是正向的迭代器。
const_iterator
为什么我们不直接用const iterator呢?
这里我们要这样看:用string的迭代器来举例,string的迭代器底层应该就是数组指针进行包装:
这样看就会明白,直接在iterator前面加const只是让它的地址不能改变。编译器会将iterator看成一个整体,所以只能修饰我们的p指针。
我们可以看到依旧可以改变值。
所以就要这样tyedef:(用string的举例)
typedef const char* const_iterator
这样就会修饰我们的*p(值)了。
这里还是用begin()的原因就是进行了重载,而编译器会根据你传入值的类型来匹配最合适的重载,因为这里要返回const_iterator类型的,所以会调用返回对应类型的begin函数。
reverse_iterator
我们可以用iterator来反着遍历吗:
我们end是指向最后一个数据的下一个指针,我们可以-1来调整到最后一个开始,但是我们的begin要怎么调整呢?-1就会报错。所以是不能反着来的。
所以就有了reverse_iterator
这里会接触到一个单词reserve(反转),要和reverse(预留)做区分。
具体的实现底层我还不知道,没学到那去。
这里的reverse_iterator就是将方向变了,原先的正方向掉了个头。现在想左是正方向。
那么也就有对应的函数来返回对应的开头和结尾迭代器rbegin rend,r就是reverse的第一个字母,联系记忆。
const_reverse_iterator
既然有了反向迭代器,那么也要考虑权限的问题,所以就有const_reverse_iterator:
只能读不能写。
容器间迭代器的分类
因为不同容器的内存形式不同,产生内存结构的局限性,所以我们的迭代器的功能不是完全一样的。
例如我们的单链表是不能逆序遍历的,只能正向便利,所以只有正向迭代器。又因为链表的内存的不连续性,所以不支持迭代器的加减,只支持自增自减。
所以就分了:
随机迭代器(RandomAccessIterator)
这种迭代器支持迭代器的加减:
因为它自身是连续内存,所以这样跳跃访问时间复杂度是O(1)。
双向迭代器(BidirectionalIterator)
这种迭代器不支持跳跃访问,因为内存的不连续,如果支持时间复杂度是O(N),将是很大的时间损耗。
但是可以双向移动,例如我们的双向链表。
单向迭代器(ForwardIterator)
这种迭代器不支持双向访问,只能一个方向访问,即正向访问。
例如单链表,就只能正向访问。
那么这样就可以用维恩图来看:
迭代器的失效问题
有些情况我们的迭代器在传入到某些函数里面后,迭代器就失效了,要么不可访问成为野指针,要么就已经不是指向所要指的值了。
这里我们用数组来举例:
例如我们有一个这样的数组,我们的一个模拟的数组迭代器指向1处。我们要在1前面插入一个数据'x',那么就会变成"0x123",但是我们的迭代器依旧指向第二个空间,所以如果我们再次访问这个地址,就会访问到'x'而不是原来的'2'。
还有很多失效的情况我就不一一举例,我们要根据各个容器的底层结构来分析,看看迭代器是否失效。
当然也有迭代器不失效的情况,例如list的插入就不失效,因为内存是不连续的,插入和删除操作不会影响每个节点。
总之我们尽量不要使用做了参量的迭代器,防止某个函数过后迭代器失效,或者我们接收一下函数返回值更新我们的迭代器值
迭代器互通的底层支持
首先像string这种连续内存的迭代器很好实现。但是像list这种的就需要额外创建一个迭代器类来进行实现:
重载和类
单独创建一个list_iterator类,然后用运算符重载来实现自增自减,还有比较重载,然后在类里面typedef一下:typedef list_iterator iterator,那么这个种类的迭代器就只能在这个类里面使用,除非你知道它原本迭代器的名字。
模板
我们会遇到一些全局函数可以传任意的迭代器:
例如上面这个实现容器元素逆置的函数,只要是双向类型及以上的迭代器都可以传给它进行数据的交换。这就用到了模板来进行支持。