3.4.1 使用迭代器(2)
迭代器类型
就像不知道string和vector的size_type成员(参见3.2.2节,第88页)到底是什么类型一样,一般来说我们也不知道(其实是无须知道)迭代器的精确类型。而实际上,那些拥有迭代器的标准库类型使用iterator和const_iterator来表示迭代器的类型:
- vector<int>::iterator it; // it能读写vector<int>的元素
- string::iterator it2; // it2能读写string对象中的字符
-
- vector<int>::const_iterator it3; // it3只能读元素,不能写元素
- string::const_iterator it4; // it4只能读字符,不能写字符
const_iterator和常量指针(参见2.4.2节,第62页)差不多,能读取但不能修改它所指的元素值。相反,iterator的对象可读可写。如果vector对象或string对象是一个常量,只能使用const_iterator;如果vector对象或string对象不是常量,那么既能使用iterator也能使用const_iterator。
术语:迭代器和迭代器类型
迭代器这个名词有三种不同的含义:可能是迭代器概念本身,也可能是指容器定义的迭代器类型,还可能是指某个迭代器对象。
重点是理解存在一组概念上相关的类型,我们认定某个类型是迭代器当且仅当它支持一套操作,这套操作使得我们能访问容器的元素或者从某个元素移动到另外一个元素。
每个容器类定义了一个名为iterator的类型,该类型支持迭代器概念所规定的一套操作。
begin和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
有时候这种默认的行为并非我们所要。在6.2.3节(第213页)中将会看到,如果对象只需读操作而无须写操作的话最好使用常量类型(比如const_iterator)。为了便于专门得到const_iterator类型的返回值,C++(www.cppentry.com)11新标准引入了两个新函数,分别是cbegin和cend:
- auto it3 = v.cbegin(); // it3的类型是vector<int>::const_iterator
类似于begin和end,上述两个新函数也分别返回指示容器第一个元素或最后元素下一位置的迭代器。有所不同的是,不论vector对象(或string对象)本身是否是常量,返回值都是const_iterator。
结合解引用和成员访问操作
解引用迭代器可获得迭代器所指的对象,如果该对象的类型恰好是类,就有可能希望进一步访问它的成员。例如,对于一个由字符串组成的vector对象来说,要想检查其元素是否为空,令it是该vector对象的迭代器,只需检查it所指字符串是否为空就可以了,其代码如下所示:
- (*it).empty()
注意,(*it).empty()中的圆括号必不可少,具体原因将在4.1.2节(第136页)介绍,该表达式的含义是先对it解引用,然后解引用的结果再执行点运算符(参见1.5.2节,第23页)。如果不加圆括号,点运算符将由it来执行,而非it解引用的结果:
- (*it).empty() // 解引用it,然后调用结果对象的empty成员
- *it.empty() // 错误:试图访问it的名为empty的成员,但it是个迭代器, // 没有empty成员
上面第二个表达式的含义是从名为it的对象中寻找其empty成员,显然it是一个迭代器,它没有哪个成员是叫empty的,所以第二个表达式将发生错误。
为了简化上述表达式,C++(www.cppentry.com)语言定义了箭头运算符(->)。箭头运算符把解引用和成员访问两个操作结合在一起,也就是说,it->mem和(*it).mem表达的意思相同。
例如,假设用一个名为text的字符串向量存放文本文件中的数据,其中的元素或者是一句话或者是一个用于表示段落分隔的空字符串。如果要输出text中第一段的内容,可以利用迭代器写一个循环令其遍历text,直到遇到空字符串的元素为止:
- // 依次输出text的每一行直至遇到第一个空白行为止
- for (auto it = text.cbegin();
- it != text.cend() && !it->empty(); ++it)
- cout << *it << endl;
我们首先初始化it令其指向text的第一个元素,循环重复执行直至处理完了text的所有元素或者发现某个元素为空。每次迭代时只要发现还有元素并且尚未遇到空元素,就输出当前正在处理的元素。值得注意的是,因为循环从头到尾只是读取text的元素而未向其中写值,所以使用了cbegin和cend来控制整个迭代过程。
某些对vector对象的操作会使迭代器失效
3.3.2节(第101页)曾经介绍过,虽然vector对象可以动态地增长,但是也会有一些副作用。已知的一个限制是不能在范围for循环中向vector对象添加元素。另外一个限制是任何一种可能改变vector对象容量的操作,比如push_back,都会使该vector对象的迭代器失效。9.3.6节(第353页)将详细解释迭代器是如何失效的。
谨记,但凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素。
3.4.1节练习
练习3.21:请使用迭代器重做3.3.3节(第105页)的第一个练习。
练习3.22:修改之前那个输出text第一段的程序,首先把text的第一段全都改成大写形式,然后再输出它。
练习3.23:编写一段程序,创建一个含有10个整数的vector对象,然后使用迭代器将所有元素的值都变成原来的两倍。输出vector对象的内容,检验程序是否正确。