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_iterator
和istream_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_int
和out_string
都是输出迭代器,因为它对输出流做写操作。
//迭代器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_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);
//迭代器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()
将信息插入另一个容器中。所以,使用同一个函数可以完成很多工作,主要取决于使用什么样的迭代器。