STL第三章迭代器收货

一、为什么需要traits

首先看一下如下例子:

template<class T>
class test
{
public:
	static T a;
};

template<class M>
void show()
{
	cout << typeid(M::a).name() << endl;
}

int main()
{
	show<test<int>>();
	show<test<string>>();
	show<test<short>>();
	system("pause");
}

结果如下:
在这里插入图片描述
在show函数中想要得到得到模板M的模板参数类型,可以使用这样的方法,但是这种方法并不能够定义一个同样的变量。那如何才能实现定义同样的变量呢?那就是使用traits机制。利用traits机制的改版如下:

template<class T>
class test
{
public:
	typedef T value_type;//在所有show函数可能用到的模板中,都加上这个定义
	static T a;
};

template<class M>
void show()
{
	M::value_type a;//这样,show就能根据模板的不同,甚至同一个模板的不同定义,而定义不同的变量。
	cout << typeid(a).name() << endl;
}

int main()
{
	//同上
	...
}

结果如下
在这里插入图片描述
其实traits就是为了解决模板函数中如何利用模板参数本身的一些信息,比如上面的例子中,就想利用模板参数M的模板参数T来定义变量。
而这种情境在泛化编程中经常遇到。

二、迭代器的作用

在书中,有这样一句话:STL的中心思想在于——将数据容器和算法分开,彼此独立设计,最后再以一帖胶着剂将他们撮合在一起。
其中数据容器,个人理解是数据结构,也就是比如线性的数组,链表等,关联式的树,堆等。可以使用模板类来设计。
算法则可以使用模板函数来设计。
==问题是=:如何让不止一个数据结构都适用同一个算法的模板函数呢。
举例来说:我设计了一个查找算法模板函数,如下:

template<class T>
? find(?, ?){...}

输入参数和返回参数我使用?来表示,因为按逻辑输入参数应该是两个,就是查找的范围,而范围用一头一尾可以表示。但是这个头和尾我该如何去表达呢?比如数组和双向链表的头和尾可以用指针来表示。但是哈希表也可以用指针来表示嘛?明显不行的,如图所示:
在这里插入图片描述
那既然无法通过头尾指针找到范围内的所有元素,又如何去执行find函数呢。
所以似乎不同的数据结构,针对同一个算法的模板函数,入口参数无法统一,如果不能统一,那么数据容器和算法就无法分开,因为需要针对每一个数据结构,都要特制一种算法。
而在STL中find函数的入口参数就是迭代器,迭代器就是数据容器和算法的粘合剂,能够统一算法的入口参数。
所以只要每一个数据容器都根据标准自定义迭代器,那么STL提供的算法的模板函数就可以使用,因为模板函数都是使用迭代器作为输入参数以及返回参数的。

三、迭代器与traits

迭代器和traits结合是非常紧密的,或者我源码看到现在为止,所有的traits都是在迭代器中呈现的(不保证traits只在迭代器中使用到
为何迭代器中使用如此频繁,因为首先迭代器自身就是一个模板类,而迭代器作为模板函数入口参数以及模板参数,完美符合了我在第一点中提到的使用情境,自然使用较多。模板函数中可以通过traits机制,萃取出许多迭代器的特性,并加以利用。

四、迭代器中traits的具体体现

迭代器的相应型别:迭代器的相应型别就是模板函数通过traits机制,可以获得的信息类型。比如在第一点中,通过萃取机制,可以获得函数模板的模板入口参数T。
而在stl迭代器中提供五种相应型别,源码如下:

template <class _Category, class _Tp, class _Distance = ptrdiff_t,
          class _Pointer = _Tp*, class _Reference = _Tp&>
struct iterator {
  typedef _Category  iterator_category;//模板函数可以萃取iterator_category知道这个迭代器是什么类型的,迭代器类型见第五点
  typedef _Tp        value_type;//迭代器或者说迭代器的模板参数,也就是迭代器所指对象的类型
  typedef _Distance  difference_type;//表示两个迭代器之间距离的类型,
  //比如说有的相邻两个类型之间相差(unsigned int)1,而有的相邻两个类型之间相差(int)1,difference_type就是存储距离类型是unsigned int还是int。
  //不过一般都是使用的ptrdiff_t类型,就是__int64类型
  typedef _Pointer   pointer;//一般都是取value_type*,就是迭代器指向对象的类型的指针
  typedef _Reference reference;//一般都是取value_type&,就是迭代器指向对象的类型的引用
};

这五种迭代器型别是最常用的,如果你想要自定义数据结构,那么在自定义迭代器时,一定要加入这五种型别,才可以被模板函数使用。
注意:以上五种型别都可以用来直接定义变量的

专门用来萃取的类

template <class _Iterator>
struct iterator_traits {
  typedef typename _Iterator::iterator_category iterator_category;//迭代器种类
  typedef typename _Iterator::value_type        value_type;
  typedef typename _Iterator::difference_type   difference_type;
  typedef typename _Iterator::pointer           pointer;
  typedef typename _Iterator::reference         reference;
};

在模板函数中,stl又加入了一层萃取封装,使用方法以count源码举例说明:

template <class _InputIter, class _Tp>
typename iterator_traits<_InputIter>::difference_type
count(_InputIter __first, _InputIter __last, const _Tp& __value) {
  typename iterator_traits<_InputIter>::difference_type __n = 0;//特别注意!!!
  for ( ; __first != __last; ++__first)
    if (*__first == __value)
      ++__n;
  return __n;
}

模板函数count想要调用迭代器模板_InputIter中的距离类型来定义一个迭代器模板_InputIter适配的距离变量。
其实这里使用

typename iterator_traits<_InputIter>::difference_type __n = 0;
//等价于
typename _InputIter::difference_type __n = 0;

问题一:为什么需要进行一层额外封装?
为了处理这样的情况,如果对于count函数,我输入的模板参数不是迭代器,而是一个普通类型的指针,那该怎么办?比如说我有一个int类型的数组,头尾节点都是int*类型的指针,如下的语句:

typename iterator_traits<int *>::difference_type __n = 0;

如果iterator_traits只有如上的一种定义的话,这显然是不对的,因为int *是没有自己定义difference_type类型的。所以这种情况下,需要对iterator_traits进行偏特化,为了让普通类型指针也能够适用这个函数。源码如下:

template <class _Tp>
struct iterator_traits<_Tp*> {
  typedef random_access_iterator_tag iterator_category;
  typedef _Tp                         value_type;
  typedef ptrdiff_t                   difference_type;
  typedef _Tp*                        pointer;
  typedef _Tp&                        reference;
};

template <class _Tp>
struct iterator_traits<const _Tp*> {
  typedef random_access_iterator_tag iterator_category;
  typedef _Tp                         value_type;
  typedef ptrdiff_t                   difference_type;
  typedef const _Tp*                  pointer;
  typedef const _Tp&                  reference;
};

如果有指针作为模板参数,那么五个相应型别就不在是从迭代器中去得到,而是直接根据指针来得到。
问题2:为什么只有指针的偏特化,如果是普通类型变量怎么办?
普通类型变量就不可以使用find函数,所有的模板函数中使用迭代器的地方,就不可能使用普通类型变量,最多只能使用普通类型指针。因为迭代器就是起到了指针的作用。相当于一个函数需要指针作为入口参数,你输入一个变量,那显然是不行的。
有了上面两种偏特化定义,下面的程序就对了

typename iterator_traits<int *>::difference_type __n = 0;

五、迭代器型别iterator_category

iterator_category是代表的迭代器指针的类型,迭代器指针类型有五种,如下所示:

  • input_iterator:只读迭代器,可以读取迭代器指向的对象
  • output_iterator:只写迭代器,向迭代器指向对象写入数值,但是不可以读取
  • forward_iterator:前向迭代器,可双向移动
  • bidirectional_iterator:双向迭代器,并对迭代器指向的对象进行读取
  • random_access_iterator:随机访问迭代器,可以直接跳到容器的任何一个元素处,对其进行读写操作。

后三种使用较多,前三种没有看到stl中有容器使用
问题一:那如何来实现这些区别的呢,其实就是不同类型的迭代器,重载了不同的运算符。
如下图所示:
在这里插入图片描述
注意:->运算符并不是那种迭代器的特例,如果需要可以自定义重载,如果不需要,就不用重载,上图所示的是属于这种迭代器必须重载的运算符。

问题二:那程序如何自动区分这些类别呢,或者说如果萃取iterator_category,得到的是什么呢?
其实得到的是如下五种空类:

struct input_iterator_tag {};
struct output_iterator_tag {};
struct forward_iterator_tag : public input_iterator_tag {};
struct bidirectional_iterator_tag : public forward_iterator_tag {};
struct random_access_iterator_tag : public bidirectional_iterator_tag {};

五种类型分别对应了一种空类,至于为什么使用继承关系,详见书98页。
其中forward_iterator_tag没有继承output_iterator_tag,网上有人解答说output_iterator不完全匹配forward_iterator,具体原因不详。
不过这其实用处不大,很少会用到继承关系的,因为使用符合自己类别的效率最高。
在这里插入图片描述
而这五种空类需要自己根据自己迭代器的特性来判断属于那种,并定义那种空类。

六、迭代器最重要的两点

上面所讲的迭代器和traits方面,都是为了迭代器和算法函数能够无缝对接,但是作为数据容器和算法的粘合剂,如何与数据容器进行无缝对接呢?
这一点很简单,就是在迭代器中,加入数据容器相关的成员变量。

所以看一个迭代器最重要的就是两点

  • 迭代器中的相应型别,其中最主要关注的就是这个迭代器是什么类型的,重载了哪些运算符
  • 迭代器中的成员变量,这些成员变量都是和数据容器相关的。

七、以一个例子来讲解一下迭代器使用的全过程

advance函数就是将__i++循环n次

template <class _InputIterator, class _Distance>
inline void advance(_InputIterator& __i, _Distance __n) {
  __advance(__i, __n, iterator_category(__i));
}
//__advance有三种重载形式
//针对只读迭代器的
template <class _InputIter, class _Distance>
inline void __advance(_InputIter& __i, _Distance __n, input_iterator_tag) {
  while (__n--) ++__i;//由于input_iterator只有++这种运算符,所以只能接收n为正值
}

//针对双向迭代器的
template <class _BidirectionalIterator, class _Distance>
inline void __advance(_BidirectionalIterator& __i, _Distance __n, 
                      bidirectional_iterator_tag) {
  if (__n >= 0)//bidirectional_iterator重载了++和--,所以可以接收负值
    while (__n--) ++__i;
  else
    while (__n++) --__i;
}

//针对随机存储迭代器的
template <class _RandomAccessIterator, class _Distance>
inline void __advance(_RandomAccessIterator& __i, _Distance __n, 
                      random_access_iterator_tag) {
  __i += __n;//由于random_access_iterator可以任意跳转,所以更快
}

可以看到,如果将random_access_iterator迭代器使用input_iterator迭代器的函数,那么效率会非常低
另外,判断类别的函数iterator_category()源码如下:

iterator_category(const _Iter& __i) { return __iterator_category(__i); }

__iterator_category(const _Iter&)//返回模板参数_Iter对应的迭代器种类
{
  typedef typename iterator_traits<_Iter>::iterator_category _Category;
  return _Category();//定义了一个iterator_traits<_Iter>::iterator_category空变量,但是足够让重载函数识别了。
}

八、STL迭代器的遍历

容器迭代器类型成员变量备注
vectorRandom Access Iterator指向该数组某一位置的普通指针在linux版本的STL中不仅仅是普通指针
listBidirectional Iterator指向链表中某一节点的指针
dequeRandom Access Iterator共有四个参数:①指向map中控数组的指针②该迭代器指向的元素③该迭代器指向元素所在缓冲区的首地址④该迭代器指向元素所在缓冲区的尾地址
slistForward Iterator指向链表中的某一节点的指针
RB-treeBidirectional Iterator指向红黑树中某一节点的指针这个迭代器也是map,set,multimap,multiset的迭代器
hashtableForward Iterator共两个参数:①迭代器所指向的元素②迭代器指向的元素所属桶的指针这个迭代器也是hash_map,hash_set,hash_multimap,hash_multiset的迭代器

注意:

  • 这上面的迭代器都只是SGI版本的迭代器,但是在其他版本中并不一定是一样的。
  • 但是可以看到的是,基本所有的迭代器中都有一个元素,是指向该容器中某一个元素的指针,所以迭代器有和指针非常类似的行为,也来源于此。

九、总结

其实迭代器从使用的角度来看,行为类似于指针,并且有和->操作符,但是从源码角度看,里面蕴含的技巧真的很多。首先迭代器是一个类,这个类中重载了和->操作符,所以行为类似指针,并且类中的成员变量也基本都是指针,也可以理解为对指针作了一些封装。收货很大!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值