1. 迭代器是什么?
我们先从一个例子说起:
int a[10], *p;
for (p = &a[0]; p !=&a[10], ++p) std::cout << *p << std::endl;
在上述for语句构成的迭代操作中,指针p首先指向了数组a的首元素,然后通过++运算和!=运算来完成迭代。这里,指针p“知道”它指向了一片连续的存储区,所以++运算就有意义了。
这里,指针p可以视为一个最为简单的迭代器。程序员可以通过该“迭代器”方便灵活地访问容器(即a数组)。
当然,这里所说的容器(a数组)过于简单。对于更复杂的情况,迭代器被定义为一个某种容器类的伴随类,一般定义在容器类的内部,具有public属性,是其包围容器类的友元。它可以在容器类的外部使用。
在多数情况下,迭代器内部有一个内部普通指针,它可以指向容器类的内部存储机构。而迭代器类本身模拟了一个指针的行为,为此它重载了用于指针操作的三个关键运算符:
l !=
l ++
l *
为了使用迭代器,其包围容器类提供了两个public成员:
l begin(),它返回一个迭代器对象,并使该对象的内部指针指向了容器对象的存储结构的首元素;
l end(),它返回一个“空”迭代器对象,该对象的内部指针指往往是个nullptr。而在一般情况下,容器对象的内部存储结构都用nullptr标识存储的结尾,所以可以说,空迭代器对象的内部指针指向了存储结构中最后一个元素的“后”一个元素。这个迭代器对象往往称为“哨兵”。注意:空迭代器仅仅参与了比较运算(及两个迭代器内部指针的比较运算),而没有引用该迭代器对象“指向的对象”,因此在使用上是安全的。
2. 为什么需要迭代器
对于内部结构相对复杂的容器,我们不能使用指针直接指向其内部存储结构去访问它,因为这样直接破坏了数据封装的原则。
因此,一个折中的方法是为容器类设计一个遍历成员traverse(),它带有一个谓词函数作为参数去访问每一个容器元素。
然而,这虽然实现了数据封装,却是遍历这类的迭代操作变得很不灵活,因为程序员可能不得不编写多种谓词函数去处理不同的情况。
迭代器的引入可以彻底解决上述种种不便。迭代器模拟了一个指针的行为,配合容器提供的begin()和end(),程序员可以像使用指针那样去使用迭代器,
而不需要直到容器的内部结构,也不需要编写太多的谓词函数,从而起到两全其美的效果。
3. 如何定义迭代器
例如,对于教材上使用链式存储的容器类List,其迭代器的定义如下:
class list
{
public:
typedef int _elemType; //元素的类型
private:
struct _NODE_ //链表的节点。这个用于存储的类型定义在private段中,因为类的外部无法获悉其存储结构的详情
{
_elemType data;
_NODE_* next;
} _head;
typedef _NODE_* _NODEPTR_;
public:
typedef _NODEPTR_ _range; //_range是_NODE_*的别名
//省略了其它容器成员
friend class iterator; //因为该迭代器类要访问List类的私有类型_NODE和其内部数据
class iterator
{
private:
_range _p; //迭代器的内部指针
public:
iterator(range = nullptr) : _p(range){}
iterator& operator++()
{
_p = _p->next; //内部指针指向下一个节点。根据容器类存储结构的不同,该语句也不尽相同
return *this;
}
bool operator!=(const iterator& i)
{
return _p != i._p; //两个指针的比较,判断两个指针是否指向了同一个节点
}
_elemType operator*()
{
return _p->data;
}
iterator& operator=(constiterator& i)
{
_p = i._p;
return *this;
}
};
iterator begin() const
{
return iterator(_head); //返回的迭代器对象(的内部指针)指向了容器存储结构的首元素
}
iterator end() const
{
return iterator(); //返回的迭代器对象(的内部指针)是nullptr
}
};
在使用迭代器时,就像使用指针那样方便:
List l{1, 2,3, 4, 5};
List::iteratoritr; //iterator是List类的内部类型,所以必须说明成这样。注意:iterator类定义在public段中
for (itr =l.begin(); itr != l.end(); ++itr) std::cout << *itr << std::endl;
如果使用C++ 11标准,上述for语句还要更为简单:
for (auto e :l) std::cout << e << std::endl;
这条语句等价于:
for(List::iterator itr = l.begin(); itr != l.end(); ++itr)
{
_List::elemType e = *itr;
std::cout << e << std::endl;
}
4. 伪迭代器
如果容器类的内部存储机构是类似于数组的、连续存储的线性结构,那么迭代器可以不用那么复杂:它可以不是一个类,而只是指针。
例如:
class Vect
{
public:
typedef int _elemType;
typedef _elemType* _range;
private:
_range comp; //容器的内部存储是一片用new分配的指针内存,其行为类似于数组
size_t len;
public:
typedef _range iterator; //由于知道容器是连续存储,因此不需要定义迭代器类,而只是为指针去一个迭代器别名
iterator begin() { return comp; } //返回的“迭代器”指向数组的首元素
iterator end() { return comp + len; }//comp + len - 1指向了数组的尾元素,而comp + len指向了改尾元素的“后”一个元素
//这里,编译器知道!=、++和*运算符作用于指针之上是什么含义,因此不需要重载这些运算符
};
这种伪迭代器的使用与真正迭代器的使用没有区别。
5. 迭代器的种类
实际上,除了上述讲到的迭代器之外,容器类还有常量迭代器、反向迭代器等等,用于不同的场合。
更多的细节请查阅相关资料。