C++ 迭代器,了解一下?
Xeonacid
2018-08-11 17:32:56
由于作者是个C++鶸,如果本文有任何错误,烦请不吝指出。(有的地方已知描述是不完整的,但是与指针行为一致,或鉴于多数看这篇文章的都是算法竞赛选手,抑或考虑实际,刻意忽略掉辣)
## 迭代器是个啥?
迭代器(Iterator):一种“能够迭代某序列内所有元素”的对象
![](https://i.loli.net/2018/08/11/5b6ea9ab74ecb.jpg)
指针都知道吧?~~不知道的先出门左转了解一下~~
迭代器:指针的抽象,指针是迭代器的子集
```cpp
#include <iterator>
```
## 迭代器能干啥?
所有迭代器都能做的操作:
```cpp
int main(){
/*
* suppose:
* T t;
* container<T> v1{t}, v2;
* container<T>::iterator
* container<T>::begin()
* container<T>::end()
* have been defined here
*/
container<T>::iterator ita = v1.begin(), itb = v2.begin();
*ita;
using std::swap;
swap(ita, itb);
++ita;
}
```
emmmm...这些跟指针没什么差别对吧。
![思考熊](http://img.uoj.ac/utility/bear-thinking.gif)真的没差别吗?
其实是有的...
指针不可能随便解引用或者交换一下,甚至你什么都没做,就非法化了对吧。但是就“迭代器”本身,不加任何限制的情况下,其只是一个可以做上述操作的类而已啦,随时有可能被非法化。
`*it`返回什么是未指定的,返回`void`也合~~fa♂~~ ~~三去~~法,意味着在这里你可能什么也做不了2333333
![捂脸熊](http://img.uoj.ac/utility/bear-facepalm.jpg)那我~~咬着~~要这迭代器有何用?
别急别急,迭代器还是有几个定义好的分类的,标准库内的迭代器何时会非法化也有严格限制。
在说这个之前,先提一嘴迭代器的一个辅助类型`std::iterator_traits`,其一般化定义是长这个样子滴:
```
template< class Iterator >
struct iterator_traits {
typedef typename Iterator::difference_type difference_type;
typedef typename Iterator::value_type value_type;
typedef typename Iterator::pointer pointer;
typedef typename Iterator::reference reference;
typedef typename Iterator::iterator_category iterator_category;
};
```
很简单对吧,直接从迭代器类型本身拿来了这几个成员,标准库提供了对于指针的特化,毕竟指针没有这些成员。
只有使`std::iterator_traits<T>`合法且含有上述五个成员,且能执行前面列出的那些迭代器操作的`T`类型才满足迭代器概念。
什么?你问我这五个鬼东西干啥用的?~~把他们翻译成中文就好了~~ ←真的如此,STL的命名还是挺清楚的哇
其中标准库自带了五个用于`iterator_category`的空类型,分别对应下方的前五种迭代器:
```cpp
struct output_iterator_tag { };
struct input_iterator_tag { };
struct forward_iterator_tag : public input_iterator_tag { };
struct bidirectional_iterator_tag : public forward_iterator_tag { };
struct random_access_iterator_tag : public bidirectional_iterator_tag { };
```
### 迭代器分类
- 输出迭代器(OutputIterator)
- 输入迭代器(InputIterator)
- 向前迭代器(ForwardIterator)
- 双向迭代器(BidirectionalIterator)
- 随机访问迭代器(RandomAccessIterator)
- 相接迭代器(ContiguousIterator)
---
- 可变迭代器(MutableIterator)
迭代器分类的依据是其可以进行的操作
![鼓掌熊](http://img.uoj.ac/utility/bear-applaud.gif)由上至下越来越像指针,~~越来越正常~~
#### 输出迭代器
```
typedef output_iterator_tag iterator_category;
```
##### 可自增(前置/后置、无操作)
##### 可`operator*`解引用(空操作)
返回的还是这个迭代器本身,所以不要做出`a = *it`这种操作,~~伦家明明是输出迭代器不要把窝搞成输入嘛~~~
实际上这个不是输出迭代器标准定义,但是STL中输出迭代器的实例就是这样的
##### 仅资瓷单趟算法
啥是单趟算法?只需要遍历这个序列一次的算法,不需要把当前位置迭代器记存一个副本,等以后再使用。
因为该序列上同一时间可能只有一个位置的迭代器是合法的
![](https://i.loli.net/2018/08/11/5b6ecf17aea62.jpg)输出迭代器这个名字看起来就是用于输出内容的,STL有两个小玩意叫做`std::ostream_iterator`和`std::ostreambuf_iterator`,把输出流包装了一下
```cpp
// 构造一个输出T类型的输出迭代器
// 第一个参数为绑定到的流
// 第二个参数为每次输出后输出的字符串,可省
std::ostream_iterator<T> it(std::cout, " ");
T t;
it = t; //这些都等同于 std::cout << t << " "
*it = t;
it++ = t;
++it = t;
*it++ = t;
// 跟上面那个一样,不过变成输出字符类型,也没有第二个参数了
std::ostreambuf_iterator<char> it(std::cout);
it = 'A'; // 等同于 std::cout << 'A'
*it = 'A';
...
```
STL还有几个用于插入序列的迭代器适配器,以及配套的为了方便的函数模板
```cpp
std::deque<int> q;
std::back_insert_iterator< std::deque<int> > it1(q);
// 等同于 auto it1 = std::back_inserter(q);
it1 = 1; // 等同于 q.push_back(1)
*it1 = 1;
...
std::front_insert_iterator< std::deque<int> > it2(q);
// 等同于 auto it2 = std::front_inserter(q);
it2 = 1; // 等同于 q.push_front(1)
*it2 = 1;
...
std::insert_iterator< std::deque<int> > it3(q, q.begin());
// 等同于 auto it3 = std::inserter(q);
it3 = 1; // 等同于 q.insert(q.begin(), 1)
*it3 = 1;
...
```
---
#### 输入迭代器
~~这玩意从名字看上去就与上面那个有许多相似之处,实际上也是~~
```
typedef input_iterator_tag iterator_category;
```
##### 可后置自增
##### 可默认构造
实际上输入迭代器标准定义不可默认构造,向前迭代器才可以,但是STL中输入迭代器的实例都是可以的
##### `operator*`返回`reference`,为可转换为`value_type`的引用
可以读取啦,但是不一定可以写入,因为返回的引用可能是`const`的(下面的输入流迭代器就如此)
为什么是“可转换为`value_type`的引用”呢?
![](https://i.loli.net/2018/08/11/5b6ecf17aea62.jpg)`std::vector<bool>`为了节约空间,每一个$01$位占一个bit而非一个byte,但是没有办法返回一个bit的对象,只能返回一个包装好的代理类辣,所以`std::vector<bool>::iterator::reference`是代理类的引用而非位引用或`bool&`
**PS:`std::vector<bool>`不是容器,不满足容器的要求,下文提到`std::vector`时均不包含`std::vector<bool>`**
~~C++真是一门难学的语言~~![](https://i.loli.net/2018/08/11/5b6eca17075cd.jpg)
##### 可比较相等和不等
##### `operator*`返回当前元素
##### 可使用`operator->`访问成员
##### 仅资瓷单趟算法
![](https://i.loli.net/2018/08/11/5b6ecf17aea62.jpg)把输出迭代器的栗子的输出改成输入就好了
emmmmm...等等,那EOF咋判断?![思考熊](http://img.uoj.ac/utility/bear-thinking.gif)
默认构造的输入流迭代器就代表EOF,判一下相等/不等就好了
```cpp
std::vector<int> v;
std::istream_iterator<int> i1(std::cin), i2;
while(i1 != i2) v.push_back(*i1++);
```
同一个位置的元素可以读多次,不过不能倒回来读
```cpp
std::istream_iterator<int> i1(std::cin);
int a = *i1, b = *i1, c = *++i1, d = *i1++; // 前提未EOF
```
`std::istreambuf_iterator`同理,只不过读入的变成了字符
![](https://i.loli.net/2018/08/12/5b6fc6e27d42a.jpg)那这个输入/输出迭代器比直接用`std::cin/cout`还麻烦啊!!!有啥用啊!!!
别急着骂我,主要是配合各种STL函数食用的
![](https://i.loli.net/2018/08/11/5b6ecf17aea62.jpg)众所周知,`std::vector`有这样一个构造函数,接收一对迭代器,将$[begin,end)$
范围内元素拷贝入容器(`std::vector<>::assign()`也是)
所以上面的代码可以改写成这样
```cpp
std::vector<int> v{std::istream_iterator<int>(std::cin), std::istream_iterator<int>()};
```
一行结束,简单多了对吧。输出的也同理
~~别问我这个在算法竞赛中的应用是什么,我知道你们都不会在比赛中用流式输入输出的~~,但是这些都是C++这门语言的重要组成部分,毕竟C++不是只用来算法竞赛的对吧
---
#### 向前迭代器
~~终于到比较正常的~~用的比较多的了
```
typedef forward_iterator_tag iterator_category;
```
##### 是输入迭代器
##### `reference`为引用
##### 提供多趟保证
可以放心地把迭代器存起来辣
不会因为解引用并赋值导致迭代器非法化
自增`it`的副本不改变解引用`it`得到的值
保证若`ita == itb`则
- 要么二者都不可解引用,要么指向同一对象
- `++ita == ++itb`
可以看成不可自减和随机访问的指针
![](https://i.loli.net/2018/08/11/5b6ecf17aea62.jpg)`std::forward_list`和无序关联容器(哈希容器)的`iterator`
---
#### 双向迭代器
```
typedef bidirectional_iterator_tag iterator_category;
```
##### 是向前迭代器
##### 可自减
行为与自增都类似
可以看成不可随机访问的指针
![](https://i.loli.net/2018/08/11/5b6ecf17aea62.jpg)`std::list`和有序关联容器的`iterator`
---
#### 随机访问迭代器
```
typedef random_access_iterator_tag iterator_category;
```
##### 是向前迭代器
##### 可以在常量时间内移动任意位置
##### 可以做加减法
##### 可以比较大小
##### 可以使用`operator[]`
指向数组元素的指针会用吧?一样的
![](https://i.loli.net/2018/08/11/5b6ecf17aea62.jpg)`std::vector, std::array, std::deque, std::string`的`iterator`、指向数组元素的指针
---
#### 相接迭代器
##### 是迭代器
##### 其所指向的逻辑相邻元素也在内存中物理上相邻
任意合法的`*(a + n)`等价于`*(std::addressof(*a) + n)`
---
顺便提一句,为什么是`std::addressof(*a)`而不是`&(*a)`呢
因为`operator&`是可以重载的...它可以返回任何奇怪的东西
所以C++11引入了`std::addressof`函数,专门用于返回一个对象的地址,其实现用了一些小trick,C++11及之后所有的标准库实现取地址用的都是这个函数而不是`operator&`
那么问题来了,如何在C++11之前将一个重载了`operator&`的类放入标准库容器呢?
![](https://i.loli.net/2018/08/12/5b6fd12ac0062.jpg)
~~C++真是一门难学的语言~~![](https://i.loli.net/2018/08/11/5b6eca17075cd.jpg)
---
![](https://i.loli.net/2018/08/11/5b6ecf17aea62.jpg)`std::vector, std::array, std::basic_string_view`的`iterator`、指向数组元素的指针
---
#### 可变迭代器
##### 是输入迭代器
##### 是输出迭代器
也就是可以读也可以写的
![](https://i.loli.net/2018/08/11/5b6ecf17aea62.jpg)所有STL容器的`iterator`(`const_iterator`除外)、指针(常量指针除外)
### 类型总结
![](https://i.loli.net/2018/08/11/5b6edd403bc5f.png)
来源cppreference
## 相对用的比较多的迭代器适配器
### `std::reverse_iterator`
反向迭代器,原迭代器+变-、-变+,提供原迭代器提供的所有功能
![](https://i.loli.net/2018/08/11/5b6ecf17aea62.jpg)STL容器的`rbegin(), rend()`返回的就是这个
### `std::move_iterator`
原迭代器需至少为输入迭代器,其`reference`为右值引用
关于`std::move`和右值引用...那应该是另一篇文章的内容了
## 迭代器非法化
See https://zh.cppreference.com/w/cpp/container#.E8.BF.AD.E4.BB.A3.E5.99.A8.E9.9D.9E.E6.B3.95.E5.8C.96
适配器均取决于其底层容器
(溜了溜了)
emmmm...还是说几个常见的吧
所有只读操作无
---
`std::vector`扩大重分配、`std::deque`插入+扩大重分配+非首尾擦除:全部
`std::vector`插入/擦除(无重分配):插入位置及其后
`std::deque`首尾擦除(无重分配):首尾
---
链表+有序关联容器:插入无,擦除仅被擦除元素
(毕竟它们是节点形式出现的)
---
哈希容器:
插入导致重哈希:全部
插入未导致重哈希:无
擦除:仅被擦除元素
## 一点废话
**在不需要返回值的情况下尽可能使用前置递增/递减,而非后置**
> 后置比前置慢
这个对于内置类型是假的,开不开优化都一样
![](http://img.uoj.ac/utility/emoticon-1.jpg)但是对于非内置类型就不一定了
像迭代器这种实现相对比较简单的类(绝大部分容器的迭代器底层都只是一个或几个指针),开了优化可能会被优化成一样
但是如果是复杂一点的就不一定了2333333
小建议:(求不喜勿喷)干脆全改成前置好了,反正不会慢对吧,要不然写的时候还要想一下是不是内置类型,也挺麻烦的![](http://static.tieba.baidu.com/tb/editor/images/client/image_emoticon25.png)
**使用`std::array`而非原生数组**
窝个人觉得,OOP的封装性的优势在这体现地淋漓尽致,不用管底层怎么搞的,用就好辣
其成员函数什么的都是STL的命名格式,会用一个STL容器就会用其他的\_(:з」∠)_
一维数组还好办,二维及以上的话指针、`sizeof`什么的比封装好的麻烦多了(表喷窝TAT,再熟练也改变不了它麻烦的事实呀)
常数这个无需担心,`std::array`只是把原生数组封装了一下,效率没有任何差别,成员函数都是内联的![鼓掌熊](http://img.uoj.ac/utility/bear-applaud.gif)
然鹅...这个东西是C++11才有的,C++98的话也可以自己封装一下嘛,几分钟就写完了(逃)
## 参考资料
[ISO/IEC 14882:2017 Programming languages -- C++](https://www.iso.org/standard/68564.html):952-986.
[cppreference.com](https://en.cppreference.com/w/).
(德)约祖蒂斯(Josuttis,N.M.)著;侯捷译.C++标准库:第2版[M].北京:电子工业出版社,2015.6:433-474.