73.迭代器
除了vector之外,标准库还定义了其他几种容器。所有标准库容器都可以使用迭代器,但是其中只有少数几种才同时支持下标运算符。严格来说,string对象不属于容器类型,但是string支持很多与容器类型类似的操作。vector支持下标运算符,这点和string一样:string支持迭代器,这也和vector是一样的。
类似于指针类型(参见2.3.2节,第47页),迭代器也提供了对对象的间接访问。就迭代器而言,其对象是容器中的元素或者string对象中的字符。使用迭代器可以访问某个元素,迭代器也能从一个元素移动到另外一个元素。迭代器有有效和无效之分,这一点和指针差不多。有效的迭代器或者指向某个元素,或者指向容器中尾元素的下一位置;其他所有情况都属于无效。
1.使用迭代器
和指针不一样的是,获取迭代器不是使用取地址符,有迭代器的类型同时拥有返回迭代器的成员。比如,这些类型都拥有名为begin和end的成员,其中begin成员负责返回指向第一个元素(或第一个字符)的迭代器。如有下述语句:
//由编译器决定b和e的类型;参见2.5.2节(第61页)
//b表示v的第一个元素,e表示v尾元素的下一位置
auto b = v.begin(), e = v.end(); //b和e的类型相同
end成员则负责返回指向容器(或string对象)“尾元素的下一位置(one past the end)" 的迭代器,也就是说,该迭代器指示的是容器的一个本不存在的"尾后(offthe end)"元素。这样的迭代器没什么实际含义,仅是个标记而已,表示我们已经处理完了容器中的所有元素。end成员返回的迭代器常被称作尾后迭代器(off-the-enditerator)或者简称为尾迭代器(enditerator)。特殊情况下如果容器为空,则begin和end返回的是同一个迭代器。
如果容器为空,则begin和end返回的是同一个迭代器,都是尾后迭代器。
1.1迭代器运算符
标准容器迭代器的运算符
指令 | 解释 |
---|---|
*iter | 返回迭代器iter所指元素的引用 |
iter->mem | 解引用iter并获取该元素的名为mem的成员,等价于(*iter).mem |
++iter | 令iter指示容器中的下一个元素 |
--iter | 令iter指示容器中的上一个元素 |
iter1 == iter2 | 判断两个迭代器是否相等(不相等),如果两个迭代器指示的是同一个元素或者它们是同一个容器的尾后迭代器,则相等;反之,不相等 |
iter1 != iter2 | 判断两个迭代器是否相等(不相等),如果两个迭代器指示的是同一个元素或者它们是同一个容器的尾后迭代器,则相等;反之,不相等 |
和指针类似,也能通过解引用迭代器来获取它所指示的元素,执行解引用的迭代器必须合法并确实指示着某个元素(参见2.3.2节,第48页)。试图解引用一个非法迭代器或者尾后迭代器都是未被定义的行为。
举个例子,3.2.3节(第84页)中的程序利用下标运算符把string对象的第一个字母改为了大写形式,下面利用迭代器实现同样的功能:
string s("some string");
if (s.begin() != s.end())//确保s非空
{
auto it = s.begin();//it表示s的第一个字符
*it = toupper(*it);//将当前字符改成大写形式
}
1.2将迭代器从一个元素移动到另外一个元素
迭代器使用递增(++)运算符(参见1.4.1节,第11页)来从一个元素移动到下一个元素。从逻辑上来说,迭代器的递增和整数的递增类似,整数的递增是在整数值上“加1",迭代器的递增则是将迭代器”向前移动一个位置”。
现在利用迭代器及其递增运算符可以实现把string对象中第一个单词改写为大写形式:
//依次处理s的字符直至我们处理完全部字符或者遇到空白
for (auto it= s.begin(); it != s.end() && !isspace(*it); ++it)
关键概念:泛型编程
原来使用C或Java的程序员在转而使用C++语言之后,会对for循环中使用=而非<进行判断有点儿奇怪,比如上面的这个程序以及C++ Primer 85页的那个。C++程序员习惯性地使用!=,其原因和他们更愿意使用迭代器而非下标的原因一样:因为这种编程风格在标准库提供的所有容器上都有效。
之前已经说过,只有string和vector等一些标准库类型有下标运算符,而并非 全都如此。与之类似,所有标准库容器的迭代器都定义了==和!=,但是它们中的大多数都没有定义<运算符。因此,只要我们养成使用迭代器和!=的习惯,就不用大在意用的到底是哪种容器类型。
1.3迭代器类型
拥有迭代器的标准库类型使用interator和const_interator来表示迭代器的类型:
vector<int>::iterator it;//it能读写vector<int>的元素
string::iterator it2;//it2能读写string对象中的字符
vector<int>::const_iterator it;//it3只能读元素,不能写读素
string::const_iterator it2;//it4只能读字符,不能写字符
术语:迭代器和迭代器类型
迭代器这个名词有三种不同的含义:可能是迭代器概念本身,也可能是指容器定义的迭代器类型,还可能是指某个迭代器对象。
重点是理解存在一组概念上相关的类型,我们认定某个类型是迭代器当且仅当它支持一套操作,这套操作使得我们能访问容器的元素或者从某个元素移动到另外一个元素。
每个容器类定义了一个名为iterator的类型,该类型支持迭代器概念所规定的一套操作。
1.4begin和end运算符
begin和end返回的具体类型由对象是否是常量决定,如果对象是常量,begin和 end返回const_iterator;如果对象不是常量,返回iterator:
vector<int> v;
const vector<int> cv;
auto it1 = v.begin(); // it1的类型是vector<int>::iterator
auto it2 = cv.begin();//it2的类型是vector<int>::const_iterator
有时候这种默认的行为并非我们所要。在C++ Primer 6.2.3节(第191页)中将会看到,如果对象只需读操作而无须写操作的话最好使用常量类型(比如const_iterator)。为了便于专门得到const_iterator类型的返回值,C++11新标准引入了两个新函数,分别是cbegin和cend:
auto it3 = v.cbegin(); //it3的类型是vector<int>::const_iterator
1.5结合解引用和成员访问操作
解引用迭代器可获得迭代器所指的对象,如果该对象的类型恰好是类,就有可能希望进一步访问它的成员。例如,对于一个由字符串组成的vector对象来说,要想检查其元素是否为空,令it是该vector对象的迭代器,只需检查it所指字符串是否为空就可以了,其代码如下所示:
(*it).empty();
C++语言定义了箭头运算符(->)。箭头运算符把解引用和成员访问两个操作结合在一起,也就是说,it->mem和(*it).mem表达的意思相同。
//依次输出text的每一行直至遇到笫一个空白行为止
for(auto it= text.cbegin(); it != text.cend() && !it->empty(); ++it)
cout <<*it<< endl;
1.6某些对vector对象的操作会使迭代器失效
虽然vector对象可以动态地增长,但是也会有一些副作用。已知的一个限制是不能在范围for循环中向vector对象添加元素。另外一个限制是任何种可能改变vector对象容量的操作,比如push_back,都会使该vector对象的迭代器失效。9.3.6节(第315页)将详细解释迭代器是如何失效的。
谨记,但凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素。
2.迭代器运算
vector和string迭代器支持的运算
指令 | 解释 |
---|---|
iter+n | 迭代器加上一个整数值仍得一个迭代器,迭代器指示的新位置与原来相比向前移动了若干个元素。结果迭代器或者指示容器内的一个元素,或者指示容器尾元素的下一位置 |
iter-n | 迭代器减去一个整数值仍得一个迭代器,迭代器指示的新位置与原来相比向后移动了若干个元素。结果迭代器或者指示容器内的一个元素,或者指示容器尾元素的下一位置 |
iter1+=n | 迭代器加法的复合赋值语句, 将iter1加n的结果赋给iter1 |
iter1-=n | 迭代器减法的复合赋值语句, 将iter1减n的结果赋给iter1 |
iter1-iter2 | 两个迭代器相减的结果是它们之间的距离,也就是说,将运算符右侧的迭代器向前移动差值个元素后将得到左侧的迭代器。参与运算的两个迭代器必须指向的是同一个容器中的元素或者尾元素的下一位置 |
>、>=、<、<= | 迭代器的关系运算符,如果某迭代器指向的容器位置在另一个迭代器所指位置之前, 则说前者小于后者。 参与运算的两个迭代器必须指向的是同一个容器中的元素或者尾元素的下一位置 |
2.1迭代器的算术运算
可以令迭代器和一个整数值相加(或相减),其返回值是向前(或向后)移动了若干个位置的迭代器。执行这样的操作时,结果迭代器或者指示原vector对象(或string对象)内的一个元素,或者指示原vector对象(或string对象)尾元素的下一位置。
举个例子,下面的代码得到一个迭代器,它指向某vector对象中间位置的元素:
//计算得到最接近vi中间元素的一个迭代器
auto mid = vi.begin() + vi.size()/2;
如果vi有20个元素, vi.size() /2得10, 此例中即令mid等于vi.begin() +10。已知下标从0开始, 则迭代器所指的元素是vi10], 也就是从首元素开始向前相隔10 个位置的那个元素。
对于string或vector的迭代器来说,除了判断是否相等,还能使用关系运算符(<、 <=、>、>=)对其进行比较。 参与比较的两个迭代器必须合法而且指向的是同一个容器的元素(或者尾元素的下一位置)。例如,假设it和mid是同一个vector对象的两个迭代器,可以用下面的代码来比较它们所指的位置孰前孰后:
if (it < mid)//处理vi前半部分的元素
只要两个迭代器指向的是同一个容器中的元素或者尾元素的下一位置, 就能将其相减,所得结果是两个迭代器的距离。所谓距离指的是右侧的迭代器向前移动多少位置就能追上左侧的迭代器,其类型是名为difference_type的带符号整型数。string和 vector 都定义了difference_type, 因为这个距离可正可负,所以difference_type是带符号类型的。
2.2使用迭代器的运算
使用迭代器运算的一个经典算法是二分搜索(基于有序的序列)。二分搜索从有序序列中寻找某个给定的值。二分搜索从序列中间的位置开始搜索,如果中间位置的元素正好就是要找的元素,搜索完成;如果不是,假如该元素小于要找的元素,则在序列的后半部分继续搜素;假如该元素大于要找的元素,则在序列的前半部分继续搜索。在缩小的范围中计算一个新的中间元素并重复之前的过程,直至最终找到目标或者没有元素可供继续搜索。
下面的程序使用迭代器完成了二分搜索:
//text必须是有序的
//beg和end表示我们搜索的范围
auto beg = text.begin(), end = text.end();
auto mid = text.begin()+(end - beg)/2;//初始状态下的中间点
//当还有元素尚未检查并且我们还没有找到sought时执行循环
while (mid ! = end && *mid ! = sought)
{
if(sought < *mid) //我们要找的元素在前半部分吗?
end = mid;//如果是,调整搜索范围使得忽略掉后半部分
else
beg = mid + 1;//在mid之后寻找
mid = beg + (end - beg)/2; //新的中间点
}
参考:C++ Primer