C++ STL 基础及应用(3) 迭代器
程序员文章站
2022-04-25 19:24:38
迭代器(iterator)是 stl 的核心技术,提供了统一访问容器元素的方法,为编写通用算法提供了坚实的技术基础。
本章将带你编写一个自带迭代器的数组类和一个自带迭代器的链表类,模拟 stl 中的...
迭代器(iterator)是 stl 的核心技术,提供了统一访问容器元素的方法,为编写通用算法提供了坚实的技术基础。
本章将带你编写一个自带迭代器的数组类和一个自带迭代器的链表类,模拟 stl 中的容器,这两个实例能够很清晰地展示 stl 的迭代器思想。并探讨迭代器类应该作为容器类的内部类的原因,然后对 stl 迭代器做一下归纳理解,最后阐述一下 stl 中真正的迭代器概况。那么什么是迭代器呢?
迭代器即指针,可以是需要的任意类型,它的最大好处是可以使容器和算法分离。例如,有两个容器类,myarray 是某类数组集合;mylink 是某类型链表集合。它们都有显示、查询和排序等功能,常规思维是每个容器都有自己的显示、查询和排序等函数。但是细想,不同容器中完成相同功能代码的思路大致是相同的,那么能不能把它们抽象出来,多个容器仅对应一个显示、一个查询、一个排序函数呢?这是泛型思想发展的必然结果,于是迭代器思维就产生了。下面将通过实例来加深对迭代器的理解。注意!对于下面代码中的模板使用、数组类编写有困惑的童鞋可以先看上一篇博文。
接下来编写带有迭代器的数组类链表类,模拟 stl 中的容器,展示 stl 的迭代器思维。
假设有一个动态数组类:
(比上一章节的数组类仅多了 begin() 和 end() 方法)
//数组类 template class myarray { private: int m_ntotalsize; //数组总长度 int m_nvalidsize; //数组有效长度,即当前元素数 t * m_pdata; //数据指针 public: //构造函数 myarray(int nsize=2) //假设默认数组长度为2 { m_pdata=new t[nsize]; m_ntotalsize=nsize; m_nvalidsize=0; } //获取数组有效长度 int getvalidsize() { return m_nvalidsize; } //获取数组总长度 int gettotalsize() { return m_ntotalsize; } //返回某一位置元素 t get(int pos) { return m_pdata[pos]; } //添加一个元素 void add(t value) { if(m_nvalidsize)>然后有一个链表类:
//链表结点数据结构 template struct unit { t value; unit * next; }; //链表类 template class mylink { private: unit * head; //链表头 unit * tail; //链表尾 unit * prev; //指向最后一个结点 public: //构造函数 mylink() { head=tail=prev=null; } //添加一个结点 void add(const t &value) { unit *u=new unit(); u->value=value; u->next=null; if(head==null) { head=u; prev=u; } else { prev->next=u; prev=u; } tail=u->next; } //返回头指针 unit* begin() { return head; } //返回尾指针 unit* end() { return tail; } //析构函数 virtual ~mylink() { unit *prev=head; unit *next=null; while(prev!=tail) { next=prev->next; delete prev; prev=next; } } };可以看出,myarray 是模板元素 t 的数组类,mylink 是模板元素 t 的链表类,以 struct unit 为一个链表单元。那么如何以 myarray 和 mylink 为基础,完成一个共同的显示函数呢?首先从需求出发,逆向考虑,
先写一个泛型显示函数,如下所示:
//泛型显示函数 template void display(t start,t end) { cout<该函数的模板参数即为迭代器类型,该泛型函数能够输出从迭代器 start 至 end 之间的所有元素,指针支持 ++ 和 * 操作。display() 函数与具体的容器没有直接关联,但是间接关联是必需的,对于 myarray 来说,该参数相当于数组类中的 t* 操作,对于 mylink 来说,该参数相当于链表类当中的 unit* 操作,所以该参数就相当于容器中元素的指针。于是写出 myarray 的迭代器 arrayiterator 类为:
template class arrayiterator { private: t * t; public: //构造函数 arrayiterator(t * t) { this->t=t; } //重载!= bool operator != (const arrayiterator &it) { return this->t!=it.t; } //重载++ void operator ++ (int) { t++; } //重载取值符* t operator *() { return *t; } };可以看出,arrayiterator 类是对 t* 的再封装,必须重载 operator !=、++、* 操作符,这是由于 display 泛型显示函数需要用到这些操作符。同样地,增加链表类迭代器 linkiterator 类,重载 operator !=、++、* 操作符:
template class linkiterator { private: t *t; public: //构造函数 linkiterator(t *t) { this->t=t; } //重载!= bool operator !=(const linkiterator &it) { return this->t!=it.t; } //重载++ void operator ++(int) { t=t->next; } //重载取值符* t operator *() { return *t; } };可以看出 operator !=、operator * 的重载内容与 arrayiterator 一致,只有 operator ++ 不同,因为这里链表并不像数组一样连续存储,它是指针的转向,所有不能是 t++ ,而应该是 t=t->next。
细心的同学会发现,若有一个链表类 mylink,当使用链表类迭代器 linkiterator start 为参数使用 display() 函数时,假设有一步要执行 cout<<*start 时,根据 linkiterator 对 operator * 的重载,该式等于 cout<,但是 unit 是一个复合类型,cout 显然不能输出,cout 只能输出 unit .value,因此需要重载全局函数 operator << ,代码如下: //重载全局函数operator << template ostream& operator<<(ostream& os,const unit &u) { os<最后给出测试函数如下:int main() { myarray ary; mylink link; ary.add(1.1); ary.add(2.2); ary.add(3.3); link.add(1); link.add(2); link.add(3); arrayiterator arystart(ary.begin()); arrayiterator aryend(ary.end()); //注意!!这里两个尖括号要分开 linkiterator > linkstart(link.begin()); linkiterator > linkend(link.end()); display(arystart,aryend); display(linkstart,linkend); return 0; }过程可以描述为:定义一个容器对象,向其中添加数据,获得迭代器对象 start 、end,最后调用泛型显式函数 display 完成数据的输出。这里给出上述完整的代码:
#include using namespace std; //数组类 template class myarray { private: int m_ntotalsize; //数组总长度 int m_nvalidsize; //数组有效长度,即当前元素数 t * m_pdata; //数据指针 public: //构造函数 myarray(int nsize=2) //假设默认数组长度为2 { m_pdata=new t[nsize]; m_ntotalsize=nsize; m_nvalidsize=0; } //获取数组有效长度 int getvalidsize() { return m_nvalidsize; } //获取数组总长度 int gettotalsize() { return m_ntotalsize; } //返回某一位置元素 t get(int pos) { return m_pdata[pos]; } //添加一个元素 void add(t value) { if(m_nvalidsize struct unit { t value; unit * next; }; //链表类 template class mylink { private: unit * head; //链表头 unit * tail; //链表尾 unit * prev; //指向最后一个结点 public: //构造函数 mylink() { head=tail=prev=null; } //添加一个结点 void add(const t &value) { unit *u=new unit(); u->value=value; u->next=null; if(head==null) { head=u; prev=u; } else { prev->next=u; prev=u; } tail=u->next; } //返回头指针 unit* begin() { return head; } //返回尾指针 unit* end() { return tail; } //析构函数 virtual ~mylink() { unit *prev=head; unit *next=null; while(prev!=tail) { next=prev->next; delete prev; prev=next; } } }; //泛型显示函数 template void display(t start,t end) { cout< class arrayiterator { private: t * t; public: //构造函数 arrayiterator(t * t) { this->t=t; } //重载!= bool operator != (const arrayiterator &it) { return this->t!=it.t; } //重载++ void operator ++ (int) { t++; } //重载取值符* t operator *() { return *t; } }; template class linkiterator { private: t *t; public: //构造函数 linkiterator(t *t) { this->t=t; } //重载!= bool operator !=(const linkiterator &it) { return this->t!=it.t; } //重载++ void operator ++(int) { t=t->next; } //重载取值符* t operator *() { return *t; } }; //重载全局函数operator << template ostream& operator<<(ostream& os,const unit &u) { os< ary; mylink link; ary.add(1.1); ary.add(2.2); ary.add(3.3); link.add(1); link.add(2); link.add(3); arrayiterator arystart(ary.begin()); arrayiterator aryend(ary.end()); //注意!!这里两个尖括号要分开 linkiterator > linkstart(link.begin()); linkiterator > linkend(link.end()); display(arystart,aryend); display(linkstart,linkend); return 0; }关于迭代器类位置的探讨
每一种容器对应一种迭代器,如数组迭代器只适用于数组,那么基于封装的思想,迭代器类自然就必须位于它所对应容器类的内部,即成为容器类的内部类。将数组迭代器 arrayiterator 放入数组类 myarray 中:
//数组类 template class myarray { private: int m_ntotalsize; //数组总长度 int m_nvalidsize; //数组有效长度,即当前元素数 t * m_pdata; //数据指针 public: //构造函数 myarray(int nsize=2) //假设默认数组长度为2 { m_pdata=new t[nsize]; m_ntotalsize=nsize; m_nvalidsize=0; } //数组迭代器 class arrayiterator { private: t * t; public: //构造函数 arrayiterator(t * t) { this->t=t; } //重载!= bool operator != (const arrayiterator &it) { return this->t!=it.t; } //重载++ void operator ++ (int) { t++; } //重载取值符* t operator *() { return *t; } }; //获取数组有效长度 int getvalidsize() { return m_nvalidsize; } //获取数组总长度 int gettotalsize() { return m_ntotalsize; } //返回某一位置元素 t get(int pos) { return m_pdata[pos]; } //添加一个元素 void add(t value) { if(m_nvalidsize注意!与原代码相比少了很多尖括号,并且由于模板编程的规则有好几处稍许变化的地方,希望读者能留心发现。然后将链表迭代器 linkiterator 放入链表类 mylink 中:;>//链表类 template class mylink { public: //链表结点数据结构 struct unit { t value; unit * next; }; //链表类迭代器 class linkiterator { private: unit *u; public: //构造函数 linkiterator(unit *u) { this->u=u; } //重载!= bool operator !=(const linkiterator &it) { return this->u!=it.u; } //重载++ void operator ++(int) { u=u->next; } //重载取值符* unit operator *() { return *this->u; } }; //重载全局函数operator << friend ostream& operator<<(ostream& os,typename mylink::unit& u) { os<value=value; u->next=null; if(head==null) { head=u; prev=u; } else { prev->next=u; prev=u; } tail=u->next; } //返回头指针 unit * begin() { return head; } //返回尾指针 unit * end() { return tail; } //析构函数 virtual ~mylink() { if(head!=null) { unit *prev=head; unit *next=null; while(prev!=tail) { next=prev->next; delete prev; prev=next; } } } private: unit * head; //链表头 unit * tail; //链表尾 unit * prev; //指向最后一个结点 };由于该 struct unit 结构体为该链表类专用,因此也一并放进了 myarray 类中。值得注意的是,全局函数 cout<< 的重载也被放了进来,因为只有用到这个链表类时,才会有 cout<的必要,因此要一并封装。另外,为了能使 ostream 对象访问该类的私有变量,必须将这个重载函数用 friend 关键字写为友元函数。 )>display()函数不用变化,测试函数相应地修改为:
int main() { myarray ary; mylink link; ary.add(1.1f); ary.add(2.2f); ary.add(3.3f); link.add(1); link.add(2); link.add(3); myarray::arrayiterator arystart(ary.begin()); myarray::arrayiterator aryend(ary.end()); mylink::linkiterator linkstart(link.begin()); mylink::linkiterator linkend(link.end()); display(arystart,aryend); display(linkstart,linkend); return 0; }最后给上封装之后的完成代码,一共只有两个类 myarray 与 mylink,迭代器被封装在了类中,然后有一个公共的显示函数 display() 和一个测试函数 main(),代码如下:
#include using namespace std; //数组类 template class myarray { private: int m_ntotalsize; //数组总长度 int m_nvalidsize; //数组有效长度,即当前元素数 t * m_pdata; //数据指针 public: //构造函数 myarray(int nsize=2) //假设默认数组长度为2 { m_pdata=new t[nsize]; m_ntotalsize=nsize; m_nvalidsize=0; } //数组迭代器 class arrayiterator { private: t * t; public: //构造函数 arrayiterator(t * t) { this->t=t; } //重载!= bool operator != (const arrayiterator &it) { return this->t!=it.t; } //重载++ void operator ++ (int) { t++; } //重载取值符* t operator *() { return *t; } }; //获取数组有效长度 int getvalidsize() { return m_nvalidsize; } //获取数组总长度 int gettotalsize() { return m_ntotalsize; } //返回某一位置元素 t get(int pos) { return m_pdata[pos]; } //添加一个元素 void add(t value) { if(m_nvalidsize class mylink { public: //链表结点数据结构 struct unit { t value; unit * next; }; //链表类迭代器 class linkiterator { private: unit *u; public: //构造函数 linkiterator(unit *u) { this->u=u; } //重载!= bool operator !=(const linkiterator &it) { return this->u!=it.u; } //重载++ void operator ++(int) { u=u->next; } //重载取值符* unit operator *() { return *this->u; } }; //重载全局函数operator << friend ostream& operator<<(ostream& os,typename mylink::unit& u) { os<value=value; u->next=null; if(head==null) { head=u; prev=u; } else { prev->next=u; prev=u; } tail=u->next; } //返回头指针 unit * begin() { return head; } //返回尾指针 unit * end() { return tail; } //析构函数 virtual ~mylink() { if(head!=null) { unit *prev=head; unit *next=null; while(prev!=tail) { next=prev->next; delete prev; prev=next; } } } private: unit * head; //链表头 unit * tail; //链表尾 unit * prev; //指向最后一个结点 }; //泛型显示函数 template void display(t start,t end) { cout< ary; mylink link; ary.add(1.1f); ary.add(2.2f); ary.add(3.3f); link.add(1); link.add(2); link.add(3); myarray::arrayiterator arystart(ary.begin()); myarray::arrayiterator aryend(ary.end()); mylink::linkiterator linkstart(link.begin()); mylink::linkiterator linkend(link.end()); display(arystart,aryend); display(linkstart,linkend); return 0; }注意!上述代码我在 vs 2010 下编译正确,运行正确,但在 codeblocks 用 gnu gcc 编译器编译时报错,难道是 gcc 编译器的bug?希望有知道的大神赐教。
恭喜!你已经能够编写一个带有迭代器的简易容器了,stl 中的容器也是以此为雏形完善代码而来的,现在你应该对于 stl 的迭代器概念有一个整体的认识了,接下来总结一下关于迭代器的思想。进一步理解迭代器
前面已经详细讨论了迭代器的编程思路,如果用图形来描述一下就是:
图一:容器、迭代器、算法示意图
每一个容器都有对应的迭代器,容器通过迭代器来共享某一具体算法,某一具体算法不依附于某一个具体的容器。这里迭代器起到一个中间媒介的作用,通过它把容器与算法联系起来。换一句话说就是,迭代器是编写通用泛型算法发展的必然结果,算法通过迭代器依次访问容器中的元素。由上图也可以得出 stl 标准模板库编程的基本步骤。stl 标准模板库编程的基本步骤:
1.形成容器元素
2.获得需要的迭代指针
3.调用通用算法
其实,生活中有很多“迭代器”的现象,如千家万户通过电话线来上网获取资源,这里电话线就类似于一个迭代器,每一个家庭就相当于容器的一个结点。
stl 迭代器
现在来看看 stl 中真正的迭代器是怎么样的。按照功能划分,迭代器共分为5大类:
(1)输入迭代器(input iterator)
(2)输出迭代器(output iterator)
(3)前向迭代器(forword iterator)
(4)双向迭代器(bidirectional iterator)
(5)随机访问迭代器(random access iterator)(1)输入迭代器(input iterator)
按顺序只读一次。完成的功能有:能进行构造和默认构造,能够被复制和被赋值,能进行相等性比较,能进行逐步向前移动,能进行读取值。输入迭代器重载的主要操作符如下所示:
operator * 访问迭代元素值
operator == 迭代元素相等比较
operator ++() 前置迭代指针++
operator != 迭代元素不等比较
operator ++(int) 后置迭代指针++
stl 提供的主要输入迭代器是 istream_iterator,支持上表中的所有操作。值得注意的是它的构造函数,有两种形式:
istream_iterator(): 默认的构造函数,创建了一个流结束的迭代器。
istream_iterator(istream &): 参数是输入流。含义是从输入流中读取数据,当遇到流结束符时停止。
一个使用 istream_iterator 迭代器迭代标准输入流的实例如下所示:#include #include using namespace std; int main() { cout<<"请输入数据(如1 2 3,):"; istream_iterator start(cin); //建立键盘输入流 istream_iterator end; //建立输入流结束迭代器 while(1) { cout<<*start<程序执行结果为: 请输入数据(如1 2 3,):1 2 3, 1 2 3 程序分析如下所示: ①先从键盘(标准输入)输入相应数据,按 enter 键后,调用 istream_iterator 迭代器进行枚举数据,迭代器指针指向第一个符合要求的数据。 ②利用循环输出枚举数据,若迭代器指针已到达数据流尾部,则退出循环。那么如何判断到达数据流尾部呢?从程序中可以看出如果迭代指针 start 不停累加到等于 end 时即可。而 end 是调用无参数的 istream_iterator 构造函数获得的,指向输入流结束位置。 注意!本例中输入的是"1 2 3,",后面跟一个逗号,若无逗号则 istream_iterator start(cin) 则不会执行完,这是由于迭代类型时整型数,只有在流中输入非整型数,迭代器迭代到此位置时发现它不是整型数才停止工作。当然,这里最后的","可以用其他任意非整型数来替换。(2)输出迭代器(output iterator) 只写一次。完成的功能有:能进行构造或默认构造,能被复制或赋值,能进行相等性比较,能进行逐步向前移动,能进行写入值(*p=x),但不能读出。输出迭代器重载的主要操作符如下所示: operator * 分配迭代元素值空间 operator ++() 前置迭代指针++ operator = 写入元素值 operator ++(int) 后置迭代指针++ stl 提供的主要输出迭代器是 ostream_iterator,支持上表中的所有操作。它的构造函数也有两种形式: ostream_iterator(ostream& out): 创建了流输出迭代器,用来迭代 out 输出流。 ostream_iterator(ostream& out,const char *delim): 创建了流输出迭代器,用来向 out 输出流输出数据,输出的数据之间用 delim 字符串分隔。即每向 out 输出流输出一个数据后,就向 out 输出流输出一个分隔符 delim 。 一个使用 ostream_iterator 迭代器迭代标准输出流的实例如下所示:
#include #include using namespace std; int main() { ostream_iterator out(cout,"\t"); *out=1; out++; *out=2; out++; *out=3; out++; return 0; }程序执行结果为:
1 2 3
(3)前向迭代器(forword iterator)
使用输入迭代器和输入迭代器基本可以满足算法和容器的要求,但是还有一些算法需要同时具备两者的功能,例如 replace 算法就是一例。前向迭代器就是为满足这些需求而定义的。先看看 replace 算法(用新值代替旧值)的需求。template void replace(forwarditerator first,forwarditerator last,const t& old_value,const t& new_value) { for(;first!=last;first++) if(*first==old_value) *first=new_value; }这里的 first,就是 forwarditerator 的模型,其需求包括 first!=last、first++、*first==old_value、*first=new_value。这样,forward iterator 的需求概括为:能进行构造或默认构造,能被复制或赋值,能进行相等性比较,能进行逐步前向移动,能进行读取值(但不能进行改写)。前向迭代器包含了输入和输出迭代器两者的所有功能,加上还可以多次解析一个迭代器指定的位置,因此可以对一个值多次进行读写。顾名思义,前向迭代器只能向前移动。但是 stl 本事并没有专门为前向迭代器预定义的迭代器。(4)双向迭代器(bidirectional iterator)
具有前向迭代器的全部功能,另外它还可以利用自减操作符 operator -- 向后一次移动一个位置,例如双向链表容器中需要的就是双向迭代器。(5)随机访问迭代器(random access iterator)
具有双向访问迭代器的所有功能,再加上一个指针所有的功能,包括使用操作符 operator [] 进行索引,加某个整数值到一个指针就可以向前或向后移动若干个位置,或者使用比较运算符在迭代器之间进行比较。
综观5种迭代器,发现从前到后的需求越来越多,这就是所谓的细化。因为如此,在一切情况下都可以使用需求最细的 random access iterator,确实可以,但是不好,因为过多的需求自然会降低它的效率,实际编程时应该选择正好适合的 iterator 来得到最高的效率。;>
;>