现在我们来讲讲C++里面的重要内容,标准模板库 先来看看标准模板库大概是什么样。 标准模板库呢简称为STL,是standard template library的这个缩写。 里面主要是一大堆的函数模板, 类模板。因为C++语言的核心优势之一就是便于软件的重用。 那它有两方面能够体现软件的重用,一方面就是面向对象的思想,比如说继承啊, 多态啊,标准类库等等啊。另外一方面体现软件重用的地方 就是C++支持泛型程序设计。 什么叫泛型程序设计啊?就是使用模板的程序设计 那C++里面你可以自己编写模板,也可以使用标准模板库里面带的各种类模板和函数模板 那在C++里面 标准模板库里面呢它是将一些常用的数据结构,比方说这个链表啦,数组啦,二叉树啦等等 实现为一些类模板。 然后它把一些常用的算法,比方说排序、查找等等写成函数模板。 那么有了这些模板以后啊,以后不论数据 结构里面存放的是什么对象,然后你这个算法针对是什么样的这个对象 那我们都不必重新自己去写这个数据结构, 也不必自己去编写算法。直接使用这些类模板和函数模板就能够处理各种各样的 数据啦。那标准模板库呢就是 常用的数据结构和算法的模板的集合。 那有了这个STL以后呢,我们就不必要编写大多数的标准数据结构和算法 而且STL里面的这些模板它的性能也是相当高的, 它 对于一个算法实现水平一般的程序员来说,你自己实现的那些 算法和数据结构可能效率还没有STL的里面的那些东西高。 那STL里面有一些基本的概念,有三个,就是容器,迭代器和算法 那我们先看什么叫容器,容器实际上就是数据结构。这个容器里面可以容纳各种类型的数据, 是通用的数据结构。那容器实现为类模板 那迭代器。迭代器实际上就是,本质上就是指针。 它可以用于依次存取容器中的元素,就是你要访问 容器中的元素,你就得必须通过迭代器来进行。 这迭代器用起来像是一个指针,它的内部 归根到底也是通过指针来实现的。那算法是什么概念呢?算法实际上就是一个个的函数模板,这些 函数模板往往都是用来操作容器中的元素的。比方说用来对这个容器进行排序,把整个容器排序 或者说在整个容器查找一些、一个值,这些都是算法所完成的功能。 那比方说我们常见的sort算法,就是对 用来对一个vector这样的容器里面的数据进行排序。把整个vector变成有序的, 然后我们可以用find这个算法来搜索一个list这种容器里面的对象 这个list呢就是链表,而且双向链表。 那算法本身与他们操作的数据类型无关,因为它是函数模板嘛, 所以这个算法它可以用在从简单数组到高度复杂的容器上面的任何数据结构上面使用。 在C++里面,就简单的数组我们也认为是一种容器。 举一个例子,这一个简单的数组,整型数组,我们就可以说它是一个容器。 那这个容器上面的迭代器是什么呢?就是int *类型的指针, 就可以作为这个容器上的迭代器使用。 比方说你让一个Int *类型的指针指向这个数组的开头,然后你通过对这个 指针不断地进行++操作,就能够让这个指针指向这个容器里面的不同的元素。 然后你就通过这些指针就可以访问这些元素,所以int*类型的指针就可以作为迭代器。那这个sort算法 它就可以作用于这个数组,这个容器上面,对它进行排序。比方说我们可以写 sort(array, array+70) 那这条语句就对这个array数组里面的前70个元素进行排序。那在这里, 这个array是个int型的指针,array+70也是一个int型的指针。这两个东西都是迭代器 那STL里面的容器呢,就是用来存放各种类型的数据的。你可以把基本类型的变量、对象等等都往里头放, 那容器就都是类模板,STL里面的容器可以说有三种, 第一种叫做顺序容器,容器都是有类模板,类模板嘛,所以这些类模板当然都是有名字啦 那STL里面的顺序容器一共有三个,它们的名字是vector,deque 和list。这个vector就是动态数组, 一维数组,deque是双向队列,list是 双向链表。 第二种容器叫关联容器,关联容器有set,multiset,map和multimap这四个。 它们都是排序的,而且查找速度会很快 第三类呢叫做容器适配器。容器适配器有stack,queue,priority_queue stack就是栈,queue是队列,priority_queue是优先级队列 STL的容器是用来放各种数据的,就可以把基本类型的变量放进去也可以把对象放进去。当一个对象 被插入容器中的时候啊,实际上这个对象并没有被放进去,被放到容器里面的是这个对象的一个复制品。 然后许多这个算法,比如说排序、查找,它要求对容器中的元素进行比较,比较是否相等或者比较大小。然后 有的容器呢本身它就是排序的,那这个这种排序的容器在工作的过程中肯定也要对这个容器里面的元素进行 比较大小,交换位置等等操作。那因此说呢,经常的,我们放入容器里面的对象,它所属的类 我们往往会需要重载==和<运算符,以便这些算法对这个容器里面的元素进行 判断相等或者判断比大小,会用得到这两个运算符。那下面 我们就说说这个顺序容器。顺序容器的特点就是说这个容器里面的元素并没有排序,那元素的插入位置同元素的值无关。 不管这个元素的值是不是大还是小,你想让它插入到哪个位置你就能让它插入在哪个位置 顺序容器有三种,它们是vector,deque和list。我们先来看看vector。vector它在头文件<vector>里面 来声明的。我们要使用它就得包含这个头文件。vector呢它称为向量,实际上它就是动态数组, 那既然是个数组,它的元素在内存中就是连续存放的。它是动态数组的意思就是说它的数组的元素的个数 可以动态地变化。我们知道数组呢,它随机存取任何元素肯定都是在常数时间完成, 为什么呢?因为你只要给一个元素的下标,那在我们已经知道了数组首地址的情况下,根据下标要取出那个元素 是很容易的。我们只需要在首地址上面再加一个下标的这种偏移量,就能够算出比如下标位为i的那个 元素的地址。然后我们马上就能访问到下标为i的这个元素,所以随机存取任何元素是在常数时间完成。这个vector呢 在尾部增删元素会具有较佳的性能,这里所说的较佳性能它的意思是大多数情况下在常数时间 就能够完成在尾部增删元素,但是在某些特殊的情况下这个增删的操作就无法在常数时间完成,就需要 O(n)这么多的时间。为什么呢?因为这个vector我们知道它是动态数组,所以它里面 肯定有动态分配的存储空间用来存放数组的元素。那vector内部在实现的时候这个存储空间往往会预先 多分配一些,比方说你这个vector元素里面只有10个元素的情况下,vector也会实现分配比如说30个元素的, 32个元素的那么多的空间。这样做的好处就是你要往里头加第11个元素,加第12个元素一直加到第32个元素的时候 在尾部添加元素都只需就直接把元素加进去就行了。存储空间不需要重新分配。那你要加到 第33个元素的时候,这个时候存储空间不够了,那此时就必须重新分配存储空间,把那个原来的32个 元素拷贝过来然后再在尾巴加新的这个元素。那么我们就知道,在不需要重新分配存储空间的情况下往尾部添加 元素就直接加进去,所以时间是常数的。那要重新分配存储空间了,那你要把老的内容还要拷过来,这个时候时间就不是常数的。 那时间是什么呢?就是O(n)了,因为它跟你原来有多少个元素是有关系的。由于vector总是提前 多分配一些存储空间,因此说在大部分情况下我们往尾部添加这个元素或者删除元素的时候 那是不需要重新分配存储空间。因此是时间复杂度就是常数,只有在少数情况下,要分配存储空间了, 这个时间才会变成O(n)的。那这个vector你在中间或者说 在头部插入或者删除一个元素,时间复杂度是怎么样的呢?这个很容易想出来,假设你在这个位置要删除掉它, 那你就会必须要把后面这些元素全部都往前移对不对?那就是时间复杂度肯定不是常数的, 而是跟你后面有多少个元素有关系。所以这个复杂度是O(n)的。包括你在前面的位置你要插入一个元素,那你肯定要把这些 已有的元素全部都要往后移,然后空出一个位置再插入新的元素。这个复杂度当然也不是常数,也是O(n)的。所以vector 在中间或者说头部插入三种元素,复杂度都是O-N的,在尾端增删元素 大部分情况下常-时间是常数的。那说到这我就要提醒同学们,那就我们学习 STR里面的容器和算法不但要知道这些容器和算法完成什么样的功能以及如何去使用它,我们还必须要了解 在这些容器和算法进行的各种操作它的时间复杂度是怎样的。一个算法运行起来它的时间是怎样的。 一个容器上面的某一种操作时间复杂度是怎样的。这我们都需要了解,否则我们写出来的成绩就会显得效率很低。因为你 滥用了某一种时间复杂度比较高的这个操作。下面我们再来看这个deque。 Deque呢它也是顺序容器。那deque它事实上是双向队列。 元素在内存应该也是连续存放的。这一点跟动态数组有点相似。它跟vector的差别在什么地方呢? 这共同点是它和vector 一样随机存取任何元素都能在常数时间完成,但这个常数要比-要可能要大一些,也就是说 随机存取元素虽然能在常数时间完成,但这个速度要比vector要慢。但vector比 但这个deque比vector明显的优点在于在deque的两端增删元素都会具有较佳的性能。所谓较佳的性能 就是前面讲的大部分情况下这个时间是常数的。那deque是如何做到这一点的呢?那我们看假设这是一个deque。 这在内存里面连续的这个9个单元。那对于deque来说它是双向队列,它会有一个队头的指针 和一个队尾的指针。那-那这个deque里面的元素就位于队头队尾指针之间。那假设队头指向的元素是有效的。 队尾指向的那个单元呢是空的,队尾指向的那个单元的前一个是最后一个队列的有效元素。 那它是双端队列,在两边增删元素,时间复杂度大多数情况下都是常数的。那只有需要 重新分配存储空间的时候才-才会变成O-N的。因为要把老的那种拷贝过来。那为什么大多数情况下复杂度 是这个常数的呢?我们想象假设我们要在头部加一个人数,那我实际上就把元素放在这,然后我把这个 头-头部的指针移动到这就行了,这个时候我们就添加了一个元素了。那我要在头部删除一个元素很简单。 我就把这个元素去掉,然后呢我把头指针移到这,实际上把头指针移到这就意味着把这个头部的元素已经删掉了。 这其他存储空间并没有发生变化,只是移动一下头指针的时间呢就是常数的。那在尾部增删元素也是一样的。比如说你要把这个尾数元素删掉。 那你实际上就直接把尾指针移到这,就意味着刚才删掉了一个元素。那增加也是相同的。所以呢在 大多数情况下deque里面增删元素在双端增删元素都是具有 常数时间就能够完成的,除非这个deque里面的存储空间都已经占满了,这个时候你要往里头添加元素就得重新分配存储空间。 那当然时间复杂度就-就-就-就是O-N的了。但是为什么deque随机存取任何元素 它的这个常数时间会慢于这个vector呢?那我们来看看这个道理。就是这个 deque添加元素有可能是这样一种情况。就原来队头指向的这个元素是a0。然后我们加了a1,a2,a3,a4,a5,a6。 加完a6以后呢就到达了这一片连续存储空间的末尾了。这个时候还可以往里头加,怎么加呀? 就是我倒过来把a7放在这个存储空间开头的位置,但是我可以把这个-这个队尾指针放在这。这并没有矛盾对吧。 这个是队头在这,顺着队头一直走到队尾,顺着这个队头一直走到存储空间的末尾,然后再倒回来往前走一个。 这是下一个元素的位置。这是符合逻辑的。不会有任何矛盾。那-那么在这种情况下,我们要随机访-存取 任何元素,也就是说我给一个下标,你就得算出这个元素的地址,那怎么算啊?当然我们用这个下标去加这个head 的对吧?但是加head的-光加在head上不够,那么有可能发生加-你这个下标加上head了以后 就超过了这个存储空间的末尾的情况。那如果在发生了超过存储空间的末尾的情况的时候,你又得把这个位置倒到这个 头部来了。就等于说你要减去一个整个存储空间的这个长度,这样看才能把位置倒回到这个头上来。 那这样一个判断是否越-越过了尾巴?并且要回到来前面的这个操作是需要时间的。那vector是没有这种时-这种操作的。 因此说deque在进行随机存取元素的时候它会比vector 下面再来看第三种顺序容器list。list呢它是一个双向链表。 既然是链表嘛,它们的元素在内存里面当然就是不连续存放的。 那而且在任何位置增删元素都能够在常数时间内完成。 这里面有一个前提就是你已经找到了那个需要增删元素的位置,在这个情况下增删元素是在常数时间内完成的。 那你不能把寻找要增删位置的这部分时间也算进去。 那-那比方说我在这个双向链表里面要把这个元素删掉,那我要做的操作是什么? 无谓就是摆一下前面那个元素的指针和后面这个元素的指针,要修改两个指针,然后中间呢要把它delete掉。 那这一部分时间当然就是在常数时间内进行完成对吧。 然后这个list呢它不支持随机存取。 也就是说list里面它并没有一个成员函数。 这个成员函数使得你给一个下标i,说我要访问第i个元素,这个成员函数就返回给你第i个元素的地址。 或者它的内容。list里面不提供这样的成员函数。那你如果是自己就是要访问第i个元素呢,你怎么办啊? 你只能从这个链表的头部开始一个一个的往后走,一直走到第i个元素为止。 所以我们说list它不支持随机存取。 那下面呢再简单的介绍一下关联容器。 关联容器它的特点是元素是排序的。 那既然是排序的,那你插入一个元素的时候,插入完了这个 关联容器一样要维持有序。所以你插入的那个元素放在什么位置 是由这个元素的值决定的。不是说你想把它插在哪它就能插在哪。 那关联容器把里面的元素都排了序它的目的是什么呢? 目的当然是为了查找。就是说关联容器在查找的时候具有非常好的性能。 我们知道如果你在一个-一个没有排序的序列上面进行查找的话 那你只能顺序查找,从头查-找到尾对吧。就是说时间的话都是O-N的。 但是如果你在一个排好序的序列或者说容器上面进行查找的话呢,你可以用这个类似于 折半查找的方法,每次缩小查找范围一半,所以就是说查找的时间复杂度是变成O(log(N))的。 那关联容器呢 它通常以平衡二叉树的方式实现。那什么叫平衡二叉树数据的内容呢我们就不提了。 我们在这个关联容器上面以进行插入一个元素 跟查找一个元素它的时间复杂度都是O(log(N))的。 就都是很快的。那关联容器有四种。 前两种呢是set和multiset。这两种关联容器 都在头文件set里面。这个set就是集合。 在set中呢它不允许有相同的元素。 有set和multiset的里面的元素都是按照这个都是排好序的,按照元素本身的值来进行排序。 set里面不允许有相同-相同的元素。但是multiset中呢它允许存在相同的元素。 那我们再看这个map和multimap它们都在头文件map里面。 它这个map和set-set不同之处在于什么呢? set的里面可以用来放各种各样类型的对象,什么基本类型的变量都可以。 但是map里面呢它里面只能放对象,而且这个对象 必须有且仅有两个成员变量。这两个成员变量名字还是确定的。一个叫做first,一个叫做second。 然后map它是根据first的值对元素进行从小到大的排序,当然这里所说的小和大 并不一定就是我们通常认为的比如说一是小这个意思。这个小和大它的含义可以 有多种方式可以自己定义的,这是后话。 那map根据first的值的元素进行排序 并且可以根据first来快速的进行检索。 那等于first就相当于是-是关键制,那个second呢就相当于是一个关键的值。然后整个map和multimap都用 根据关键值来排序。那这个map和multimap 它唯一的不同之处就在于 这个map里面不允许有两个元素。它们的 first的成员变量是相等的。 Multimap呢它允许有多个元素。它们的first的成员变量相同。 也就是说在map里面关键值是不能重复的。但是在multimap里面呢关键值是可以重复的。 接下来我们说这个容器适配器。 容-容器适配器有三种。第一种是stack。stack呢就是栈。 什么叫栈啊?栈是项的有限序列,然后满足序列中被 删除、检索和修改的项最近插入的那个序列的项。 也就是说是个栈顶的项。那栈就是后进先出的一种数据结构。 这里就是一个栈。这一块iii指向的就是一个的栈井。 那对于一个stack来说你要访问的元素必须是位于top的。你不能访问这里面的元素。你要访问的只能是top的元素。 而且你要进行这个插入操作的时候你也只能把这个元素加在这个栈顶。 这个就是stack,你要删除的元素也只能是位于栈顶的元素。 它是后进先出的。第二种容器适配器叫做queue, 它是队列,那队列是先进先出的,这个 跟前面的双向队列不一样哦,这里是一个单向的队列,也就是说呢, 对于queue来说,插入操作只能在尾部进行,就只能在尾部添加元素。但是删除和检索 以及修改都只能在头部进行,就是在队列里面,你能访问的 元素一定是位于这个队头的,你可以取它的内容,修改它的内容,把它删掉。 当时你要往这个队列里面添加元素的时候呢,你只能加在队尾。 队尾的元素你也没法访问, 你可能往队尾加入元素,反正队列里面,除了队头元素以外的其它元素你都是没有办法访问的。 你要访问它们,你只能把队头元素弹出去,删掉,让下面的元素变成队头元素以后,你才能访问它。 这个就是queue。那还有一种容器适配器呢, 叫做priority_queue,它也是一个队列, 但是呢,它是优先级队列,就是说,它内部是维持某种有序的, 然后呢,这个priority_queue能够确保优先级最高的那个元素 总是位于这个队列的头部。也就是说,你访问到的元素,你删除的元素,都一定是 优先级最高的那个元素。那至于什么叫优先级最高,这个可以自己定义。 那接下来我们就要看看这个,顺序容器和关联容器中它们都有哪些成员函数可以使用。 这里列出来只是一部分,并不是说全部的啊,这一部分只是顺序容器和关联容器中都有的成员函数。 比如它有一个定义,这个成员的函数呢,它返回一个迭代器 这个迭代器指向元素中的第一个元素,就是头一个元素。 那and呢,它返回自己也是一个迭代器。 这个迭代器指向容器中最后一个元素 后面的位置。注意,它不是指向容器中的最后一个元素,它是指向容器中最后一个元素后面的那个位置。 那那个位置实际上是没有东西的,对吧?你想要访问end所指向的这个东西会导致出错的, 这个rbegin,它返回指向容器中最后一个元素的迭代器。 它指向了最后一个元素。 rend它就返回一个迭代器,这个迭代器呢,指向容器中第一个元素最前面的那个元素 的前面的那个位置,实际上rend它也没有指向任何东西。还有这个erase, 可以用它从 容器中删除一个或者几个元素。clear就是把容器清空,所有的元素都删除。 前面那页的那些函数是顺序容器和关联容器里边都有的,现在我们再来看看顺序容 器里面有哪些常用的成员函数。比如它有front,它能够返回容器中第一个元素的引用,注意它返回的 引用,而不是迭代器。back,它返回容器中最后一个元素的引用。push_back,这个特别常用,它用来在 容器的末尾添加新的元素。pop_back呢,它用来删除容器末尾的元素。erase,是用来删除迭代 器某一个迭代器所指向的元素,然后或者用来删除一整个区间。不过在删除 以后呢,erase能够返回被删除元素后面的那个元素的迭代器。 那说了半天迭代器,我们就要说说到底迭代器是个什么东西。它是用 于访问容器中元素的一种,相当于中介。它用起来像指针一样,归根到底,也是通过 指针来实现的。总之,它用于指向顺序容器和关联容器中的元素。它的用法呢,跟指针类似。 它也有const和非const两种,就跟指针有const和非const一样。然后 通过迭代器就可以读取它指向的元素,这跟指针是一致的,对吧?然后通过非const的迭代器还能修改其指向的元素。 那迭代器到底长什么样子呢?那我们可以自己定义一个,定义一个容器类上面的迭代器,它的方法 可以是容器类名iterator,然后再变量名。这个迭代器嘛, 它一定是具体针对于某一个容器类的。 在一个容器类上面,就会,你可以定义一个迭代器出来。什么是容器类啊?从容器模板实例化出来的 类就叫做容器类。这就是定义一个迭代器的方法。有了这个迭代器以后呢,我们就可以访问这个容器类 的对象的里面的那个元素了。或者我们也可以写容器类名const _iterator 变量名,那这个变量名 也是一个迭代器。只不过这个迭代器是常量的迭代器了。那如果我们要访问一个迭代器所指向的元素,怎么办呢?我们写*迭代器 变量名,这就访问它所指向的元素了。这看上去,就跟指针的用法是一样的。那迭代器上面可以执行 这个++操作,迭代器一旦执行了++操作,它就会指向容器中的下一个元素。那如果迭代器到达了 容器中最后一个元素的后面,那你这个时候再使用它就会出错,因为它已经不指向任何有效 的东西了。这种错误就类似于使用NULL或未初始化的指针一样。下面我们看一个迭代器的 例子。这个例子里面用到了vector,所以我们include了文件Veck。 vector<int> v这个时候我们就定义了一个vector容器类的对象v,这个v是一个动态的数组。 然后我们这边写的是int,所以这个数组里面的每一个元素都是整形的,那么这个v的类型是什么呢?就是vector<int>。 用这种方法初始化的v呢,它里面什么元素都没有,是一个空的这个数组。 接下来我们往v里面放入一些元素,都是在尾部进行添加。push_back在尾部添加1,然后push_back 2添加 了2,一直push back,最后放进去1,2,3,4四个元素。然后我们在定义这个 数组v。怎么做呢?我们可以定义一个迭代器。vector<int> 是容器类名,const_iterator 然后i,这个i就是一个迭代器,它是一个常量迭代器,你通过i只能访问它所指向的内容,不能修改它指向的内容。 下面我们通过这个i去便利整个v。怎么便利呢?让i从v.begin开始,v.begin 回忆一下,返回值是什么啊?就是v里面第一个元素的迭代器。那i就一开始让它指向 v里面的第一个元素。然后这个v.end是什么啊?是v里面最后一个元素后面的那个位置。v.end 返回值是一个迭代器,它指向v里面最后一个元素的后面。那我们让这个 迭代器i从v.begin开始,就是从第一个元素的位置开始,不停地通过++,指向下一个元素,一直走到 v.end为止。那只要i还没有到达这个v.end,那i的指向的内容就是有意义的。对吧?它就真实地指向一个 元素。那我们就输出*i。i是迭代器,*i就是这个迭代器所指向的元素。 然后呢再输出一个换行,这个程序还有下面一部分。就是这一部分,这个循环所输出的结果 是什么呢?就是1,2,3,4。它便利了整个数组,输出每一个元素的值。那接着往下走, 然后我们还可以定义一个所谓的反向迭代器。叫做reverse_iterator,它是反向迭代器,它跟那个 iterator类型是不兼容的啊。它名字都不一样,所以类型就不兼容。类型的名字不一样,所以当然也不兼容。那对于反向迭代器 来说呢,我们可以让这个r=v.rbegin,那rbegin是什么东西呢?就是v里面 的最后一个元素的迭代器。然后呢只要 r还没有等于v.rend,就让这个r++。对于一个反向迭代器来说,你让它执行++操作, 实际上它就会指向容器里面的前一个元素。反向迭代器跟所谓的正向迭代器不一样的地方。 上一节片子里面看到的是正向迭代器,反向迭代器,你对它进行++操作,它是从后往前走的。那这个rend是指向 的v里面的第一个元素前面的位置,然后这边的r呢,一开始指向的是最后一个元素,那我们现在让这个r 不停地往前走,然后把每一个r都指向的元素都输出,那这个循环输出的结果当然就是4,3,2,1。就是 从这个v后面倒着往前,把所有的元素都打出来。刚才又输出了一个换行。那下面在定义一个这个非常量的迭代器, 我们反正通过j就可以修改它所指向的元素,通过这个j便利整个v,把j指向 每个元素都修改成100,然后再来通过前面的那个i,输出了v里面每一个元素的值,那它 输出的值就是四个100了,因为全部已经被改成100了。 SDL里面我们常用的迭代器有两种,一种叫双向迭代器,一种叫随机访问迭代器。 那这两种迭代器上面所能进行的操作是不一样的。对于双 向迭代器,我们可以进行哪些操作呢?列在这了,也就是说,如果p和p1都是双向迭代器, 那下面这些表达式都是有定义的,而且有其功能。比如说++p和p++,它都能够使 得p指向容器中的下一个元素。而--p和p---呢,它都能够使得p指向容器中的上一个元素, 就是往这个容器的头部,开始的位置走。这个*p呢?它能够取p指向的元素,实际上这个表达式,它的返回值 是p所指向的那个元素的引用。然后p=p1,当然就是赋值操作了, 然后p==p1就是用来判断两个迭代器是不是相等。两个迭代器相等就意味着 它们指向相同的元素,然后两个迭代器可以来用不等号来判断是否不相等。 这些都是双向迭代器能够进行地操作。那随机访问迭代器,它的功能 更强。首先所有双向迭代器能做的操作,随机访问迭代器都可以做。此外, 它还能够做一些双向迭代器不能做的操作。比方说我们可以来一个P+=i, 那这个表达式它的效果就是,把p向后移动了i个元素,它修改了p的值。那p -=i呢,它就是把p向前移动了i个元素,就是往容器的头部移动。那 这个表达式它是有定义的,这个p+i这个表达式的值也是一个迭代器,这个迭代器呢,它指向p后面的 第i个元素。那p-i呢,这个表达式的值自然就指向了p前面的第i个元素。 还有,p[i],也是有定义的,显然这个中括号是经过重载的。p[i]这个表达式的值是什么呢? 它的值是p后面第i元素的引用。注意这个表达式的值不再是迭在一起了,而是以p后面第i元素的引用。 然后我们看,对于两个随机访问迭代器来说,我们可以用<,≤,≥ 进行 比较。那这个两个随机访问迭代器,p<p1,到底代表什么意思呢? 那就是说p所指向的这个元素,它在p1所指向 的这个元素的前面。这就是p<p1的含义了。