从C到C++___STL入门(二)迭代器

STL入门(二)迭代器

迭代器是广义的指针。

1. 为什么需要迭代器

C++中使用模板,模板使得算法独立于存储的数据类型,而迭代器使得算法独立于容器类型(即数据结构)

例如:不同的数据结构:数组和链表,它们都可以实现find()函数,即查找算法,我们能不能用一种通用的方法使得,数组和链表都可以实现查找功能?迭代器的出现使得算法独立于数据结构。

我们看一下查找算法

iterator find(iterator begin,iterator end,const elemtype & val )
{
    iterator ar;
    for(ar=begin;ar!=end;ar++)
        if(*ar==val)
            return ar;
    return end;
}

上面这段代码中iterator就是合适的迭代器类型,而elemtype就是查找元素类型,find()方法的前两个参数是用来确定查找区间的,最后一个参数是提供要查找的数据。
我现在断言:只要提供合适的迭代器,数组和链表两种容器都可以使用上面这个算法。

我们看看这个迭代器需要满足什么条件?

  • 能够解除应用*ar
  • 能够进行比较ar!=end
  • 能够进行赋值运算ar=begin
  • 能够使用ar++

那么,只要我提供的迭代器满足上面这四个条件,那么就可以使用上述的查找算法。

我们知道对于容器:double数组来说,迭代器类型就是double*指针。

//迭代器1.cpp
#include<iostream>
typedef double* iterator;
typedef double elemtype;
iterator find(iterator begin,iterator end,const elemtype & val )
{
    iterator ar;
    for(ar=begin;ar!=end;ar++)
        if(*ar==val)
            return ar;
    return end;
}

int main()
{
    using std::cout;
    using std::endl;
    double a[10]={1.21,2.34,4.12,7.0,8,0.25,8.49,9.99,1.25,3.75};
    double tofind=0.25;
    iterator n=find(a,a+10,tofind);
    cout<<*n<<endl;
}

对于结构体链表来说,我们如何设计自己的迭代器呢?我们可以使用类。

struct Node
{
   elemtype item;
   shared_ptr<Node> next;
};
class iterator
{
private:
    shared_ptr<Node> p;
public:
    iterator(shared_ptr<Node> pr=0):p(pr){}
    double operator*(){return p->item;}
    bool operator!=(iterator  q)
    {
        if((**this)==*q)
            return false;
        else
            return true;
    }
    iterator operator++(int)
    {
        iterator tmp=*this;
        p=p->next;
        return tmp;
    }
};

以上就是我们为结构体链表设计的迭代器。它满足实现算法的最低要求,可以解除引用operator*,进行比较判断operator!=,进行移动operator++(int)

注意了,C++将operator()作为前缀版本,而operator++(int)是其后缀版本

下面我们测试迭代器:

#include<iostream>
#include<memory>
using std::shared_ptr;
typedef double elemtype;
struct Node
{
   elemtype item;
   shared_ptr<Node> next;
};

class iterator
{
private:
    shared_ptr<Node> p;
public:
    iterator(shared_ptr<Node> pr=0):p(pr){}
    double operator*(){return p->item;}
    bool operator!=(iterator  q)
    {
        if((**this)==*q)
            return false;
        else
            return true;
    }
    iterator operator++(int)
    {
        iterator tmp=*this;
        p=p->next;
        return tmp;
    }
};

iterator find(iterator begin,iterator end,const elemtype & val )
{
    iterator ar;
    for(ar=begin;ar!=end;ar++)
        if(*ar==val)
            return ar;
    return end;
}
int main()
{

    shared_ptr<Node> end(new Node{0,0});
    shared_ptr<Node> elem5(new Node{9.37,end});
    shared_ptr<Node> elem4(new Node{3.25,elem5});
    shared_ptr<Node> elem3(new Node{0.25,elem4});
    shared_ptr<Node> elem2(new Node{1.73,elem3});
    shared_ptr<Node> elem1(new Node{4.89,elem2});
    shared_ptr<Node> begin(new Node{6.67,elem1});
    //以上是构建链表

    iterator dot=find(begin,end,1.73);
    std::cout<<*dot<<std::endl;

}

经过上面两个例子,我们看出来只要存在合适的迭代器,算法就可以独立于数据结构。现在我们明白了迭代器的重要性了。

2.迭代器的种类

刚刚我们自己设计了一个迭代器,但是不同的算法对迭代器的要求也不同。我们根据迭代器提供的接口可以把迭代器分为5种:

  • 输入迭代器
  • 输出迭代器
  • 前向迭代器
  • 双向迭代器
迭代器功能输入输出正向双向随机访问
解除引用读取
解除引用写入
固定和可重复排序
++i i++
–i i–
i[n]
i+n
i-n
i+=n
i-=n

上面这里i是迭代器类型,n是整型

  • 输入迭代器

输入是从程序的角度来说的,即来自容器的信息被视为输入。则输入迭代器的要求很低是:能够遍历所有元素且可以读取。而且我们不要求固定和重复排序。而且迭代器递增后,先前的值不保证可以解除引用。
举个容易理解的例子:容器就像一个黑盒子,数据就像盒子里面的马铃薯,输入迭代器的要求就是,我们可以把马铃薯从黑盒子里面全部拿出来看看,而且看完就放回去,而且不允许我们重复拿同一个马铃薯。马铃薯不需要整齐的排列在盒子中,我们每次遍历马铃薯的顺序,不需要固定,我们只需要随手抓一个就行了。

  • 输出迭代器

输出是从程序的角度来说的,即将信息从程序传输给容器。输出迭代器和输入迭代器类似,它只能写入,但不能读取。
还是马铃薯的例子:我们从黑盒子中拿出一个马铃薯,看都不看一眼,直接扔了,然后去地里挖一个马铃薯放仅黑盒子里面,而且不允许我们重复拿同一个马铃薯。马铃薯不需要整齐的排列在盒子中,我们每次遍历马铃薯的顺序,不需要固定,我们只需要随手抓一个就行了。

  • 前向迭代器

或者称正向迭代器,它可读可写,而且它总是按相同的顺序遍历一些列的值,是单向遍历。我们在谈输入输出迭代器的时候,我们的数据根本没有顺序可言,只要能遍历完就算成功,就好像装在麻袋里面的马铃薯,随手抓一个。
不知道你有没有买过羽毛球,一盒羽毛球就支持前向迭代器,我们可以一个一个拿出羽毛球,顺序都是固定的,但是我们不能跳过第一个羽毛球直接去拿第三个羽毛球,而且我们永远只能单向遍历,因为开口只有一个。

  • 双向迭代器

在前向迭代器的基础上我们可以双向遍历容器。
双开口的一盒羽毛球就支持双向迭代器。

  • 随机访问迭代器

我们可以直接跳过第一个羽毛球去拿第三个羽毛球,这就是随机访问,随机访问迭代器是在双向迭代器的基础上加了随机访问。
一盒巧克力就支持随机访问迭代器,巧克力放在盒子中的一个个小格子中,我们想吃哪个就拿哪个。

当然所有迭代器都可以进行关系运算符运算,最基本的==!=是要有的。

经过上面的介绍,不难发现,正向迭代器具有输入输出迭代器的全部功能,双向迭代器具有正向迭代器的功能,随机访问迭代器具有正确迭代器的全部功能。而且我们发现迭代器的类型,和数据的组织形式息息相关,例如链表最多只能提供双向迭代器而不能提供随机迭代器。

  • 为何需要这么多迭代器?

目的是在编写算法时尽可能使用要求最低的迭代器,例如find()算法只需要输入迭代器就行了,find()算法对迭代器要求很低,也就是说所有容器都支持find()算法。

每个容器都定义了一个typedef名称iterator,它就是迭代器,但是迭代器的类型取决于容器类型(数据结构),例如矢量类是随机迭代器,链表类是双向迭代器。容器的迭代器类型直接决定了,我们可以对容器使用的算法。

3.使用迭代器

迭代器就是广义指针,而指针满足迭代器的所有要求。迭代器是STL算法的接口,所以指针也是STL算法的接口。例如我们可以把STL算法用于非STL容器,例如我们可以对数组使用算法。

double Receipts[SIZE];
sort(Receipts,Receipts+SIZE);

注意这里Receipts+SIZE就是指向超尾的迭代器。所以说,C++使得超尾概念应用于数组,使得STL算法用于常规数组。

3.1 STL预定义迭代器:ostream_iteratoristream_iterator

STL中有一种copy()算法,它把数据从一个容器复制到另一个容器中,它接受三个迭代器参数,前两个迭代器确定要复制的区间,最后一个迭代器确定要复制到什么位置(起始位置)。函数会覆盖目标容器中的已有数据,同时目标容器必须足够大,以便能够容纳复制的元素。

int casts[10]={6,7,2,9,4,11,8,7,10,5};
vector<int> dice(10);
copy(casts,casts+10,dice.begin());

copy()的前两个参数最起码是输入迭代器,最后一个参数最起码是输出迭代器。

C++中有一个表示输出流的迭代器模板,还有一个表示输入流的迭代器模板。这些迭代器模板都在iterator头文件中

  • ostream_iterator迭代器模板

用STL的语言来说,它是一个模型,也是一个适配器(adapter),可以将一些其他接口转换成STL使用的接口。简单的说,ostream_iterator可以让一些对象转化成迭代器,以适配STL算法。

#include <iterator>
...
ostream_iterator<int,char> out_iter(cout," ");

ostream_iterator有两个模板参数,第一个是指出发送给输出流的数据类型,第二个是指出输出流使用的字符类型(也可以使用wchar_t);而这个迭代器的构造函数也接受两个参数,第一个cout指出要使用标准输出流,你可以使用文件输出流,第二个参数" ",指出了在发送给输出流的每个数据项后显式的分隔字符

//迭代器3.cpp
#include<iterator>
#include<iostream>
#include<string>

int main()
{
    using std::string;
    using std::cout;
    using std::ostream_iterator;
    ostream_iterator<int,char> out_int(cout,"$");
    ostream_iterator<string,char> out_string(cout,"#");
    *out_string++="I am a test.exe";
    *out_string++="numbers:";
    for(int i=0;i<10;i++)
        *out_int++=i;
}
I am a test.exe#numbers:#0$1$2$3$4$5$6$7$8$9$

我们看出来这个迭代器可以直接访问输出流,并且对输出流做修改。out_intout_string都是输出迭代器,因为它对输出流做写操作。

那么我们可以对这个迭代器使用copy函数

//迭代器4.cpp
#include<iterator>
#include<iostream>
#include<string>
#include<vector>
#include<algorithm>
int main()
{
    using std::vector;
    using std::ostream_iterator;
    using std::string;
    using std::getline;
    using std::cout;
    using std::copy;
    vector<string> a(5);
    cout<<"Enter the strings: \n";
    for(auto &x:a)
       getline(std::cin,x);
    ostream_iterator<string,char> cout_string(cout,"\n");
    cout<<"after copy:\n";
    copy(a.begin(),a.end(),cout_string);
}
Enter the strings: 
apple
banana
cat
dog
elephant
after copy:
apple
banana
cat
dog
elephant

其实,也可以使用匿名迭代器:
copy(a.begin(),a.end(),ostream_iterator<string,char>(cout,"\n"));

  • istream_iterator迭代器模板

同样的,istream_iterator适配器使得istream类对象转化为迭代器。

istream_iterator<string,char> eos;
istream_iterator<string,char> cin_string(cin);

istream_iterator的模板参数和ostream_iterator一样,但是他的构造函数有不同,如果采用默认构造函数,那么就会返回输入流的超尾迭代器;也可以用istream类对象cin作为唯一参数,那么就会返回标准输入流的第一个迭代器。

#include<iterator>
#include<iostream>
#include<algorithm>
int main()
{
    using std::copy;
    using std::cin;
    using std::cout;
    using std::endl;
    cout<<"Enter 5 values: \n";
    std::istream_iterator<int,char> eos;
    std::istream_iterator<int,char> cin_int(cin);

    int arr[5];
    copy(cin_int,eos,arr);
    cout<<"results:\n";
    for(auto x:arr)
        cout<<x<<endl;
}
Enter 5 values: 
1 2 3 4 5
^Z
results:
1
2
3
4
5

3.2 STL其它预定义迭代器

  • reverse_iterator迭代器模板

(我们不关注这个模板如何实例化,我们使用auto关键字)
我们需要反向遍历容器,幸运的是,我们确实有反向迭代器,reverse_iterator
vector类中有一个名为rbegin()的接口,它返回一个指向超尾的反向迭代器,rend()返回一个指向第一个元素的方向迭代器。而且我们对反向迭代器做++,将导致迭代器递减。那么我们可以这样使用这两个迭代器:

vector<int> dice(10);
ostream_iterator<int,char> int_cout(cout," ");
....
copy(dice.rbegin(),dice.rend(),int_cout);

上面这段代码就可以反向打印矢量类

还有一件事是,反向迭代器的operator*和正向迭代器不一样,反向迭代器解除引用会返回前一个元素的值,例如反向迭代器r指向第6个元素,那么*r将是位置5的值。

//迭代器6.cpp
#include<iostream>
#include<iterator>
#include<vector>
int main()
{
    using std::copy;
    using std::vector;
    using std::cout;
    using std::endl;
    using std::ostream_iterator;
    int cast[10]={7,6,4,10,12,1,5,8,0,17};
    vector<int> dice(10);
    copy(cast,cast+10,dice.begin());
    cout<<"显示矢量内容:";
    ostream_iterator<int,char> int_cout(cout," ");
    copy(dice.begin(),dice.end(),int_cout);
    cout<<endl;
    cout<<"逆序显式矢量内容:";
    copy(dice.rbegin(),dice.rend(),int_cout);
    cout<<endl;
    cout<<"再次逆序显式矢量内容:";
    for(auto i=dice.rbegin();i!=dice.rend();i++)
        cout<<*i<<" ";

}
显示矢量内容:7 6 4 10 12 1 5 8 0 17 
逆序显式矢量内容:17 0 8 5 1 12 10 4 6 7 
再次逆序显式矢量内容:17 0 8 5 1 12 10 4 6 7 
  • insert_iteratorback_insert_iteratorfront_insert_iterator迭代器模板

这些都是插入迭代器,这些迭代器有个特点,它能使容器的长度增大。

insert_iterator的实例化:

insert_iterator<vector<int>> insert_iter(dice,dice.begin());`

我们可以看出来它的模板参数接受一个容器类型,它的构造函数接受一个容器标识符以及一个容器的迭代器。上面这句代码创建了一个名为inset_iter的插入迭代器,而且它指向的位置是dice.begin()

为什么插入迭代器能扩容?

首先copy()算法,没有扩容的功能,但是我们的插入迭代器中有一个它的模板参数对应的push_back()方法,push_back()方法会使得容器扩容。

back_insert_iterator的实例化:

back_insert_iterator<vector<int>> back_iter(dice);

我们发现它的模板参数也是一个容器类型,但是构造函数中少了一个迭代器参数,这是因为这是一个指向dice.end()的插入迭代器,它自然不需要额外参数了

同理,front_insert_iterator的实例化:

front_insert_iterator<vector<int>> front_iter(dice);

上面这段代码,front_iter是一个指向dice.begin()的插入迭代器

//迭代器7.cpp

#include<iostream>
#include<iterator>
#include<vector>
#include<string>
#include<algorithm>
void output(const std::string& s)
{
    std::cout<<s<<" ";
}

int main()
{
    using std::string;
    using std::copy;
    using std::vector;
    using std::back_insert_iterator;
    using std::insert_iterator;
    using std::for_each;
    using std::cout;
    vector<string> dice(4);
    string a1[4]=
    {
        "apple",
        "banana",
        "cat",
        "dog"
    };
    string a2[2]=
    {
        "favorite",
        "love"
    };
    string a3[2]=
    {
        "insert",
        "pizza"
    };
    copy(a1,a1+4,dice.begin());
    cout<<"\n第一次显示:";
    for_each(dice.begin(),dice.end(),output);
    copy(a2,a2+2,back_insert_iterator<vector<string>>(dice));
    cout<<"\n第二次显示:";
    for_each(dice.begin(),dice.end(),output);
    copy(a3,a3+2,insert_iterator<vector<string>>(dice,dice.begin()));
    cout<<"\n第三次显示:";
    for_each(dice.begin(),dice.end(),output);

}
第一次显示:apple banana cat dog 
第二次显示:apple banana cat dog favorite love 
第三次显示:insert pizza apple banana cat dog favorite love 

经过这一节的学习,我们发现一个事实:copy()不仅可以将信息从一个容器复制到另一个容器,还可以将信息从一个容器复制到输出流,从输入流复制到容器中,还可以使用copy()将信息插入另一个容器中。所以,使用同一个函数可以完成很多工作,主要取决于使用什么样的迭代器。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值