迭代器之入门介绍(上)

迭代器之入门介绍(上)

什么是迭代器(iterator)?对于没有接触过STL或类似模式的库的同学们可能会觉得摸不着头脑,而刚刚接触STL的同学们这个概念也可能会让他们迷惑,迭代?器?貌似挺复杂的啊。

其实一点也不复杂。我们先不管它们具体的写法、用法,先来看看到底迭代器是个什么东西。

其实它还有另外一种更加形象的翻译名字:游标。这个名称对初学者来说其实更能帮助他们理解。游标卡尺见过吧?(什么?没见过?你们初中物理不上实验课的吗?……)游标卡尺上面就有一个可以来回移动的游标,通过移动它指向不同的地方我们就可以测量出物体的长度。

其实这里的STL,和你中学实验课手上拿的那个家伙的概念是一样的。

如果你不是急躁到非游标不听,那么还是听我从STL娓娓道来吧……


算法和容器,迭代器之由来

大家都知道STL的大三主体:算法、容器、迭代器。(不知道?不要紧,慢慢往下看)

容器顾名思义,就是可以存放东西的东西。本质是一堆封装好的数据结构,比如常见的数组、链表、队列、栈等等。(什么?没听过这些?那还是赶快回去找本书恶补下吧-_-
还记得自己大学数据结构课程上被要求写一个链表时调试的那难受劲么?呵呵,有了STL封装好的这些常用数据结构,我们就不用自己头疼写那些玩意啦,这里有世界大师们写好的东西供我们使用。(什么?你当时一点也不难受不头疼?好吧,算你talent
有现成的容器当然很爽啦,不过我怎么又想起了大学算法课上写快速排序的时候那一个难受啊。。(喂喂,有完没完?)哎呀这些算法也是最常用的嘛,怎么就没人写好一个来供我用呢?想得美。的确我想的很美,STL果然就有大师们写好的算法了,嘿嘿,用就是啦。

好了啰嗦了这么多,估计你会认为我是个弱智,同时你也会发现貌似有了容器和算法,STL已经很完整了,还要搞个迭代器这么难听的名字的东西干吗?

嗯,接下来我们设想你非常talent,于是成为了大师,负责来写这些STL的算法。
就来写一个最简单的算法吧:在一个容器中找到最小的那个元素。
你一想这还不简单:

C++ 代码:

template<typename T,typename Cont> //Cont是容器的类型,T是容器中元素的类型,如果你不知道什么是模版,建议还是先回去找本书补一补
T findMin(Cont& v)
{
  T tmp;
 
for(int i=0;i<v.size();++i)
  {
   
if(tmp>v[i]){tmp=v[i];}
  }
 
return tmp;
}


ok
,很好。但是又有人说,啊呀,这个只对数组类的容器有用,对链表没用啊,你再为链表写一个吧。
你皱皱眉头,说,行。于是又操起键盘敲敲打打,ok了。
结果第二天又有人来,说,啊呀啊呀,这个还不够啊,对二叉树没用啊,你再写一个吧……
后来,老大干脆过来,拍拍你的肩,说,STL是可以扩展的,以后还会有更多的容器,你的算法都得跟上啊……
是不是想吐血?
好吧我相信你的实力,你不会吐血,这些对你来说小菜一碟,这个算法写n遍你都没问题。可是算法多着呢,求距离、排序、稳定排序、变序、剔除重复、上界、下界、建堆……不下m个,你怎么办?写m×n个算法版本?好吧再算你毅力如夸父逐日,但是扩展怎么办啊?更多的用户写的新容器、更多的用户写的新算法,你怎么兼容?

我承认我上面废话那么多是为了练习语文老师教我的作文欲扬先抑的写法。
其实你知道我写这么多只是为了引出一句话:
STL的一个基本思想就是,采取某种措施将算法与容器分开。
这样我们就不必为每个容器单独写算法了。

对,它采用的措施就是迭代器。
使用一个小小的游标来指明一个元素在容器中的位置。这样我们就可以针对这个游标来写算法,完全不用管容器是什么啦。

比如上面那个算法,我们现在就可以这么写:

C++ 代码:

//如果你编译这段代码出现错误,请看本文最后附注
template<typename T,typename Cont>
T findMin(Cont& v)
{
  T tmp;
 
for(Cont::iterator it=v.begin();it!=v.end();++it)
  {
   
if(tmp>(*it)){tmp=(*it);}
  }
 
return tmp;
}


慢着,这都是什么啊?
别急,我来仔细解释下
Cont
是容器的类型,容器中定义了一个此容器所用的游标(迭代器)的类型,叫iterator,于是Cont::iterator it;就定义了一个名叫it的迭代器。
啥?我咋知道容器中怎么定义了iterator这个类型?嘿嘿,这就是一个Concept了。这是约定,这是规矩!STL中所有的容器都定义了一个名叫iterator的类型来表示它所用的迭代器,下文中的begin()end()成员函数也是,所有容器都有,未来要适用于STL的容器也得有,因此你就不用担心啦。(即使实在没有,也有一些办法
现在我们有了游标,还要让它指向正确的位置。v.begin()就是这么一个位置。它代表容器的起始位置,返回值是一个游标,指向容器的头部。于是Cont::iterator it=v.begin();这句就是定义一个迭代器并且让它指向容器的第一个元素的位置啦。
那么v.end()也就很好理解了,它返回容器的最后一个元素的位置……错!是最后一个元素后面的一个位置
有点绕口?记得文件读取的时候有一个EOF位置么?和那个很相似,也就是越过最后一个元素(pass the end),它后方的这么一个位置。
为啥要搞这么复杂?返回最后一个元素的位置多简单直白啊?嗯,STL是有它的考虑的。

区间
这个考虑就是,为了方便表示区间这个概念。
大师们比你考虑的要全面。他们想很多算法不仅仅是作用于一个容器之上,而是作用于容器的某一部分。
比如我只想找容器中前30个元素中最小的。那么上面那个算法就没办法啦,它只能一股脑的把整个容器扫描一遍。这时候是不是就想起了中学数学中区间这个概念?如果我指明一个区间,指定区间起始,区间末尾,那么算法就可以针对这个区间上元素进行操作。这也就是迭代器概念引入STL的另外一个原因——有了迭代器,就可以不针对容器写算法,而是针对迭代器写算法。
我们现在再把上面的那个寻找最小值代码改一下:

C++ 代码:

template<typename T,typename Iterator>
T findMin(Iterator begin,Iterator end)
{
  T tmp;
 
for(Iterator it=begin;it!=end;++it)
  {
   
if(tmp>(*it)){tmp=(*it);}
  }
 
return tmp;
}


好了,现在函数传入的参数不再是一个容器,而是两个迭代器,这两个迭代器分别代表区间的起始和末尾,
这样我们的算法也就可以完成上面所说的局部扫描的功能啦。

STL规定的区间,就是用两个迭代器来表示:一个指向区间的第一个元素位置,作为区间的起始,一个指向区间的最后一个元素的后面的那个位置(pass the end),作为区间的末尾。

Hal


如果你还没有把你的中学数学都忘光的话,你一定会觉得这个概念和数学上的半开半闭区间的概念很相似。是的,它们是一致的。事实上STL中的区间都是左闭右开区间。区间[1,5)中有1234,但是没有5。同样[begin,end)中没有end这个位置,end是最后一个元素后面的那个虚位置,很好理解了吧?

为啥要这样做呢?一个主要的好处是,空区间就很容易表示了,起始和末尾都是同一个位置的区间就是空区间,不需要做任何特殊处理。同时还有一些其它好处,比如一些搜寻算法,如果没有找到符合条件的元素,可以返回这个区间的末尾来表示没有找到的概念,而如果没有末尾这个特殊的位置的话就会很难办。

迭代器的基本操作

好了我们扯了个大圈子,虽然搞清楚了那个诡异的end位置以及区间的概念,但是貌似还是有些不对劲的地方,这个游标(迭代器)光指向一个位置有屁用啊,不能操作元素还不是废物一个。
好好不要这么FQ,回到上面的代码。
  for(Iterator it=begin;it!=end;++it)
  {
    if(tmp>(*it)){tmp=(*it);}
  }
你肯定也看出来了,迭代器貌似和指针很相似。通过*操作符可以取到它所指的位置的元素的值。
you are right
。事实上,迭代器就是对指针这个概念的高层抽象。
而且和指针一样,如果这个迭代器不是常量迭代器(后文会讲),那么一般情况下也是可以修改它所指向位置的值的。例如(*it)=5;

这样,我们就可以掌握基本的迭代器操作了:
假设有一个迭代器 it,我们一般都可以做这些操作:

 

it++ ++it

迭代器自增,也就是将游标向后移动一个位置

it-- --it

迭代器自减,也就是将游标向前移动一个位置

it1 == it2

比较两个迭代器是否相等,也就是说它们俩指向的位置是否是同一个位置

it1 != it2

同上,是否不等

it1 = it2

it2赋值给it1,也就是让it1指向it2的位置。

*it

取迭代器指向的元素。

it->XXX

调用it指向的元素的某成员(如果有的话)。


一切都和C/C++语言中的指针那么相似,真是太好用了,嘿嘿。

不过不要高兴的太早,有时候有些迭代器是没有这里面的一些功能的(后文会讲)。
不过对于STL中的所有标准容器(vectorlistdequemapset……),它们的迭代器全都支持上面的操作,尽情地去用吧。


容器的迭代器

到此,你已经大致了解了迭代器的由来、迭代器的意义,迭代器的基本操作。
那么想要更好的配合STL中的容器使用,还是来看一个列表。
STL
中的每个容器都遵循一些Concept,也就是说是一种概念约定,容器必须带有一些东西,以配合迭代器这个概念来使用。
上文已经说到了一些,这次用一个列表明确的列出来:

 

value_type

类型,表示这个容器保存的元素的类型

iterator

类型,表示这个容器适用的迭代器

const_iterator

类型,表示这个容器适用的常量迭代器

begin()

成员函数,表示这个容器的起始位置(第一个元素的位置)

end()

成员函数,表示这个容器的结束位置(最后一个元素的后面的那个位置)



什么是常量迭代器?
既然迭代器和指针很像,你肯定会想到有像const char* p这样的一种只读指针,指针本身指向的位置可以变换,但是你却不能通过它来改动它指向的地方的数据。
常量迭代器就是这么一种只读迭代器。可以移动它,但是不能改变它指向的元素,很简单吧。
大家都知道从char *转化成const char*无缝的,也就是说不需要强制类型转换,可以直接把char *当作const char*来用,但是反之则不行。常量迭代器也是一样的,因此你可以将一个普通迭代器赋值给常量迭代器,但是反之则不能。
因此:

C++ 代码:

vector<int>::const_iterator it=v.begin(); //ok
vector<int>::iterator it2=it; //Error:不能把常量迭代器赋值给普通迭代器



最后再说下这个value_type类型。正如上面表格中所说,它代表的是这个容器中元素的类型:
vector<int>::value_type
等价于int
vector<double*>::value_type
等价于double*
list<string>::value_type
等价于string
而且不仅是容器有这个类型,一些迭代器也有:
list<int>::iterator::value_type
等价于int (但请注意并不是所有迭代器都有,而且即使同一种迭代器在不同版本的STL中也可能不同)

如果我想定义一个容器中的元素类型的变量,就可以使用value_type来定义了。

C++ 代码:

vector<int> v;
vector<int>::value_type val=5; //这里的val就是int类型的变量
v.push_back(val);


你可能会疑惑,这不是写起来更麻烦吗?明明简单的int,非得搞成XXX<int>::value_type这副模样,有啥用啊?
呵呵,在这里看起来的确没什么用,但是当你去写一个通用(泛型)算法的时候就会非常有用了:

C++ 代码:

void func(T container)
{
  T::value_type val;
//在这里你可根本不知道T到底是vector<int>还是list<string>啊,呵呵
  //...
}




你是不是马上想起之前写的那个寻找最小元素的算法了?
我们之前写的那个针对容器的版本:

C++ 代码:

template<typename T,typename Cont>
T findMin(Cont& v)
{
  T tmp;
 
for(Cont::iterator it=v.begin();it!=v.end();++it)
  {
   
if(tmp>(*it)){tmp=(*it);}
  }
 
return tmp;
}


为了定义一个临时变量,不得不在模版中多加入一个类型T,使得使用起来非常不方便:
findMin<int, vector<int> >(v);
太麻烦了!我的容器类型都告诉你是vector<int>了,容器里面的元素类型肯定就是int了嘛!还非要我再告诉你一遍吗?不会自己去容器里面查一下啊?
于是我们现在就可以这样改进:

C++ 代码:

//如果你编译这段代码出现错误,请看本文最后附注
template<typename Cont>
Cont::value_type findMin(Cont& v)
{
  Cont::value_type tmp;
//here,我们现在终于可以丢掉那个该死的T
  for(Cont::iterator it=v.begin();it!=v.end();++it)
  {
   
if(tmp>(*it)){tmp=(*it);}
  }
 
return tmp;
}


不错吧,这样你使用这个函数的时候就可以直接调用:findMin(v);函数就可以自动知道容器的类型、元素的类型,而不必像以前一样臃肿的写findMin<int,vector<int> >(v);了。
(啥?为啥连vector<int>也省了?忘了C++强大的类型机制可以帮我们推导出函数参数的类型吗?)

你或许一拍脑袋,说既然如此那个迭代器版本的findMin也可以这么改了:

C++ 代码:

template<typename Iterator>
Iterator::value_type findMin(Iterator begin,Iterator end)
{
  Iterator::value_type tmp; 
// !! may not work!
  for(Iterator it=begin;it!=end;++it)
  {
   
if(tmp>(*it)){tmp=(*it);}
  }
 
return tmp;
}


哦不,不行。你头脑太热了,还是赶快去水龙头把脑袋冲5分钟吧,冷静下来是不是想起刚才说过的:只有一些迭代器才有value_type成员类型,并不是所有的迭代器都有,因此这么写可能导致编译错误哦。
但是要想得到迭代器指向的元素的类型,办法还是有的,不然STL里面那么多需要迭代器的算法是怎么搞出来的?嘿嘿,有一个巧妙的技巧,不过这里暂且不谈~~(卖什么关子,砸~



好啦,这次就说这么多,作为新人迭代器的基础入门介绍呵呵,如果觉得写的好请顶一下,如果觉得写的搓也请拍一下啊。

(当然写的搓极了,这么简单的东西废话了这么多才讲完……@#%&


下次(如果有下次的话),我们可以一起来聊一聊稍微进阶一点的内容啦。(砖~~

-----------------------------------------------------------------

附注:

可能有的伙计编译上面的一些代码出现错误。
如果排除你的头文件包含问题的话,可能是因为编译器对类型的识别错误导致的:

比如下面这句:
for( Cont::iterator it=v.begin();it!=v.end();++it)
在有些编译器中,由于编译器无法正确判别Cont::iterator到底是一个类型还是Cont的一个静态成员,可能就会导致错误:如果Cont::iterator被编译器误判为静态成员,那么当然这就不符合定义的语法了,会报编译错误。这时候可以通过加入typename关键字来强制指明这是一个类型来解决:
for(typename Cont::iterator it=v.begin();it!=v.end();++it)
当编译器看到那个typenme的时候,就知道Cont::iterator在这里应被看做类型而不是静态成员,于是就不会再出现上面的问题了。

如果还发现其它问题,欢迎你留言告诉我~

 

 

----------------------------------------------------------------------------

附注的附注:这篇文章是我很久之前写在自己私人blog上的。因为有朋友建议,于是现在拿到csdn上晒一晒。(现在看来觉得写的很挫,不过懒得做修改了)希望大家不要BS啊。。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值