1. 迭代器的基础概念(iterator)
1.1 本质
迭代器能够用来遍历容器的对象,与能够遍历数组的指针类似,是广义指针。
1.2 作用:
- 能够让迭代器与算法不干扰的相互发展,最后又能无间隙的粘合起来。
- 重载了*,++,==,!=,=运算符。用以操作复杂的数据结构。
- 容器提供迭代器,算法使用迭代器。
1.3 产生由来
迭代器的设计来源于软件工程中对数据集合遍历的需求。在早期的编程中,特别是那些没有内建数据结构遍历功能的语言,程序员需要编写复杂的循环结构来访问容器中的元素,比如数组、列表或树。这种做法既繁琐又容易出错,特别对于动态大小的数据结构,如果直接操作下标会随着添加或删除元素而变得复杂。
为了简化这个过程并提供一种统一的访问机制,迭代器的概念应运而生。迭代器是一个独立的对象,它按照一定的规则(通常是顺序)逐个返回容器中的元素,开发者无需关心数据的具体存储方式。通过迭代器,可以方便地对各种数据结构进行遍历,提高了代码的灵活性和可维护性。迭代器模式也是设计模式的一种,体现了“隔离关注点”的原则。
简言之,用迭代器遍历比用下标访问访问(就是用[ ]来访问)更通用。因为大多数容器并不是数组实现的,不能用[ ]来访问容器元素,因此c++就需要迭代器这种更加普遍,更加通用的工具来提高效率。
1.4 类型分类
迭代器属于一种数据类型,就像是c语言的内置类型有整型(整型又分为整型int,短整型short,长整型long),浮点型(浮点型又分为浮点float,双精度浮点型double,长精度浮点型long double),类似的,他也有好几种类型——五种:输入迭代器,输出迭代器,前向迭代器,双向迭代器,随机迭代器。
他支持++,--,>,<,>=,<=,==,!= 这几种运算,注意这些运算只是针对迭代器的位置关系(如:两个迭代器相减,得到的是两个迭代器之间的距离),但这些类型的性质有所不同。
以上是他们所支持的运算,但是>,<,>=,<=要自己自定义迭代器去实现。
1.5 迭代器的操作(上面的运算)
与容器相同,迭代器有公共的接口(接口(Interface)是一种抽象类型,用于定义一组方法或属性,但不提供这些方法的具体实现。它是一个合约,规定了类必须实现的方法,以确保不同类之间能够以统一的方式进行交互。):
如果一个迭代器执行某个操作,那么想要实现相同操作的迭代器对于这个操作的实现方式都相同。举个栗子来帮助你的理解吧,标准容器类型上的所有迭代器都允许我们访问容器中的元素(并得到他具体的值),而所有迭代器都是通过解引用运算符来实现这个操作的。类似的,标准库容器的所有迭代器都定义了递增运算符,从当前元素移动到下一个元素。如下有具体的介绍:
表二的运算符只能应用于string,vector,deque和array的迭代器,我们不能把他们用于任何其它类型的迭代器.
1.6 迭代器的范围
一个迭代器的范围(iterator range)由一对迭代器表示,两个迭代器分别指向同一个容器中的第一个元素以及最后一个元素的后面一个位置。这两个迭代器通常被称为begin和end,但也有的地方叫做first和last——有误导(last通常表示最后一个元素,但这里不是指的最后一个元素,有的时候会因为这个被误导,大家注意一下),他们标记了迭代器的一个范围,范围中的元素是first表示的元素到last(但不包括last)之间的所有元素。数学表达为左闭右开,其数学描述为[begin,end)。
注意点:
· begin和end都必须指向同一个容器;
· begin和end能够指向同一个元素位置,但是不能指向begin之前的元素位置,可以指向最后一个元素之后的元素位置;
· end不在bengin之前
· begin与end相等,则范围为空;
· begin与end不相等,则范围至少包括一个元素,且begin指向该范围中第一个元素;
·我们可以对begin进行若干次递增,使得begin==end;
这样就意味着我们用一个循环来处理一个元素的范围,循环是安全的;
while (begin != end)
{
*begin = val; // begin指向一个元素
begin++; // 移动迭代器,获取下一个元素
}
相关解释:
给定构成一个合法范围的迭代器begin和end,若begin==end,则范围为空。在此情况下,我们应该退出循环。如果范围不为空,begin指向此非空范围的一个元素。因此,在while循环体中,可以安全地解引用begin,因为begin必然指向一个元素。最后,由于每次循环对begin递增一次,我们确定循环最终会结束。
2. 迭代器的使用
这不得上代码揭开迭代器的神秘面纱
2.1 定义方法
迭代器变量定义使用iterator关键字,形式为:容器<类型>:——iteaator 变量名
(如:std::vector<int>::iterator a、vector<int>::iterator a、auto a)。
2.2 具体演示
插入名称空间一丢丢知识
在演示之前,我们经常在看别人的题解的过程中会发现:有的人定义迭代器直接是vector<int> iterator a,但有的人是std::vector<int> std::iterator b,a和b本质上其实是一样的,为什么要加上std::这个看着很恐怖的东西呢?
说到这,我先给大家铺垫点std命名空间的知识,在日常写c++代码的时候,我们经常会看到在头文件之后会有一句"using namespace std"——这是干嘛的呢,有什么意思呢?
using namespace std,是C++编程中的一个指令,它的作用是将标准库命名空间std中的所有名称导入到当前代码文件的全局作用域中。如果不加这句话,在运行过程中不加std::的cin,cout,endl,创建迭代器等等语句都会报错,明明加了using namespace std的语句如此方便,那么为什么有的人还是会选择不加using namespace std,而选择更麻烦的需要在标准库中的函数,变量,类加上std::前缀?
using namespace std展开,标准库就全部暴露出来了,如果我们定义跟标准库重名的类型/对象/函数,就存在冲突问题。该问题在日常练习中很少出现,但是项目开发中代码较多、规模大,就很容易出现。所以建议在项目开发中使用,像std::cout这样使用时指定命名空间 + using std::cout展开常用的库对象/类型等方式,但我们在日常练习中,是没有必要的。
所以当你看到以下代码的时候不要慌,理解起来也不难的呦!
实践(实践才能记得更牢固哦)
#include <iostream>
#include <vector>
int main(void){
std::vector<int> num;
int x;
std::cin>>x; //输入一个值x
num.push_back(x); //将x加入容器
std::vector<int>::iterator pb; //定义迭代器
pb=num.begin(); //让迭代器指向容器的第一个数
std::cout<<*pb; //输出迭代器指向的值——通过解引用的方式获取
std::cout<<std::endl;
return 0;
}
简便写法:
#include <iostream>
#include <vector>
using namespace std;
int main(void){
int x;
cin>>x;
vector<int> num;
num.push_back(x);
vector<int>::iterator a;
a=num.begin();
cout<<*a<<endl;
return 0;
}
2.3 详解常用迭代器begin和end操作
begin和end的具体介绍这两个迭代器最常见的用途就是包含容器中所有元素。
begin和end的其他类型:
带r的版本返回反向迭代器;c++新标准引入的带c的版本返回const迭代器。
下面for循环中的终止条件用</!= 都是可行的!!!
2.3.1 普通版的原汁原味版的begin与end使用
begin用来指向容器中的第一个元素,end用来指向容器中最后一个元素的后面一个元素。
具体使用:
#include <iostream>
#include <vector>
using namespace std;
int main(void){
vector<int> num={0,1,2,3,4,5};
vector<int>::iterator i;
for(i=num.begin();i<num.end();i++){
cout<<*i<<" ";
}
return 0;
}
运行结果:
#include <iostream>
#include <vector>
using namespace std;
int main(void){
vector<int> num={0,1,2,3,4,5};
// vector<int>::iterator i;
// for(i=num.begin();i!=num.end();i++){
// cout<<*i<<" ";
// }
// 这里可以利用c++引入的新标准来简化以下,不用记住用的具体是什么迭代器
for(auto i=num.begin();i!=num.end();i++){
cout<<*i<<" ";
}
return 0;
}
运行结果:
2.3.2 加了r(reverse)又是用来干嘛的呢?
rbegin指向容器最后一个元素,rend指向容器的第一个元素前面一个元素.
#include <iostream>
#include <vector>
using namespace std;
int main(void){
vector<int> num={0,1,2,3,4,5};
vector<int>::reverse_iterator i;
for(i=num.rbegin();i!=num.rend();i++){
cout<<*i<<" ";
}
return 0;
}
运行结果:
#include <iostream>
#include <vector>
using namespace std;
int main(void){
vector<int> num={0,1,2,3,4,5};
// vector<int>::reverse_iterator i;
// for(i=num.rbegin();i!=num.rend();i++){
// cout<<*i<<" ";
// }
// 这里可以利用c++引入的新标准来简化以下,不用记住用的具体是什么迭代器
for(auto i=num.rbegin();i!=num.rend();i++){
cout<<*i<<" ";
}
return 0;
}
运行结果:
2.3.3 加了const究竟有什么不同呢?
我们都知道const是常量的意思,那么const_iterator型迭代器不可以修改所指向容器的值,但还是可以改变迭代器的指向。与相配对的cbegin()/cend(),它们返回的都是常量迭代器,不能通过常量迭代器来修改容器中的内容,并且如果你尝试通过它来改变这些元素的值,编译器会报错。
#include <iostream>
#include <vector>
using namespace std;
int main(void){
vector<int> num={0,1,2,3,4,5};
vector<int>::const_iterator i;
for(i=num.cbegin();i<num.cend();i++){
cout<<*i<<" ";
}
return 0;
}
运行结果:
#include <iostream>
#include <vector>
using namespace std;
int main(void){
vector<int> num={0,1,2,3,4,5};
// vector<int>::const_iterator i;
// for(i=num.cbegin();i<num.cend();i++){
// cout<<*i<<" ";
// }
// 这里可以利用c++引入的新标准来简化以下,不用记住用的具体是什么迭代器
for(auto i=num.cbegin();i!=num.cend();i++){
cout<<*i<<" ";
}
return 0;
}
运行结果:
2.3.4 crbegin以及crend
#include <iostream>
#include <vector>
using namespace std;
int main(void){
vector<int> num={0,1,2,3,4,5};
vector<int>::const_reverse_iterator i;
for(i=num.crbegin();i!=num.crend();i++){
cout<<*i<<" ";
}
return 0;
}
运行结果:
#include <iostream>
#include <vector>
using namespace std;
int main(void){
vector<int> num={0,1,2,3,4,5};
// vector<int>::iterator i;
// for(i=num.crbegin();i!=num.crend();i++){
// cout<<*i<<" ";
// }
// 这里可以利用c++引入的新标准来简化以下,不用记住用的具体是什么迭代器
for(auto i=num.crbegin();i!=num.crend();i++){
cout<<*i<<" ";
}
return 0;
}
运行结果:
在 STL(标准模板库)的上下文中,const_iterator
及其对应的常量(cbegin()
和 cend()
)是用来实现对容器元素的不同级别的访问权限。
list<string> a = { "Milton","Shakespeare","Austen" };
auto it1 = a.begin(); // list<string>::iterator
auto it2 = a.rbegin(); // list<string>::reverse_iterator
auto it3 = a.cbegin(); // list<string>::const_iterator
auto it4 = a.crbegin(); // list<string>::const_reverse_iterator
// 显式指定类型
list<string>::iterator it5 = a.begin();
list<string>::const_iterator it6 = a.begin();
// 是iterator还是const_iterator依赖于a的类型
auto it7 = a.begin(); // 仅当a是const时,it7是const_iterator
auto it8 = a.cbegin(); // it8是const_iterator
3. 容器操作可能使迭代器失效
3.1 迭代器失效
向容器中添加元素或删除元素的操作可能会使在原来的容器中指向容器元素的指针、引用或迭代器失效。一个失效的指针、引用或迭代器将不再表示任何元素。使用失效的指针、引用或迭代器是一种严重的程序设计错误,所以使用失效的迭代器很可能引起与使用未初始化指针一样的问题。
在向容器添加元素后:
对于vector或string,且存储空间被重新分配,则指向容器的迭代器、指针和引用都会失效。如果存储空间未重新分配,指向插入位置之前的元素的迭代器、指针和引用仍有效,但指向插入位置之后元素的迭代器、指针和引用将会失效。
对于deque,插入到除首尾位置之外的任何位置都会导致迭代器、指针和引用失效。如果在首尾位置添加元素,迭代器会失效,但指向存在的元素的引用和指针不会失效。
对于list和forward_list,指向容器的迭代器(包括尾后迭代器和首前迭代器)、指针和引用仍有效。
在容器中删除元素后:
指向被删除元素的迭代器、指针和引用会失效,这应该不会令人惊讶。毕竟,这些元素都已经被销毁了。但是:
对于list和forward_list,指向容器其他位置的迭代器(包括尾后迭代器和首前迭代器)、引用和指针仍有效。
对于deque,如果在首尾之外的任何位置删除元素,那么指向被删除元素外其他元素的迭代器、引用或指针也会失效。如果是删除deque的尾元素,则尾后迭代器也会失效,但其他迭代器、引用和指针不受影响;如果是删除首元素,这些也不会受影响。
对于vector和string,指向被删元素之前元素的迭代器、引用和指针仍有效。注意:当我们删除元素时,尾后迭代器总是会失效。
建议:管理迭代器
当你使用迭代器(或指向容器元素的引用或指针)时,最小化要求迭代器必须保持有效的程序片段是一个好的方法。
由于向迭代器添加元素和从迭代器删除元素的代码可能会使迭代器失效,因此必须保证每次改变容器的操作之后都正确地重新定位迭代器。这个建议对vector、string和deque尤为重要。
参考:
原文链接:https://blog.youkuaiyun.com/2301_78933228/article/details/136343418
原文链接:https://blog.youkuaiyun.com/Qiuhan_909/article/details/129939184