这里记录一下学习第三章迭代器章节的学习心得。
开篇首先介绍了迭代器这种设计模式的思想,通过迭代器这个方法,我们可以遍历容器的元素,但又不关心容器内部的具体实现,就如同,“万花丛中过,片叶不沾身”。
3.1 开篇提出了一个很重要的思想,迭代器像是胶水,将容器与算法粘连在一起,因此算法不需要考虑容器的内部实现,容器也不需要考虑算法会怎样操作自己。可以说,迭代器是连接二者的桥梁。
3.2 一开始说明,我们可以将迭代器想象成指针,这句话很重要,事实上,我们后续小节为了将二者统一化做了很多工作,就是后面讲到的萃取技术。这里以auto_ptr为例,在该指针类的析构函数中自动完成了该指针指向对象的空间的释放,而无需程序员手动释放。事实上,这也是智能指针的主要思想。
以auto_ptr为例,我们设计了一个链表类,链表节点,以及链表节点迭代器listiter。我们发现在该链表迭代器对前置++ 和 后置++运算符做了重载,相当于函数重载。接着通过find算法,我们在该链表中通过链表迭代器去寻找一个指定值。因为!=这个运算符并不提供对链表节点与int值的逻辑比较。因此我们要在全局重载!=运算符。从这个例子中我们看到,容器,容器的迭代器,算法之间有着密切的关系,因此,容器的迭代器一般是由容器开发者写好的,换言之,如果我们想自定义一种新的容器,我们也需要提供该新容器的迭代器去适配stl中各种内置算法。
3.3 进一步提出,迭代器指向对象的类型如何确定?可以根据类模板参数的自动推导实现,这项工作是编译器做的。
3.4 提出了上述这种模板参数推导方法的局限性,无法推导函数的返回值类型。(这句话没看懂,存疑??暂且搁置)然后给出一种在迭代器内部内嵌类型声明的做法,就是使用typedef,这样做的好处十分明显。但是仍然有一个致命缺点,就是我们的原生指针,比如int * ,char *, double *.他们并不是一个迭代器类,相应的,其内部更不可能有类似的内嵌类别声明。这个缺点十分致命?stl如何解决呢,使用模板偏特化技术。
这里对模板偏特化技术的意义讲解的十分精辟。并不是将模板参数限定死为int 或者 double,而是更进一步的条件限制,比如模板参数是T,这里进一步限制为T*,相当于范围缩小。
接着介绍了一个类模板iterator_traits,这是一个萃取器,通过typedef,将迭代器中的内嵌型别value_type提取出来,接着,通过模板特化技术,我们做一个特化版本的iterator_traits,其参数被进一步限制为T*,这样我们就可以将value_type定义为T类型,相当于原生指针没有该类型,我们在这里为其加上。这样的话,通过统一的接口iterator_traits,我们就可以将无论是迭代器还是原生指针的value_type萃取出来。其实iterator_traits就像一个漏斗,或者滤网,从迭代器中将我们感兴趣的属性提取出来,如果没有呢(对于原生指针),我们就为其加上。那么,将这种思想推广,我们不仅关注迭代器与原生指针的的value_type,还有iterator_category difference_type pointer reference等其余类型,那么,照猫画虎,我们会为这些其余类型做相同的操作。
3.4.1 介绍了value_type
3.4.2 介绍了difference_type,表示两个迭代器之间的距离。并且以count算法为例,展示了difference_type类型的作用,其实根据我的理解,就是一种整数类型,用来表示指针或者迭代器之间的距离,甚至可以粗暴理解为int。
3.4.3 介绍了reference type,这里通过一个指向常量的指针与普通指针为例,当分别对他们做提领,应分别返回右值与左值类型。
3.4.4 介绍了pointer。
3.4.5 介绍了iterator_category,注意是迭代器类别。或者迭代器分类,我们将其分为五类。input:只读 ,output:只写, forward:前向 ,bidirectional:双向 ,random:随机。这里给出了一张图,表明了不同迭代器的概念强化关系,很明显,概念越强,权限越大。
接着以advance算法为例,分别给出了三个版本,针对input bidirectional random,我们发现,不同类别的迭代器版本,具体实现不同,时间复杂度也不同。如何根据不同的迭代器类别选择最具效率的版本呢?这里给出了一个函数重载的做法,被否决了,因为其是在运行期做判断,效率低。
这里再一次运用了萃取技术,通过萃取器萃取出不同的迭代器型别,(是一个类,仅仅做标记用)这里的做法十分高明。相应的,我们也需要为原生指针做一个特化版本,这里为所有的原生指针都归类为随机类型,也就是赋予他们最强的权限,这也符合我们一直以来对指针的认知。接着提到了advance的模板参数命名问题,其实这里仅仅是一个命名规范问题,stl算法的迭代器型别参数永远采用最低阶的型别,也就是input。但只要我愿意,我随便起一个名字都可以,问题不大,只是一个规范问题。接着举了一个B D1 D2的例子来模拟了迭代器五种型别的实现,其实思想很简单,就是派生类的对象可以赋值给基类对象。接着又举了一个distance的例子,思想大差不差。
3.5 是对前面的总结,提出stl中任何容器的迭代器,都必须有iterator_category, difference_type, pointer, reference,value_type这五种类型,这是一种规范或者公约,以维持和其他stl算法的合作。相应的,提出了一个标准模板类iterator,类似于我们第二章中提到的simple_alloc 模板,如果我们自己开发了一种容器,需要写迭代器,就可以从这个标准类中派生。
3.6 是所有iterator.h的源代码
3.7 更进一步,iterator_traits是针对迭代器或者原生类型指针的一个萃取器(漏斗,滤网),接下来介绍的type_traits是针对型别(比如int double,或者我们自定义的struct class)的一个萃取器。
我们关注型别的五个特性,是否具有默认的构造,析构,拷贝构造,重载等号运算符,以及是否为POD类型。如果是的话我们可以进行更为高效的操作,比如内存拷贝,转移,memcpy等等。如果否我们只能去调用他们的构造与析构函数。
这里提到,type_traits内部默认对这五个特性都认为是false,也就是默认一个新类型对象,如果未定义针对该对象的特化版本的type_traits,那么这五个对象的五个属性都是false,对该对象做操作都是最保守,效率最低的做法。接下来给出了针对int double char等做的一系列type_traits特化版本,并且以uninitialized_fill_n算法为例,在判断出对象是否为POD后,我们分别可以做不同的操作,很明显,一旦是pod类型,就可以直接等号赋值,而不用调用construct()。本节最后给我们是否要为我们自己设计的类做type_traits特化版本提供了建议,如果类内有指针成员,就不要做,而采用默认的false。
这里也很好理解,比如我们定义一个ListNode链表节点类,其内部必然有next指针,当我们为一对迭代器框定的范围初始化某个链表节点,肯定是一个个遍历过去用拷贝构造去做,而不是直接赋值,甚至直接memcpy。