目录
前言
一.迭代器的使用
1.vector迭代器
2.list迭代器的使用
二.迭代器失效问题
1.vector迭代器失效问题
2.list迭代器失效问题
三.vector和list的对比
前言
我们在学习C++STL部分的时候,在vector和list的部分初步认识了迭代器,以及在初学阶段,会觉得迭代器失效是一个很头痛的问题...
所以接下来我们就先从迭代器的使用开始,对迭代器进行一个浅浅的梳理,希望对大家有所帮助~
好的,废话不多说,直接开始今天的内容
一.迭代器的使用
1.vector迭代器
首先,我们需要先对其 建立一个整体的印象。在cpluslpus这个网站,我们可以先看一下它对vector容器的归纳:
因为今天的主题是迭代器,所以我们着重对上面框起来的部分进行讲解,剩下的模块大家有兴趣下来可以自行去cplusplus该网站去探索~
int main()
{
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
//遍历
auto it = v1.begin();
while (it != v1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
return 0;
}
上面我们创建了一个int类型的容器,用它来完成了一个遍历的操作。使用vector其实使用的就是一段连续的数组空间
我们暂且可以将迭代器理解成一个指针
v1.begin()指向的就是第一个元素的位置,end()就是最后一个元素的下一个位置
这么看的话我们是否觉得使用迭代器还不如直接使用指针呢?那么接下来我们来看下面这段代码:
int main()
{
vector<string> s1;
s1.push_back("hello");
s1.push_back("world");
s1.push_back("hello");
s1.push_back("iterator");
//遍历
auto it = s1.begin();
while (it != s1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
return 0;
}
这次我们vector用的是string自定义类型,而不是内置类型int,这在使用以前的数组是无法办到的,这就体现了迭代器的价值,将其封装后可以使得底层可能截然不同的结构在使用上保持形式的统一~
这一点在之后的list中大家会有更深的体会,我们先继续
其实说了正向迭代器begin() 和end()之后,反向迭代器的使用rbegin()和 rend()就非常简单了,因为它们完全是一样的
请看下面代码:
int main()
{
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
//遍历
auto rit = v1.rbegin();
while (rit != v1.rend())
{
cout << *rit << " ";
++rit;
}
cout << endl;
return 0;
}
反向迭代器实现的效果只是将本来的内容反过来遍历了,使用起来也是一样的,是不是?
2.list迭代器的使用
对于list迭代器,同样的,我们需要先对其 建立一个整体的印象。在cpluslpus这个网站,我们可以先看一下它对list容器的归纳:
接下来是使用,代码如下:
int main()
{
list<int> ls;
ls.push_back(1);
ls.push_back(2);
ls.push_back(3);
ls.push_back(4);
ls.push_back(5);
auto LT = ls.begin();
while (LT != ls.end())
{
cout << *LT << " ";
LT++;
}
cout << endl;
}
这里我们在使用上发现玩的跟上面的vector是一样的,这也就体现了迭代器的一个巨大的好处
因为其实在底层上,两者是完全不同的
我们知道,list是一种带头双向循环的链表,那么每个结点就有三个区域:
数据域data
指向下一个结点的next指针
指向前一个结点的prev指针
这时如果我们还将迭代器简单的理解为指针的话,那么不妨想一下,如果我们使用++运算符的话,还能找到下一个结点的位置吗?
答案是不能的,因为list不像vector一样是一段连续的内存空间,++以后,可能对应是下一个结点的位置,也可能不是,我们就没法确定了
那又为什么我们却能像vector一样的去使用list的迭代器呢?
这都归功于类的封装,在对迭代器封装的时候,我们重载了这些操作符的意义(例如:++, * 等)
这才使得我们能就像使用指针一样去使用迭代器
至于迭代器的封装具体是怎么做到这样的封装,这里博主自己实现了一个list的迭代器,大家可以看一下思路,相当于是STL关于 list迭代器部分的一个简化版~:
//list的模拟实现分成3个类,这里就不细说了下面给出了大致的框架,并非完整的代码哦~
//节点的结构体
template<class T>
struct ListNode
{
T _data;
ListNode<T>* _next;
ListNode<T>* _prev;
//...
};
//list迭代器的模拟实现
template<class T, class Ref, class Ptr>
struct ListIterator
{
typedef ListNode<T> Node;
typedef ListIterator<T, Ref, Ptr> Self;
Node* _node;//变量
ListIterator(Node* node)//构造
:_node(node)
{}
Self& operator++()
{
_node = _node->_next;
return *this;
}
Ref operator*()//解引用
{
return _node->_data;
}
Ptr operator->()//指针
{
return &_node->_data;
}
bool operator!=(const Self& it)
{
return _node != it._node;
}
};
//list--->链表
template<class T>
class list
{
typedef ListNode<T> Node;
public:
typedef ListIterator<T, T&, T*> iterator;
typedef ListIterator<T, const T&, const T*> const_iterator;
iterator begin()
{
return iterator(_head->_next);
}
iterator end()
{
return iterator(_head);
}
const_iterator begin() const
{
return const_iterator(_head->_next);
}
const_iterator end() const
{
return const_iterator(_head);
}
//...
//...
}
private:
Node* _head;//头节点
};
为了更能体现出迭代器并非简单的指针,所以就用list的迭代器作为实例啦(上面的vector的迭代器我们自己实现的话其实跟指针差不多,不具备代表性,所以就没有提啦)
好,接下来我们来看一下关于迭代器失效的问题,这也是一个重点问题
二.迭代器失效问题
1.vector迭代器失效问题
vector的迭代器失效,我们是在实现vector的insert和erase这两个接口时发现的
请看下面的代码:
int main()
{
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
//遍历
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
auto it = find(v1.begin(), v1.end(), 3);//查找3的位置
v1.erase(it);//删除3
v1.insert(it, 0);//插入0
//遍历
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
return 0;
}
上面代码的意思就是找到3并删除掉,然后再在该位置插入0
按照理解这段代码应该是没有问题的,就是很简单的一段删除和插入嘛
然后,我们运行一下代码:
???代码崩溃了
其实这就是因为erase函数使得迭代器失效导致的
迭代器it原本指向的是3的位置,之后我们将他删除后,it指向的数据是4,不再是原数据了
而VS在实现函数时是assert处理的,所以才导致的上面的结果
解决办法就是it接收一下erase的返回值,erase的返回值是该位置的下一个位置的迭代器。即对it进行修正,这样就能有效的避免上面的问题
请看修正后的代码:
int main()
{
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
//遍历
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
auto it = find(v1.begin(), v1.end(), 3);//查找3的位置
it = v1.erase(it);//删除3
v1.insert(it, 0);//插入0
//遍历
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
return 0;
}
还有一种迭代器失效是在insert时导致的
假如我们一开始的容器大小是5个,且这个容器已经满了,此时我再进行insert的时候,就会发生扩容,而C++的扩容是异地扩容的
异地扩容就必定会导致一个问题,我们原来it指向的地址发生了改变,此时如果不做处理的话,it指向的就是一块未知的空间,it就变成了一个野指针,这也是一种迭代器的失效
2.list迭代器失效问题
list的迭代器在删除时也会导致和上面的vector一样的问题,但与vector不同的是,vector在erase时,会导致当前迭代器失效,而list只会导致当前迭代器失效,其他迭代器不受影响
解决办法也是接收一下erase的返回值,同样list的erase的返回值也是该位置的下一个结点位置
但是list在insert插入的时候并不会导致迭代器失效,因为list不存在扩容的概念
三.vector和list的对比
最后我们将vector和list这2个容器进行一个对比:
在底层结构上:
vector是动态顺序表,开辟的是一段连续空间
list 是带头双向循环链表
在随机访问上:
vector支持随机访问,访问某个元素的效率为O(1) ([ ]运算符访问)
list不支持随机访问,访问某个元素的效率为O(N)
在插入和删除上:
vector在任意位置插入和删除效率低,需要搬移元素,时间复杂度为O(N),插入时有可能需要扩容,开辟新空间,拷贝元素,释放旧空间,导致效率更低
list在任意位置插入和删除效率高,不需要搬移元素,时间复杂度为O(1)
迭代器上:
vector的迭代器是原生态指针,在插入和删除元素均会导致迭代器失效
list的迭代器是在原生态指针上进行了封装,插入元素不会导致迭代器失效,删除元素时,只会导致当前迭代器失效,其他迭代器不受影响
在使用场景上:
vector在需要高效存储,支持随机访问,不关心插入和删除效率的场景下更适合
list在需要大量插入和删除操作,不关心访问的场景下更适合