一文读懂C++五种常用的迭代器
c++ STL迭代器(iterator)
C++:迭代器
--------------------------------------------------------
什么是迭代器?
- 好怪异的名词,搞了好多年都没有搞懂。
说白了,迭代器,就是“开瓶器”而已。
- 开瓶器的种类:有开啤酒瓶的,有开红酒瓶的,还有其他xx的。
- 它们的目的,都是为了方便人们的使用。
- 可以将“开瓶器”和“瓶子”,直接做在一起吗?
当然可以,但是,这就好臃肿,成本好高了。
于是,开瓶器和瓶子就分离了。
- 不要开瓶器,直接手动开瓶子,可以吗?
当然可以,结局就是,用牙齿开啤酒瓶,结果崩坏了一颗牙。
用螺丝刀开红酒,结果塞子掉到了酒里。
程序员不断造轮子,使用好困难,或者好浪费时间。浪费时间,就是浪费生命。
===================================
一文读懂C++五种常用的迭代器
算法要求的迭代器操作分为五个类别:
Input iterator(输入迭代器) 读,不能写;只支持自增运算
Output iterator(输出迭代器) 写,不能读;只支持自增运算
Forward iterator(前向迭代器) 读和写;只支持自增运算
Bidirectional iterator(双向迭代器) 读和写;支持自增和自减运算
Random access iterator(随机访问迭代器) 读和写;支持完整的迭代器算术运算
输入迭代器
- 输入迭代器可用于读取容器中的元素,但是不保证能支持容器的写入操作。输入迭代器必须至少提供下列支持。
- 相等和不等操作符(==,!=),比较两个迭代器。
- 前置和后置的自增运算(++),使迭代器向前递进指向下一个元素。
- 用于读取元素的解引用操作符(*),此操作符只能出现在赋值运算的右操作数上。
- 箭头操作符(->),这是 (*it).member 的同义语,也就是说,对迭代器进行解引用来获取其所关联的对象的成员。
- 输入迭代器只能顺序使用;一旦输入迭代器自增了,就无法再用它检查之前的元素。要求在这个层次上提供支持的泛型算法包括 find 和accumulate。标准库 istream_iterator 类型输入迭代器。
输出迭代器
- 输出迭代器可视为与输入迭代器功能互补的迭代器; 输出迭代器可用于向容器写入元素,但是不保证能支持读取容器内容。输出迭代器要求:前置和后置的自增运算(++),使迭代器向前递进指向下一个元素。
- 解引用操作符(*),该引操作符只能出现在赋值运算的左操作数上。给解引用的输出迭代器赋值,将对该迭代器所指向的元素做写入操作。
- 输出迭代器可以要求每个迭代器的值必须正好写入一次。使用输出迭代器时,对于指定的迭代器值应该使用一次 * 运算,而且只能用一次。输出迭代器一般用作算法的第三个实参, 标记起始写入的位置。 例如, copy 算法使用一个输出迭代器作为它的第三个实参, 将输入范围内的元素复制到输出迭代器指定的目标位置。标准库 ostream_iterator 类型输出迭代器。
前向迭代器
- 前向迭代器用于读写指定的容器。这类迭代器只会以一个方向遍历序列。
- 前向迭代器支持输入迭代器和输出迭代器提供的所有操作,除此之外,还支持对同一个元素的多次读写。可复制前向迭代器来记录序列中的一个位置,以便将来返回此处。需要前向迭代器的泛型算法包括 replace。
双向迭代器
- 双向迭代器从两个方向读写容器。除了提供前向迭代器的全部操作之外,双向迭代器还提供前置和后置的自减运算(–)。
- 需要使用双向迭代器的泛型算法包括 reverse。所有标准库容器提供的迭代器都至少达到双向迭代器的要求。
随机访问迭代器
- 随机访问迭代器提供在常量时间内访问容器任意位置的功能。这种迭代器除了支持双向迭代器的所有功能之外,还支持下面的操作:
- 关系操作符 <、<=、> 和 >=,比较两个迭代器的相对位置。
- 迭代器与整型数值 n 之间的加法和减法操作符 +、+=、- 和 -=,结果是迭代器在容器中向前(或退回)n 个元素。
- 两个迭代器之间的减法操作符(–),得到两个迭代器间的距离。
- 下标操作符 iter[n],这是 *(iter + n) 的同义词。
- 需要随机访问迭代器的泛型算法包括 sort 算法。vector、deque 和string 迭代器是随机访问迭代器,用作访问内置数组元素的指针也是随机访问迭代器。
- 除了输出迭代器,其他类别的迭代器形成了一个层次结构:需要低级类别迭代器的地方,可使用任意一种更高级的迭代器。对于需要输入迭代器的算法,可传递前向、双向或随机访问迭代器调用该算法。调用需要随机访问迭代器的算法时,必须传递随机访问迭代器。
- map、set 和 list 类型提供双向迭代器,而 string、vector 和 deque 容器上定义的迭代器都是随机访问迭代器都是随机访问迭代器, 用作访问内置数组元素的指针也是随机访问迭代器。istream_iterator 是输入迭代器,而ostream_iterator 则是输出迭代器。
- C++ 标准为所有泛型和算术算法的每一个迭代器形参指定了范围最小的迭代器种类。例如,find(以只读方式单步遍历容器)至少需要一个输入迭代器。
- replace 函数至少需要一对前向迭代器。replace_copy 函数的头两个迭代器必须至少是前向迭代器,第三个参数代表输出目标,必须至少是输出迭代器。
- 对于每一个形参,迭代器必须保证最低功能。将支持更少功能的迭代器传递给函数是错误的;而传递更强功能的迭代器则没问题。
- 向算法传递无效的迭代器类别所引起的错误,无法保证会在编译时被捕获到。
————————————————
版权声明:本文为CSDN博主「iVSCoder」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qinghuaooo/article/details/107937027
c++ STL迭代器(iterator)
迭代器是STL库的三大组件之一[1]。其作为容器和算法的连接器,将算法对各种容器的遍历操作与具体的容器类型解耦,即迭代器可用于对相应的容器进行元素遍历。这是STL库的核心技术。
(本文全部代码均来自标准库源码)
迭代器类型
STL库支持的迭代器类型是所有编程语言中最全面的[2],共有五种:
- InputIterator:输入迭代器。支持对容器元素的逐个遍历,以及对元素的读取(input);
- OutputIterator:输出迭代器。支持对容器元素的逐个遍历,以及对元素的写入(output)。
- ForwardIterator:前向迭代器。向前逐个遍历元素。可以对元素读取;
- BidirectionalIterator:双向迭代器。支持向前向后逐个遍历元素,可以对元素读取。
- RandomAccessIterator:随机访问迭代器。支持O(1)时间复杂度对元素的随机位置访问,支持对元素的读取。
输出迭代器可以修改元素,这可能会导致内部结构的调整,进而导致原有的迭代器失效!可能的情况有:
- 结构和元素顺序变更:比如对map,set,priority_queue插入元素;
- 内存变化:比如对vector插入元素,可能导致重新申请内存并拷贝!
从上面的描述我们可以很容易的知道他们之间的关系:
实际上在标准库上的实现如下(GNU):
/// Marking input iterators.
struct input_iterator_tag { };
/// Marking output iterators.
struct output_iterator_tag { };
/// Forward iterators support a superset of input iterator operations.
struct forward_iterator_tag : public input_iterator_tag { };
/// Bidirectional iterators support a superset of forward iterator
/// operations.
struct bidirectional_iterator_tag : public forward_iterator_tag { };
/// Random-access iterators support a superset of bidirectional
/// iterator operations.
struct random_access_iterator_tag : public bidirectional_iterator_tag { };
迭代器类型成员
一个迭代器的类型成员包含如下:
template<typename _Category, typename _Tp, typename _Distance = ptrdiff_t,
typename _Pointer = _Tp*, typename _Reference = _Tp&>
struct iterator
{
/// One of the @link iterator_tags tag types@endlink.
typedef _Category iterator_category;
/// The type "pointed to" by the iterator.
typedef _Tp value_type;
/// Distance between iterators is represented as this type.
typedef _Distance difference_type;
/// This type represents a pointer-to-value_type.
typedef _Pointer pointer;
/// This type represents a reference-to-value_type.
typedef _Reference reference;
};
所有容器的迭代器实现必须都包含如上的五个类型成员!这些成员会作为各个算法的类型参数或者用于iterator traits。
由于各个不同的容器其内部实现不同,且元素的数据结构也不同,所以stl并没有一个统一的iterator类实现。即各个不同容器在内部实现其自己的迭代器,同时对迭代器的各个类型成员重定义,以符合struct iterator的定义规范。
该类最初本想所有的迭代器实现类均继承之,但是从一开始就没有使用过!仅仅成为了可选的实现。所以在C++17中将该类废弃了。
iterator traits
迭代器类型萃取可以提取迭代器的各个类型。
template<typename _Iterator, typename = __void_t<>>
struct __iterator_traits { };
template<typename _Iterator>
struct __iterator_traits<_Iterator,
__void_t<typename _Iterator::iterator_category,
typename _Iterator::value_type,
typename _Iterator::difference_type,
typename _Iterator::pointer,
typename _Iterator::reference>>
{
typedef typename _Iterator::iterator_category iterator_category;
typedef typename _Iterator::value_type value_type;
typedef typename _Iterator::difference_type difference_type;
typedef typename _Iterator::pointer pointer;
typedef typename _Iterator::reference reference;
};
template<typename _Iterator>
struct iterator_traits
: public __iterator_traits<_Iterator> { };
迭代器的基本实现
迭代器在c++实际实现为指针。即使用指针访问对应索引的对象。为了更加直观简便的使用迭代器,各个迭代器均对该指针重载了"->"和"*"解引用操作符。而不需要向某些编程语言一样需要执行如下操作:
itr->value();
itr->next();
而c++中则:
itr = container.begin();
itr++;
itr--;
*itr;
itr->...
以std::list的迭代器实现为例:
template<typename _Tp>
struct _List_iterator
{
typedef _List_iterator<_Tp> _Self;
typedef _List_node<_Tp> _Node;
typedef ptrdiff_t difference_type;
typedef std::bidirectional_iterator_tag iterator_category;
typedef _Tp value_type;
typedef _Tp* pointer;
typedef _Tp& reference;
_List_iterator() _GLIBCXX_NOEXCEPT
: _M_node() { }
explicit
_List_iterator(__detail::_List_node_base* __x) _GLIBCXX_NOEXCEPT
: _M_node(__x) { }
_Self
_M_const_cast() const _GLIBCXX_NOEXCEPT
{ return *this; }
// Must downcast from _List_node_base to _List_node to get to value.
reference
operator*() const _GLIBCXX_NOEXCEPT
{ return *static_cast<_Node*>(_M_node)->_M_valptr(); }
pointer
operator->() const _GLIBCXX_NOEXCEPT
{ return static_cast<_Node*>(_M_node)->_M_valptr(); }
_Self&
operator++() _GLIBCXX_NOEXCEPT
{
_M_node = _M_node->_M_next;
return *this;
}
_Self
operator++(int) _GLIBCXX_NOEXCEPT
{
_Self __tmp = *this;
_M_node = _M_node->_M_next;
return __tmp;
}
_Self&
operator--() _GLIBCXX_NOEXCEPT
{
_M_node = _M_node->_M_prev;
return *this;
}
_Self
operator--(int) _GLIBCXX_NOEXCEPT
{
_Self __tmp = *this;
_M_node = _M_node->_M_prev;
return __tmp;
}
friend bool
operator==(const _Self& __x, const _Self& __y) _GLIBCXX_NOEXCEPT
{ return __x._M_node == __y._M_node; }
friend bool
operator!=(const _Self& __x, const _Self& __y) _GLIBCXX_NOEXCEPT
{ return __x._M_node != __y._M_node; }
// The only member points to the %list element.
__detail::_List_node_base* _M_node;
};
具体使用
迭代器的常见使用方式有三种。
常规遍历方式
std::vector<int> vec;
...
for (std::vector<int>::iterator itr = vec.begin(); itr != vec.end(); ++itr)
{
// 需要对itr执行解引用!
...
}
这是最为常用的方式!
c++11遍历方式
std::vector<int> vec;
...
for(int item : vec)
{
// item已经执行了解引用
...
}
c++11工具函数
c++11增加了两个工具函数begin()/end(),支持对原生数组的迭代器访问,同时也支持对已有容器的访问。
int eles[10] = {...};
for (auto itr = begin(eles); itr != end(eles); ++itr)
{
...
}
容器迭代器的获取
各个容器对象提供了一系列的获取迭代器的方法。
- begin()/end():获取的迭代器的类型是T::iterator,可通过该迭起修改对应元素。即可变。
- cbegin()/end():获取的迭代器的类型是T:const_iterator,仅能读取对应元素,不可修改之。即常量访问。
- rbegin()/rend():获取的迭代器的类型是T::reverse_iterator。反向访问元素,即从最后一个元素向第一个元素遍历。可修改对应元素,即可变。
- crbegin()/crend():获取的迭代器的类型是T::const_reverse_iterator。反向访问元素,即从最后一个元素向第一个元素遍历。不可修改对应元素,即常量访问。
- 从c++11开始,提供了工具函数begin()/end()。
获取容器迭代器是算法的第一步。获取后即完成了于容器的解耦。
算法的使用
标准库对容器操作提供了丰富的算法,提高开发人员的开发效率。这些算法全是模板函数,传入迭代器。迭代器的类型在类型参数中显示标明,但是实际的类型检查在运行期间才能判断出来。
下面是几个标准库中提供的函数原型:
template< class InputIt, class T >
typename iterator_traits<InputIt>::difference_type
count( InputIt first, InputIt last, const T &value );
template< class BidirIt1, class BidirIt2 >
BidirIt2 copy_backward( BidirIt1 first, BidirIt1 last, BidirIt2 d_last );
template< class ForwardIt, class UnaryPredicate >
ForwardIt partition( ForwardIt first, ForwardIt last, UnaryPredicate p );
下面是一个具体的三大组件联合使用的快速排序的实例:
#include <algorithm>
#include <iostream>
#include <iterator>
#include <vector>
#include <forward_list>
template <class ForwardIt>
void quicksort(ForwardIt first, ForwardIt last)
{
if(first == last) return;
auto pivot = *std::next(first, std::distance(first,last)/2);
ForwardIt middle1 = std::partition(first, last,
[pivot](const auto& em){ return em < pivot; });
ForwardIt middle2 = std::partition(middle1, last,
[pivot](const auto& em){ return !(pivot < em); });
quicksort(first, middle1);
quicksort(middle2, last);
}
int main()
{
std::vector<int> v = {0,1,2,3,4,5,6,7,8,9};
std::cout << "Original vector:\n ";
for(int elem : v) std::cout << elem << ' ';
auto it = std::partition(v.begin(), v.end(), [](int i){return i % 2 == 0;});
std::cout << "\nPartitioned vector:\n ";
std::copy(std::begin(v), it, std::ostream_iterator<int>(std::cout, " "));
std::cout << " * " " ";
std::copy(it, std::end(v), std::ostream_iterator<int>(std::cout, " "));
std::forward_list<int> fl = {1, 30, -4, 3, 5, -4, 1, 6, -8, 2, -5, 64, 1, 92};
std::cout << "\nUnsorted list:\n ";
for(int n : fl) std::cout << n << ' ';
std::cout << '\n';
quicksort(std::begin(fl), std::end(fl));
std::cout << "Sorted using quicksort:\n ";
for(int fi : fl) std::cout << fi << ' ';
std::cout << '\n';
}
容器对迭代器的支持
并不是所有的stl容器均支持迭代器!
支持迭代器的容器
- array:
- vector:
- deque:双向队列
- forward_list:单向(前向)列表
- list:双向链表
- set:
- map:
不支持迭代器的容器
不支持迭代器的均是容器适配器,其提供对其他容器的统一接口的访问。
- stack:栈
- queue:
- priority_queue:优先队列
迭代器失效问题
在对容器元素访问的时候,迭代器可能会失效,即迭代器指向的元素已经失效(内存不可访问)。这个是哪怕老手也很容易忽视的问题,并且一旦出问题,非常难以排查定位!
- 顺序型容器:在修改元素时迭代器不会失效!而删除和插入时可能导致失效。(可能是因为内存可能重新申请)。
- 关联型容器:在修改/删除/插入时均可能导致迭代器失效!
参考
编辑于 2021-02-26 10:23
C++:迭代器
迭代器(iterator)
迭代器(iterator)功能:访问容器对象的元素。
所有标准库容器都可以使用迭代器,其中只有少数几种(vector)才同时支持下标运算符。
string对象不属于容器类型,但是string支持很多与容器类型类似的操作。
迭代器分为有效和无效,这点与指针差不多。
- 有效的迭代器:指向某个元素,或者指向容器中尾元素的下一个位置;
- 无效的迭代器:除上述的其他所有情况。
使用迭代器
迭代器不使用取地址符(要与指针区分开),有迭代器的类型同时拥有返回迭代器的成员,比如begin和end成员。
- begin成员:负责返回指向第一个元素(或第一个字符)的迭代器
- end成员:负责返回指向容器(或string对象)“尾元素的下一位置(one past the end)”的迭代器,即:该迭代器指示的是容器的一个本不存在的“尾后(off the end)”元素。所以又名:尾后迭代器(off-the-end iterator)简称尾迭代器(end iterator)(无实际意义,只是个标识而已)。
如果容器为空,则begin和end返回的是同一个迭代器,都是尾后迭代器[1]。
标准容器迭代器运算符返回迭代器所指元素解引用并获取该元素名为的成员,等价于令指示容器的下一个元素令指示容器的上一个元素判断两个迭代器是否相等不相等,如果两个迭代器指示的是同一个元素或它们是同一个容器的尾后迭代器,则相等;反之,不相等。
因为end返回的迭代器并不实际指示某个元素,所以不能对其进行递增或解引用的操作 [1]。
C++定义了 箭头运算符(->),把解引用和成员访问两个操作结合在一起。即it->mem和(*it).mem表达的意思相同 [2]。
迭代器类型
就像不知道string和vector的size_type成员到底是什么类型一样[3],一般来说也不知道迭代器的精准类型(其实也无需知道)。
拥有迭代器的标准库类型使用iterator和const_iterator来表示迭代器的类型:
- const_iterator:和常量指针差不多,只能读取,不能修改它所指元素的值;
- iterator:对其所指元素可读可写。
如果对象是常量,只能用const_iterator,如果对象不是常量,既能用iterator也能使用const_iterator。
begin和end运算符返回的具体类型,由对象是否是常量决定。如果对象是常量,begin和end返回const_iterator;如果对象不是常量返回iterator。
如果对象只需读操作,最好使用常量类型(const_iterator)。为便于专门得到const_iterator类型的返回值,C++11新标准引入了两个新函数,分别是cbegin和cend。
cbegin和 cend类似于begin和end,不同的是返回值都是const_iterator类型 [2]。
对vector对象的某些操作会使得迭代器失效
- 失效情况一:不能在范围for循环中向vector对象添加元素
- 失效情况二:任何可能改变vector对象容量的操作,比如push_back,都会使该vector对象的迭代器失效。
但凡使用了迭代器的循环体,都不要向迭代器所属的容器添加元素。
迭代器的算术运算
只要两个迭代器指向的是同一个容器中的元素,或者尾元素的下一位置,就能将其相减,所得结果是两个迭代器的距离。
所谓距离:右侧的迭代器向前移动多少位置就能追上左侧的迭代器,其类型名为difference_type的带符号整型数。
string和vector都定义了difference_type,此距离可正可负,所以difference_type是带符号类型[4]。
参考
- ^ab斯坦利·李普曼,约瑟·拉乔伊,芭芭拉·默,等. C++ Primer中文版:第5版[M]. 北京: 电子工业出版社, 2013: 95.
- ^ab斯坦利·李普曼,约瑟·拉乔伊,芭芭拉·默,等. C++ Primer中文版:第5版[M]. 北京: 电子工业出版社, 2013: 98..
- ^C++标准库string、其操作size()、size的输出类型string::size_type - 知乎
- ^斯坦利·李普曼,约瑟·拉乔伊,芭芭拉·默,等. C++ Primer中文版:第5版[M]. 北京: 电子工业出版社, 2013: 100.
编辑于 2019-09-09 17:39