《白话C++》第10章 Page109 10.5.1 迭代器基本分类

10.5.1迭代器基本分类

如果将容器比喻成公园,那么迭代其就是导游。C++标准库的容器和迭代器分开设计。

前者重点负责存储元素,后者负责遍历元素,不同的迭代器可以有不同的迭代路线(策略),并且通常多个迭代器可以同时“行走”在同一个容器之上。

迭代器遍历,也需要遵循容器的特点。比如单向的列表,迭代器就只能在一个方向上前进。

迭代器以只读型迭代器居多,但也有一些写入型迭代器,它会在迭代过程中,改变容器内部数据,造成正在访问该容器的其他迭代器失效。

在使用的语法上,迭代器之于容器非常类似于指针之于指针所指向的内存。以一个裸数组和指向该数组的指针为例:

char buf[] = {'A','B','C','D','E'}; //数组 = 容器
char * p = buf; //指针 = 迭代器

//相关操作
//1. 通过'*'可以取到当前指针指向位置数组中的元素
char c = *p; // c is 'A' now

//2. 通过++或--,可以改变指针指向数组的位置
p++;
++p;
c = *p; // c is 'C' now
--p;
c = *p; //c is 'B' now

将int裸数组换为 list <char>,指针 char* p 换成 list <char>::iterator,代码如下:

list <char> buf =  {'A', 'B', 'C', 'D', 'E'}; //list=容器
list <char> :: iterator p = buf.begin(); //iterator = 迭代器

//相关操作
//1. 通过'*'可以取到当前迭代器指向位置上,容器中的元素
char c = *p;

//2. 通过++或--,可以改变指针指向数组的位置
p++;
++p;
c = *p; //c is 'C' now
--p;
c = *p; //c is 'B' now

迭代器操作被刻意设计得和指向数组的指针操作非常类似。反过来,很多时候我们把裸指针也视为一种迭代器。

1.输入输出迭代器

根据从容器读出数据,还是往容器写入(删除,修改或添加)数据,迭代器可区分为输入和输出迭代器。

说到输入输出,我们最早学习的是cin/cout,二者都是“流”。广义上,“流”包括标准输入输出设备,文件,网络,或者内存流。多数情况下我们直接在“流”上面进行输入输出操作,以stringstream为例:

#include <sstream>
...
std::stringstream ss;
//输出操作:将字符串内容输出到“流”中
ss << "123 456"; 
int i1,i2;
//输入操作:将“流”中的数据输入到变量
ss >> i1 >> i2; 

今天学习另一种方法:将迭代器架在“流”上面,通过迭代器访问“流”。且慢,说到“流”,容易想到“流动”,即“流”本身“会动”;说到“迭代器”,自然关注“迭代”,然后想到迭代器可以“游历”容器。因此,如果将迭代器架在“流”身上,到底是树动?风动?还是心在动?

标准要求“输入迭代器(Input Iterator)”能从容器读出数据;同时也要求输入迭代器提供前进到容器下一可读位置的能力,但不保证在容器的同一位置上读多次,所读的内容或结果一致;不管是同一迭代器连续读多次,还是不同迭代器指向容器同一位置同时读多次。

同时,标准要求“输出迭代器(Output Iterator)”能够向容器写入数据;同时也要求输入迭代器提供前进到容器下一可写位置的能力,但不保证在容器的同一位置写多次,使得内容或结果一致;不管是同一迭代器连续写多次,还是不同迭代器指向容器同一位置同时写多次。

输入迭代器(Input Iterator)

下面以stringstream为例,在上面搭建迭代器,先测试输入迭代器,即通过迭代器从流中读出数据。

#include <iterator>

void test_iterator_on_stream()
{
    std::stringstream ss("123 456 789");
}

在“流”之上进行输入操作的迭代器,就叫“输入流迭代器(istream_iterator)”。在标准库中,它是一个类模板。定义流迭代器,需要指定以何种类型解读将要流动的数据,这里是int,将它作为模板类入参,再以“流”对象作为迭代器构造入参

void test_iterator_on_stream()
{
    std::stringstream ss("123 456 789");

    istream_iterator <int> ii(ss);
}

接下来就可以从迭代器身上读取数据了。直接从“流”中读数据时,可以通过eof()方法判断流是否结束,我们将“流”视作“容器”,但“流”并没有提供“begin()/end()”等方法指定开始和结束的位置。事实上许多类型的“流”没有确切的结束位置,比如从键盘输入,不可能事先知道用户什么时候结束某次输入。

为此,标准做一个简单的约定默认构造的流迭代器,就表示流的结束位置

void test_iterator_on_stream()
{
    std::stringstream ss("123 456 789");

    istream_iterator <int> ii(ss);
    istream_iterator <int> eof; //结束位置  默认构造

    for(; ii != eof; ++ ii)
    {
        std::cout << *ii << std::endl;
    }
}

为什么要将对“流”的访问,封装成对迭代器的操作?

这是因为标准库的许多算法(函数模板或函数)都以迭代器作为入口,有了“流迭代器”,就可以让不少算法可以套用,并最终作用在“流”身上。

比如,标准库有min_element算法用于从指定迭代器范围内,取出最小的元素,它的入参要求是迭代器:

//min_element算法
template <typename ForwardIterator>
ForwardIterator min_element (ForwardIterator first
                            , ForwardIterator last);

将它套用在“流”身上,先定义一个含有多个整数的字符串流:

//标准库常用算法
#include <algorithm>

void test_min_element_on_istream_iterator()
{
    std::stringstream ss("90 89 100 45 78");
}

将“流”中的数据视为整数,则最小的那个是45,配合使用min_element和流迭代器,可以快速地将它从“流”中揪出来:

“cin”是标准输入流,所以也可以在它身上搭建输入流迭代器,再套用算法,这次我们一是将元素类型以字符串对待,二是改成找最大元素:

#include <string>
#include <iterator>
#include <algorithm>

void test_max_element_on_istream_iterator()
{
    std::istream_iterator <std::string> beg(cin);
    std::istream_iterator <std::string> end;

    auto the_max = std::max_element(beg, end);
    std::cout << *the_max << std::endl;
}

在Windows控制台下运行以上测试函数,输入数字后,先按回车键,再按F6或者Ctrl + Z,再按回车,就可以得到结果。

输出迭代器(Output Iterator)

再说说“输出迭代器(Output Iterator)”的例子。我们准备一个裸数组,视之为容器;准备一指向它的指针,视之为输出迭代器;接着使用标准库的copy算法将标准输入cin得到的数据,复制到裸数组中,最后使用copy算法将数组中的元素,复制到标准输出cout

先看copy算法的声明:

template <typename Input, typename OutputIt>
OutputIt copy(InputIt first, InputIt last, OutputIt d_first);

copy将从[first, last)  范围内复制元素从 d_first 开始一个个写入目标容器,当然,从入参看不到容器,因为这又是一个面向迭代器的算法。

copy算法返回最后一次输出时的迭代器位置:

以上主要以“流迭代器”为例说明输入和输出迭代器,千万别误以为纯正的容器,比如vector,list等没有输入输出迭代器(倒是“流迭代器”从实现的角度上看,算不得“纯正”的迭代器,它们更像某种适配器)。

2.前向/双向迭代器

前面用“写”或“读”的角度区分迭代器,

“前向/双向”迭代器则从迭代器可行进的方向进行区分。

注意,不是“前向”和“后向”之分,而是“只能前向”和“既能前向又能后向”之分。

前向迭代器称为“Forward Iterator”,双向迭代器称为“Bidirection Iterator”。一个双向迭代器肯定同时也是一个前向迭代器。

裸指针通常是双向迭代器,因为它既能通过“++”操作符前进,也可以通过“--”操作符后退。

p++;
p--;

++p;
--p;

推荐能使用前置++(或--)就使用前置,对于多数迭代器,前置操作性能更好。

为什么会有“只能一个方向上前行”的迭代器呢?主要是因为许多业务,不需要在容器中双向前进,可以简化容器的结构。

3.随机访问迭代器(Random-Access Iterator)

“随机访问迭代器”也是一种“双向访问迭代器”,事实上它可以想访问容器哪个元素,就直接跳去访问这个元素(这里的随机和随机数意义不同,更多是随心所欲的一是)。裸数组是典型的,支持随机访问的容器,所以指向它的指针就是一个随机访问迭代器:

p[3] = 'd';//直接访问第四个元素

//往前直接跳过两个元素(而不是一步步迈过去)
p += 2; 
p -= 2; //往后跳多步

以 p -= 2; 为例,前向迭代器不支持后退,没有“--”和“-=”操作。

双向迭代器可以通过两次后退模拟实现,但这是“伪随机”。

在标准库中,std::vector是支持随机访问的典型容器。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值