STL — STL迭代器的原理以及迭代器失效

旧城等待, 2022-06-08 01:29 399阅读 0赞

STL迭代器

作为STL六大组件之一 Iterator(迭代器)模式又称Cursor(游标)模式,通俗一点来讲就是用来遍历容器的,并对容 器 进行一定 的操

作. 或许就有人 问了,我不用迭代器也一样可以遍历容器还要它干啥? 这样想是不对的. 现在我们常 用 的 容器vector,list, map,set

,multiset,map ,multimp,deque 这 几个容器的内部是实现有顺序表,链表,红黑树. 那 我们遍 历这些容器就要明白它的底 层构造, 这

样 很不方便. Iterator就是在底层给你定义好,然后 任何容器都是同样 的遍历方 式. 这样就算你不懂底层实现也可以 使用.所以就是可

以操 作容器, 又不需暴露该对象的内 部表示。或者这样说 可能更容易理 解 : Iterator模式是运用于聚合对象的一种 模式,通过运用该

模式, 使得 我们可以在不知道对象内 部表 示的情况下,按照一 定顺序(由 iterator提供的 方法)访问聚合对象中 的各个元素。

迭代器的作用:能够让迭代器与算法不干扰的相互发展,最后又能无间隙的粘合起来,重载了*,++,==,!=, = 运算符。用 以

操作复杂的数据 结构,容器提供迭代器,算法使用迭代器;常见的一些迭代器类型:

iterator、 const_iterator、 reverse_iterator 和const_reverse_iterator. 你就 可以更简单的理解就是,我不知道该对象内部 的实

现 ,但是我还是可以很轻易 的 遍历它以及访问修 改数据. 迭代器其实是一个蛮好用的STL组件. 我一直以为空间配置器是STL六大组件

当 中最难总结和理解的,那么迭代器对于空间配置器来说属于蛮好理解的知识 点, 但 对容器和 仿函数来说就带一 点抽象啦,不过呢这

都 不 打紧我用只要会实现它我们就一定会使用它,不过呢我们 就先开 始了 解它是如何使用的,

迭代器的使用

首先我们对iterator进行一个最简单的操作,用它来遍历list. 等等介绍迭代器原理的时候,我会实现一个List容器 ,并且实现该 容

器的迭代器,到时 候大家就明白迭代器的原理了.代码如下:

  1. list<int> arr(5, 3);
  2. list<int>::iterator it = arr.begin();
  3. //这里就是申请一个迭代器,
  4. while (it != arr.end())
  5. {
  6. cout << *it << " ";
  7. ++it;
  8. }

这是最简单的应用,遍历一遍容器并且输出他们的值,我们先来看看结果:

20170913180118036

如果不会使用库的list容器的话,可以去看我这个博客:手把手教你使用list.现在我们看到了iterator访问到了list 的每一个元

素 其实大家都知道, 当你可以访问到一个元素时,那么操作它就很容易了什么增删查改都不在话下. 相信大家对iterator基本的使

用已经有一点掌握了,不过呢这里还是有 几点要注意的.比如const_ iterator迭代器的使 用 前面的程序用vector::iterator改变

vector中的元素值。每种容器类型还定义了一种名为 const_iterator的类型, 该类型只能访问 容器内元素,但不能改变其值。

当我们对普通iterator类型解引用时,得到对某个元素的非const引用。而如果我们对const_iterator类型解引用时,可以得到

一个指向const对象的引用,如同任何常量一样,该对象不能进行重写。例如,如果text是vector类型,程序员想要遍

历它,输出每个元素,可以这样编写程序:

  // use const_iterator because we won’t change the elements

  for (vector::const_iterator iter = text.begin();

  iter != text.end(); ++iter)

  cout << *iter << endl; // print each element in text

除了是从迭代器读取元素值而不是对它进行赋值之外,这个循环与前一个相似。由于这里只需要借助迭代器进行读,不需要写,这里

把iter定义为 const_iterator类型。当对const_iterator类型解引用时,返回的是一个const值。不允许用const_iterator进行赋值:

  for (vector::const_iterator iter = text.begin();

  iter != text.end(); ++ iter)

  *iter = “ “; // error: *iter is const

使用const_iterator类型时,我们可以得到一个迭代器,它自身的值可以改变,但不能用来改变其所指向的元素的值。可以对迭代

器进行自增以及使用解引用操作符来读取值,但不能对该元素值赋值.

迭代器的实现原理

当你创造出来一个东西的时候,那么你对他的使用或者对于他的bug可能烂熟于心了吧.首先我们知道迭代器就是通过一定的封装

然后暴露出接口,再然后进行使用. 我们不需要知道底层的封装,我们记住它的方法如何使用就可以了. 但是我今天偏偏就要明

白迭代器的底层实现,通过我最近查看源代码,其实跟iterator跟指针是非常相似的,拥有 *-> 操作,并且具有

++,—,== ,!= ,目前为止我们就只实现这几个就够了.现在我要写一个山寨版的list容器的iterator的定义.

其他容器大概都是这么一个框架实现,只是实现具体方法不同.

代码实现:

  1. template<class T, class Ref, class Ptr>
  2. struct __ListIterator
  3. {
  4. typedef __ListNode<T> Node;
  5. typedef __ListIterator<T, Ref, Ptr> Self;
  6. Node* _node;
  7. __ListIterator(Node* x)
  8. :_node(x)
  9. {}
  10. Ptr operator->()
  11. {
  12. return &(operator*())
  13. }
  14. Ref operator*()
  15. {
  16. return _node->_data;
  17. }
  18. Ptr operator->() const
  19. {
  20. return &(operator*()) const;
  21. }
  22. Ref operator*() const
  23. {
  24. return _node->_data const;
  25. }
  26. bool operator != (const Self& s)
  27. {
  28. return _node != s._node;
  29. }
  30. inline Self& operator++()//前置++
  31. {
  32. _node = _node->_next;
  33. return *this;
  34. }
  35. Self& operator++(int) //后置++
  36. {
  37. //第一种先初始化再返回.
  38. /*Self tmp(*this);
  39. _node = _node->_next;
  40. return tmp;*/
  41. //第二种返回的时候再初始化.
  42. Node* cur = _node;
  43. _node = _node->_next;
  44. return Self(cur);
  45. //第一种做了三件事情. 首先调用了构造函数构造一个tmp,返回的时候再调用拷贝构造把自己拷给外部存储,最后调用析构函数让自己析构.
  46. //第二种做了一件事情,在外部存储初始化一个tmp就Ok了. 所以效率来说第二种方法是最优解决方案.
  47. }
  48. inline Self& operator--()
  49. {
  50. _node = _node->_prev;
  51. return *this;
  52. }
  53. Self& operator--(int)
  54. {
  55. Node* cur = _node;
  56. _node = _node->_prev;
  57. return Self(cur);
  58. }
  59. bool operator != (const Self& s) const
  60. {
  61. return this->_node != s._node;
  62. }
  63. bool operator ==(const Self& s) const
  64. {
  65. return !(operator!=(s));
  66. }
  67. };

我们看到迭代器的构造函数是一个单参数构造函数,其实我们发现迭代器跟智能指针蛮相似的,都是底层封装一个指针,最后对

这个类进行各种各样的运算符重载. 让它成为一个拥有指针功能的特殊类. 接下来我们在list容器当中定义它.

  1. template<class T>
  2. class List
  3. {
  4. typedef __ListNode<T> Node;
  5. public:
  6. typedef __ListIterator<T, T&, T*> Iterator;
  7. typedef __ListIterator<const T, const T&, const T*> ConstIterator;
  8. }

可能上面有的朋友看不懂为什么会有三个模板参数? 现在明白了吧.这些都是为了代码的复用.要不然当你要实现const_iterator

的时候又得重新写一个const__ListIterator的类了. T& T* 这些都是要在代码中高频出现所以我们也给他们开出来一个模板参数

的位置,到时候iterator和const_iterator都要使用相同的模板框架,直接嵌套进去就ok. 如图所示:

20170913211619914

因为iterator指向节点所以他就可以访问并操作节点节点的内容,而const_iterator只能访问到元素不能够操作节点. 这是迭代器

的定义.有木有觉得它其实没有你想的那么抽象. 仔细想一想我相信你可以想明白了.接下来是一个迭代器很重要的问题> 迭代器

失效. 接下来我们引出来这个问题吧. 首先我们开始完善我们的山寨List,什么push_back,popback这些我们写过很多遍. 我下面

会有完整代码的. 我们今天着重研究一下erase删除函数. 像我们正常实现的erase函数只是将参数改为迭代器即可.

下面是我对该函数的实现;

  1. void erase(Iterator& it)
  2. {
  3. assert(it != End() && it._node);
  4. Node* cur = it._node;
  5. Node* prev = cur->_prev;
  6. Node* next = cur->_next;
  7. prev->_next = next;
  8. next->_prev = prev;
  9. delete cur;
  10. }

这个删除对于双向循环链表应该是没有bug的. 然后这个时候我做一个操作, 我利用迭代器删掉元素当中所有偶数:

  1. void Test()
  2. {
  3. List<int> l;
  4. l.PushBack(1);
  5. l.PushBack(2);
  6. l.PushBack(3);
  7. l.PushBack(4);
  8. List<int>::Iterator it = l.Begin();
  9. while (it != l.End())
  10. {
  11. if (*it % 2 == 0)
  12. {
  13. l.erase(it);
  14. }
  15. ++it;
  16. }
  17. it = l.Begin();
  18. while (it != l.End())
  19. {
  20. cout << *it << " ";
  21. ++it;
  22. }
  23. cout << endl;
  24. }

运行结果:

20170913211716969

正常情况就是最后窗口打印出来1 3. 好的那我们运行程序.结果程序崩了,居然还是越界. 经过我调试之后我发现程序在调用

earse之前没问题,调用earse之后就出现问题. 我再继续调试,发现第一次删除后,节点被删除了是没有问题的,而且连接也

没有问题,但是接下来的一次*it就访问越界了,

所以我有理由相信这里的问题出在it上面,所以我返回第一次删除前调试.

20170913211735957

我们发现第一次earse之前,It的值还没问题.

20170913211743528

但是earse之后,我们发现it变成一个随机值,原来这个就是我们程序崩溃的罪魁祸首.当删除掉该节点之后,该节点的iterator

变为随机值,所以后面的++it,生成的还是一个随机值,最后下次*it是后就会发生访问越界. 这个就是我们的迭代器失效问题.

突然一个很重要的问题就被引出来了,这就是迭代器失效,就是你删除该节点后节点的迭代器因为变成一个随机值,没有办

法跳转到下一个节点. 这个时候你的迭代器++之后就是一个随机值,最后程序再次使用到it时,程序崩溃. 这就类似于野指

针的问题.

那么问题来了,我们该如何解决它? 既然节点被删了,它的迭代器也就成随机值了,那我每次删除完就要对iterator重新定

义一次,我们每次删除结束之后我们把就是删除节点的上一个节点的Iterator,赋给it,这时候++it的时候,迭代器就会跳

转到下一个节点的位置. 具体如何实现呢? 那我们就来尝试一下.

  1. void earse(Iterator& it)
  2. {
  3. assert(it != End() && it._node);
  4. Node* cur = it._node;
  5. Node* prev = cur->_prev;
  6. Node* next = cur->_next;
  7. prev->_next = next;
  8. next->_prev = prev;
  9. delete cur;
  10. it = prev;//这里发生了隐式转换 类似于 it = Iterator(prev); 单参数的构造函数. 就会发生隐式转换.
  11. }

我把it的引用传进去,然后在删除cur之后,把it赋值为cur前面的节点的迭代器. 接下来我们来看程序结果:

20170913211818981

从结果上面来看,我们应该是成功了,迭代器失效每个容器导致的原因好像都不一样,但是他们的原理都是因为不当操作使得

iterator失去它自己的内容变为随机值,我们应该避免这些东西. 就能够熟练掌握迭代器. 下面我把整个山寨List的全套代码

贴在下面,有空我们可以去尝试山寨别的容器,提高自己对容器的理解和认识.

List实现代码:

  1. template<class T>
  2. struct __ListNode
  3. {
  4. __ListNode<T>* _next;
  5. __ListNode<T>* _prev;
  6. T _data;
  7. __ListNode(const T& x)
  8. :_data(x)
  9. , _next(NULL)
  10. , _prev(NULL)
  11. {}
  12. };
  13. template<class T, class Ref, class Ptr>
  14. struct __ListIterator
  15. {
  16. typedef __ListNode<T> Node;
  17. typedef __ListIterator<T, Ref, Ptr> Self;
  18. Node* _node;
  19. __ListIterator(Node* x)
  20. :_node(x)
  21. {}
  22. //T& operator*()
  23. Ptr operator->()
  24. {
  25. return &(operator*())
  26. }
  27. Ref operator*()
  28. {
  29. return _node->_data;
  30. }
  31. Ptr operator->() const
  32. {
  33. return &(operator*()) const;
  34. }
  35. Ref operator*() const
  36. {
  37. return _node->_data const;
  38. }
  39. bool operator != (const Self& s)
  40. {
  41. return _node != s._node;
  42. }
  43. inline Self& operator++()
  44. {
  45. _node = _node->_next;
  46. return *this;
  47. }
  48. Self& operator++(int)
  49. {
  50. //第一种先初始化再返回.
  51. /*Self tmp(*this);
  52. _node = _node->_next;
  53. return tmp;*/
  54. //第二种返回的时候再初始化.
  55. Node* cur = _node;
  56. _node = _node->_next;
  57. return Self(cur);
  58. //第一种做了三件事情. 首先调用了构造函数构造一个tmp,返回的时候再调用拷贝构造把自己拷给外部存储,最后调用析构函数让自己析构.
  59. //第二种做了一件事情,在外部存储初始化一个tmp就Ok了. 所以效率来说第二种方法是最优解决方案.
  60. }
  61. inline Self& operator--()
  62. {
  63. _node = _node->_prev;
  64. return *this;
  65. }
  66. Self& operator--(int)
  67. {
  68. Node* cur = _node;
  69. _node = _node->_prev;
  70. return Self(cur);
  71. }
  72. bool operator != (const Self& s) const
  73. {
  74. return this->_node != s._node;
  75. }
  76. bool operator ==(const Self& s) const
  77. {
  78. return !(operator!=(s));
  79. }
  80. };
  81. template<class T>
  82. class List
  83. {
  84. typedef __ListNode<T> Node;
  85. public:
  86. typedef __ListIterator<T, T&, T*> Iterator;
  87. typedef __ListIterator<const T, const T&, const T*> ConstIterator;
  88. List()
  89. {
  90. _head = new Node(T());
  91. _head->_next = _head;
  92. _head->_prev = _head;
  93. }
  94. Iterator Begin()
  95. {
  96. return Iterator(_head->_next);
  97. }
  98. ConstIterator Begin() const
  99. {
  100. return ConstIterator(_head->_next);
  101. }
  102. Iterator End()
  103. {
  104. return Iterator(_head);
  105. }
  106. ConstIterator End() const
  107. {
  108. return ConstIterator(_head);
  109. }
  110. T& Front()
  111. {
  112. return _head->next;
  113. }
  114. T& back()
  115. {
  116. return _head->_prev;
  117. }
  118. void PushBack(const T& x)
  119. {
  120. Insert(End(), x);
  121. }
  122. void PushFront(const T& x)
  123. {
  124. Insert(Begin(), x);
  125. }
  126. void popFront()
  127. {
  128. earse(Begin());
  129. }
  130. void popBack()
  131. {
  132. earse(--End());
  133. }
  134. void Insert(Iterator pos, const T& x)
  135. {
  136. assert(pos._node);
  137. Node* next = pos._node;
  138. Node* prev = next->_prev;
  139. Node* cur = new Node(x);
  140. prev->_next = cur;
  141. cur->_prev = prev;
  142. cur->_next = next;
  143. next->_prev = cur;
  144. }
  145. void earse(Iterator& it)
  146. {
  147. assert(it != End() && it._node);
  148. Node* cur = it._node;
  149. Node* prev = cur->_prev;
  150. Node* next = cur->_next;
  151. prev->_next = next;
  152. next->_prev = prev;
  153. delete cur;
  154. it = prev;//这里发生了隐式转换 类似于 it = Iterator(prev); 单参数的构造函数. 就会发生隐式转换.
  155. }
  156. void Clear()
  157. {
  158. Iterator it = Begin();
  159. while (it != End())
  160. {
  161. Node* del = it._node;
  162. ++it;
  163. delete del;
  164. }
  165. _head->_next = _head;
  166. _head->_prev = _head;
  167. }
  168. Iterator Find(const T& x)
  169. {
  170. Iterator it = Begin();
  171. while (it != End())
  172. {
  173. if (*it == x)
  174. return it;
  175. ++it;
  176. }
  177. return End();
  178. }
  179. protected:
  180. Node* _head;
  181. };

发表评论

表情:
评论列表 (有 0 条评论,399人围观)

还没有评论,来说两句吧...

相关阅读

    相关 520-C++STL()

    const\_iterator:常量的正向迭代器,只能读,而不能写了 iterator:普通的正向迭代器,从第一个访问到最后一个,可以更改元素值 reverse\_it

    相关 STL容器学习】-

    STL中的容器类广泛使用迭代器,迭代器是一种对象,使用它可方便地对容器中的元素进行遍历。迭代器与内置指针很相似,都提供了一种访问和操纵容器中元素的方便途径。迭代器可以分为以下5

    相关 STL

    什么是迭代器 迭代器是STL中行为类似指针的设计模式,它可以提供了一种对容器中的对象的访问方法;并且它没有暴露容器中内部的表述方式。 例如STL中的map和set,它

    相关 STL&&失效

    1.说说设计模式?(迭代器模式)         迭代器模式作为STL的六大组件之一,通俗来讲,它就是用来遍历容器的,对容器进行一定的操作。我们通常使用的容器vector

    相关 STL list实现

    list list是一个线性双向链表结构,它的数据由若干个节点构成,每一个节点都包括一个信息块(即实际存储的数据)、一个前驱指针和一个后驱指针。它无需分配指定的内存大小且