C++ Primer 笔记之使用迭代器

使用迭代器

和指针不一样,获取迭代器不是使用取址符,有迭代器的类型同时拥有返回迭代器的成员。比如,这些类型都拥有名为begin和end的成员,其中begin成员负责返回第一个元素(或第一个字符)的迭代器。如有下述语句:

//由编译器决定b和e的类型

//b表示v的第一个元素,e表示v尾元素的下一位置

auto b = v.begin(), e = v.end(); //b和e的类型相同

end成员则负责返回指向容器(或string对象)“尾元素的下一位置(one past the end)”的迭代器,也就是说,该迭代器指示的是容器的一个本不存在的“尾后(off the end)”元素。这样的迭代器没什么实际含义,仅是个标记而已,表示我们已经处理完了容器中的所有元素。end成员返回的迭代器常被称作尾后迭代器(off-the-off iterator)或者简称为尾后迭代器(end iterator).特殊情况下

如果容器为空,则begin和end返回的是同一个迭代器。

Note:如果容器为空,则begin和end返回的是同一个迭代器,都是尾后迭代器。

一般来说,我们不清楚(不在意)迭代器准确的类型到底是什么。在上面的例子中,使用auto关键字定义变量b和e,这两个变量的类型也就是begin和end的返回值类型。

迭代器运算符

使用==和!=来比较两个合法的迭代器是否相等,如果两个迭代器指向的元素相同或者是同一个容器的尾后迭代器,则他们相等;否则就说这两个迭代器不相等。

*iter 返回迭代器iter所指元素的引用

iter->mem 解引用iter并获取该元素的名为mem的成员,等价于                            (*iter).mem

++iter 令iter指示容器中的下一个元素

--iter 令iter指示容器中的上一个元素

iter1 == iter2  判断两个迭代器是否相等(不相等),如果两个迭代器指示的是同一个元

iter1 != iter2    素或者他们是同一个容器的尾后迭代器,则相等;反之,不相等

和指针类似,也能通过解引用迭代器来获取它所指示的元素,执行解引用的迭代器必须合法并确实指示着某个元素。

试图解引用一个非法迭代器或者尾后迭代器都是未被定义的行为。

举个例子,利用下标运算符把string对象的第一个字母改为了大写形式,下面利用迭代器实现同样的功能:

string s("some string");

if (s.begin() != s.end()) { //确保s非空

auto it = s.begin(); //it表示s的第一个字符

*it = toupper(*it); //将当前字符改成大写形式

本例和原来的程序一样,首先检查s是否为空,显然通过检查begin和end返回的结果是否一致就能做到这一点。如果返回的结果一样,说明s为空;如果返回的结果不一样,说明s不为空,此时s中至少包含一个字符。

我们在if内部,声明了一个迭代器变量it并把begin返回的结果赋给它,这样就得到了指示

s中第一个字符的迭代器,接下来通过解引用运算符将第一个字符更改为大写形式。和原来的程序

一样,输出结果将是:

Some string

将迭代器从一个元素移动到另外一个元素

迭代器使用递增(++)运算符来从一个元素移动到下一个元素。从逻辑上来说,迭代器的递增

和整数的递增类似,整数的递增是在整数值上“加1”,迭代器的递增是将迭代器“向前移动一个位置”。

Note: 因为end返回的迭代器并不实际指示某个元素,所以不能对其进行递增或解引用操作。

之前有一个程序把string对象中第一个单词改写为大写形式,现在利用迭代器及其递增运算符可以

实现相同功能:

//依次处理s的字符直至我们处理完全部字符或者遇到空白

for (auto it = s.begin(); it != s.end() && !isspace(*it); ++it)

*it = toupper(*it); //将当前字符改成大写形式

这个程序用的是迭代器,上面循环遍历s的字符直到遇到空白字符为止。

循环首先用s.begin的返回值来初始化it,意味着it指示的是s中的第一个字符(如果有的话)。

条件部分检查是否已达到s的尾部,如果尚未到达,则将it解引用的结果传入isspace函数检查是否遇到

空白。每次迭代的最后,执行++it令迭代器前移一个位置以访问s的下一个字符。

先解引用it,然后将结果传入toupper函数得到该字母对应的大写形式,再把这个大写字母重新赋值给

it所指示的字符。

关键概念:泛型编程

原来使用C或Java的程序员在转而使用C++语言之后,会对for循环中使用!=而非<进行判断有点儿奇怪,

比如上面的这个程序。C++程序员习惯性地使用!=,其原因和他们更愿意使用迭代器而非下标的原因一样:因为

这种编程风格在标准库提供的所有容器上都有效。

之前已经说过,只有string和vector等一些标准库类型有下标运算符,而并非全都如此。与之类似,

所有标准容器的迭代器都定义了==和!=,但是它们中的大多数都没有定义<运算符。因此,只要我们养成

使用迭代器和!=的习惯,就不用太在意用的到底是哪种容器类型。

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和常量指针差不多,能读取但不能修改它所指的元素值。相反,iterator的对象

可读可写。如果vector对象或string对象是一个常量,只能使用const_iterator;如果vector对象

或string对象是一个常量,只能使用const_iterator;如果vector对象或string对象不是常量,那么

既能使用iterator也能使用const_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

有时候这种默认的行为并非我们所要。如果对象需读操作而无须写操作的话最好使用常量类型。

为了便于专门得到const_iterator类型的返回值,C++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()中圆括号必不可少。该表达式的含义是先对it解引用,然后解引用的操作再执行

点运算符。如果不加圆括号,点运算符将由it来执行,而非it解引用的结果:

(*it).empty() //解引用it,然后调用结果对象的empty成员

*it.empty() //错误:试图访问it的名为empty的成员,但it是个迭代器,

//没有empty成员

上面第二个表达式的含义是从名为it的对象中寻找其empty成员,显然it是一个迭代器,

它没有哪个成员是叫empty的,所以第二个表达式将发生错误。

为了简化上述表达式,C++语言定义了箭头运算符(->).箭头运算符把解引用和成员访问

两个操作结合在一起,也就是说,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来控制

整个迭代过程。

WARNNING: 谨记,但凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素

iter + n 迭代器加上一个整数值仍得一个迭代器,迭代器指示的新位置与原来相比向前移动了若干个

元素。结果迭代器或者指示容器内的一个元素,或者指示容器尾元素的下一位置

iter - n 迭代器减去一个整数值仍得一个迭代器,迭代器指示的新位置与原来相比向后移动了若干个

元素。结果迭代器或者指示容器内的一个元素,或者指示容器尾元素的下一位置

iter1 += n 迭代器加法的复合赋值语句,将iter1加n的结果赋给iter1

iter1 -= n 迭代器减法的复合赋值语句,将iter1减n的结果赋给iter1

iter1 = iter2 两个迭代器相减的结果是它们之间的距离,也就是说,将运算符右侧的迭代器向前

移动差值个元素后将得到左侧的迭代器。参与运算的两个迭代器必须指向的是同一

个容器中的元素或者尾元素的下一位置

>,>=,<,<= 迭代器的关系运算符,如果某迭代器指向的容器位置在另一个迭代器所指位置之前,

则说前者小于后者。参与运算的两个迭代器必须指向的是同一个容器中的元素或者

尾元素的下一位置。

时间: 2024-10-10 00:45:40

C++ Primer 笔记之使用迭代器的相关文章

C++ Primer笔记12_运算符重载_递增递减运算符_成员访问运算符

1.递增递减运算符 C++语言并不要求递增递减运算符必须是类的成员.但是因为他们改变的正好是所操作对象的状态,所以建议设定为成员函数. 对于递增与递减运算符来说,有前置与后置两个版本,因此,我们应该为类定义两个版本的递增与递减运算符. 问题来了,程序是如何区分前置和后置呢?因为都是++和-- 为了解决这个问题,后置版本的递增递减运算符接受一个额外的(不被使用)int类型的形参.当我们使用后置运算符时,编译器为这个形参提供一个值为0的实参.这个形参唯一的作用就是区分前置和后置运算符函数. 因为不会

重读C++ Primer笔记

C++ Primer 5E 有符号和无符号 无符号类型和有符号类型混合运算时,有符号数会被提升至无符号类型,如果其值为负责会产生错误. int main() { unsigned int u = 10; int i = -42; std::cout<<u+i<< std::endl; // 4294967264 if sizeof(int)==4 return 0; } 列表初始化 列表初始化过程不允许损失数据类型精度,所以下面代码中的两行无法通过编译 int main() { d

C++ Primer笔记6_STL之泛型算法

1.泛型算法: 大多数算法定义在头文件algorithm中,标准库还在头文件numeric中定义了一组数值泛型算法 只读算法: 举例: find函数用于找出容器中一个特定的值,有三个参数 int val = 10;//val为我们需要查找的值 auto result = find(vec.begin(), vec.end(), val): cout << "The value "<< val << (result == vec.end() ? &qu

C++ Primer笔记7_STL之关联容器

关联容器 与顺序容器不同,关联容器的元素是按关键字来访问和保存的.而顺序容器中的元素是按他们在容器中的位置来顺序保存的. 关联容器最常见的是map.set.multimap.multiset map的元素以键-值[key-value]对的形式组织:键用作元素在map中的索引,而值则表示所存储和读取的数据. set仅包含一个键,并有效的支持关于某个键是否存在的查询. pair类型 首先介绍下pair,pair定义在utility头文件中,一个pair保存两个数据成员,类似容器,pair是一个用来生

C++ Primer 笔记 第三章

C++ Primer 第三章 标准库类型 3.1using声明 例: using namespace atd; using std::cin; 3.2string类型 初始化方式 string s1 默认构造函数,s1为空串 string s2(s1) 将s2初始化为s1的一个副本 string s3(“value”) 将s3初始化为一个字符串的副本 string s4(n, 'c') 将s4初始化为字符'c'的n个副本 getline读取整行文本 getline接受两个参数:一个是输入流对象和

C++ Primer笔记5_STL之顺序容器

1.STL(Standard Template Library) 标准模板库.从根本上说,STL是一些"容器"的集合,这些"容器"有list, vector,set,map等,STL也是算法和其它一些组件的集合.这里的"容器"和算法的集合指的是世界上很多聪明人很多年的杰作.每一个C++程序员都应该好好学习STL.大体上包括container(容器).algorithm(算法)和iterator(迭代器),容器和算法通过迭代器可以进行无缝连接. 2

C++ Primer笔记8_动态内存_智能指针

1.动态内存 C++中,动态内存管理是通过一对运算符完成的:new和delete.C语言中通过malloc与free函数来实现先动态内存的分配与释放.C++中new与delete的实现其实会调用malloc与free. new分配: 分配变量空间: int *a = new int; // 不初始化 int *b = new int(10); //初始化为10 string *str = new string(10, ); 分配数组空间: int *arr = new int[10];//分配的

C++ Primer笔记2_四种类型转换_异常机制

1.类型转换 命名的强制类型转换: 有static_cast.dynamic_cast.const_cast.reinterpret_cast static_cast: 编译器隐式执行的任何类型转换都可以由static_cast完成 当一个较大的算术类型赋值给较小的类型时,可以用static_cast进行强制转换. 可以将void*指针转换为某一类型的指针 可以将基类指针强制转换为派生类指针,但是不安全. 无法将const转化为nonconst,这个只有const_cast才可以办得到 举例:

Lua学习笔记(七):迭代器与泛型for

1.迭代器与闭包 迭代器是一种支持指针类型的结构,它可以遍历集合的每一个元素.在Lua中我们常常使用函数来描述迭代器,每次调用该函数就返回集合的下一个元素. 迭代器需要保留上一次成功调用的状态和下一次成功调用的状态,也就是他知道来自于哪里和将要前往哪里.闭包提供的机制可以很容易实现这个任务.记住:闭包是一个内部函数,它可以访问一个或者多个外部函数的外部局部变量.每次闭包的成功调用后这些外部局部变量都保存他们的值(状态).当然如果要创建一个闭包必须要创建其外部局部变量.所以一个典型的闭包的结构包含