Th2.9:迭代器iterator详述

 本博客将记录:迭代器iterator这个知识点的笔记!在我们之前的学习过程中一般都把迭代器理解为一种特殊形式的指针,因为迭代器也有解引用的操作 *it ...,奔着想成为一名优秀的cpp开发人员的目的,那么今天我就来详细地学习了解下迭代器这个知识点。今天主要是学习迭代器的演绎、失效分析以及弥补、实战的内容。

 

今天总结的知识分为以下7个点:

  一、迭代器简介
  二、容器的迭代器类型
  三、迭代器begin()/end()的操作,以及反向迭代器rbegin()/rend()的操作
  四、迭代器运算符 
  五、const_iterator 迭代器

        (5.1)介绍C++11新引入的cbegin()和cend()这2个迭代器函数的操作
  六、迭代器失效问题

  七、范例演示

        (7.1)用迭代器遍历sring类型数据

        (7.2)vector容器的常用操作与内存释放

  一、迭代器简介

        迭代器:是一种用来遍历容器内元素的 数据类型,这种数据类型从直观上看有点类似于指针,我们大可以把它理解为,迭代器就是用来指向容器中的某个元素的。 

通过使用迭代器,我们既可以读容器中的元素值,也可以修改某个迭代器所指向的元素的值

前面我们回顾了string和vector这2种容器,都可像使用一维数组那样去读取or修改其元素,也即用[]符号来do。

string str = "lzf";
cout << str[0] << endl;//str[0] == 'l';//读取
str[0] = 't';//str[0] == 't';//修改
cout << str[0] << endl;
vector<int>  vec{ 1,3,5,7 };
cout << vec[0] << endl;//vec[0] == 1;//读取
vec[0] = 1001;//vec[0] == 1001;//修改
cout << vec[0] << endl;

但是,后面我们还会继续回顾更多的容器类型(比如list/map),这些容器类型有些支持[]符号去读写相应的元素值,有些则不支持。因此,我们就可以使用迭代器了,因为对于每一种容器而言,都具有其本身自带的迭代器。访问容器的最常用的方式就是用迭代器来do!

string str = "lzf";
string::iterator itStr = str.begin();//string类型的开始迭代器
cout << *itStr << endl;//<===> str[0] == 'l';//读取
*itStr = 't';//<===> str[0] == 't';//修改
cout << *itStr << endl;
vector<int>  vec{ 1,3,5,7 };
vector<int>::iterator itVec = vec.begin();//vector<int>类型的开始迭代器
cout << *itVec << endl;//<===> vec[0] == 1;//读取
*itVec = 1001;//<===> vec[0] == 1001;//修改
cout << *itVec << endl;


  二、容器的迭代器类型

定义容器迭代器的一般格式:

迭代器对应的容器的类型::iterator 普通迭代器变量名;//普通迭代器可读可写 
用法举例:vector<int>::iterator it = v.begin() | it = v.end();
迭代器对应的容器的类型::reverse_iterator 反向迭代器变量名;//反向迭代器可读可写
用法举例:vector<int>::reverse_iterator it = v.rbegin() | it = v.rend();
迭代器对应的容器的类型::const_iterator 常量迭代器变量名;//常量迭代器只读不可写 
用法举例:vector<int>::const_iterator it = v.begin() | it = v.end();
//下面还是以vector这种容器来演示迭代器的类型
vector<int> vec {10,20,30,};
vector<int>::iterator it;//it就是个迭代器,是对应vector<int>这种容器的迭代器

        我们在理解的时候,可以直接把vector<int>::iterator理解为一种变量的类型,这种类型是专门用于迭代器的,当我们使用这个类型去定义一个变量时,这个变量(比如这里的it),就是个迭代器,然后在使用迭代器时,你可以直接像使用指针一样去用即可!

  三、迭代器begin()/end()的操作,以及反向迭代器rbegin()/rend()的操作:

        每个容器中都会内置好了正向开头迭代器begin()和正向末尾迭代器end(),反向开头迭代器rbegin()和反向末尾迭代器rend()这4种函数。这些函数让我们可以访问容器中的all元素。

正向迭代器:begin()和end()这2个函数的返回值就是对应位置处的迭代器。

(当需要在容器中从前往后遍历访问元素值时,就使用这种普通的正向迭代器)

反向迭代器:rbegin()和rend()这2个函数的返回值也是对应位置处的迭代器。

(当需要在容器中从后往前遍历访问元素值时,就使用这种反向(或说逆向)迭代器)

vector<int> iv = { 100,200,300,400,500 };
vector<int>::iterator it = iv.begin();//定义了一个迭代器(变量),类型是 vector<int>
while (it != iv.end()) {
    cout << *it << "\t";
    //一开始it指向iv[0]这个位置的迭代器,解引用*it就是取其值的意思
    //然后不断遍历下去知道走到了iv的last一个位置的下一个位置处
    //这好比你遍历访问一个数组int a[4]{1,2,3,4,};for循环中的最后一个i肯定是遍历到4了才推出循环的
	it++;//指向下一个位置处的迭代器
}
cout << "\n-------------------------------" << endl;
//reverse_iterator是反向迭代器
vector<int>::reverse_iterator rit = iv.rbegin();
while (rit != iv.rend()) {
	cout << *rit << "\t";
	rit++;//指向下一个位置处的迭代器
}

运行结果:

 

 可能有些小伙伴没有搞懂begin()和rbegin()有啥区别,end()和rend()有啥区别,那么现在我就画张图帮大家搞clear:

 补充一下用for_each()标准遍历算法配合rbegin(),rend()的使用代码:

vector<int> iv = { 100,200,300,400,500 };
void print(const int& val) { cout << val << "\t";}
//用正向迭代器输出vector容器对象所存储的all元素
for_each(iv.begin(), iv.end(), print);//100,200,300,400,500
cout << "\n-------------------------------" << endl;
//用反向迭代器输出vector容器对象所存储的all元素
for_each(iv.rbegin(), iv.rend(), print);//500,400,300,200,100
cout << endl;

 运行结果同上!

此外,迭代器的操作中还有2个需要格外注意的事项:

 注意事项1:当一个容器为空时(也即一个元素都没有时)有:begin()==end(), rbegin()==rend()

vector<int> iv;//空的vector容器
if (iv.begin() == iv.end())cout << "iv.begin() == iv.end()" << endl;
if (iv.rbegin() == iv.rend())cout << "iv.rbegin() == iv.rend()" << endl;

 运行结果:

 注意事项2:用迭代器遍历容器时,有2种方法

vector<int> iv = { 100,200,300,400,500 };
void print(const int& val) { cout << val << "\t";}

方法一、用标准算法库<algorithm>中所提供的for_each()来do遍历操作(推荐使用)
//正向遍历输出
for_each(iv.begin(), iv.end(), print);//100,200,300,400,500
cout << "\n-------------------------------" << endl;
//反向遍历输出
for_each(iv.rbegin(), iv.rend(), print);//500,400,300,200,100
cout << endl;
方法二、用for循环来do遍历操作
//正向遍历输出
for(vector<int>::const_iterator it = iv.begin();it!=iv.end();it++){
    cout<<*it<<"\t";100,200,300,400,500
}
cout<<endl;
//反向遍历输出
for(vector<int>::reverse_iterator rit = iv.rbegin();rit!=iv.rend();rit++){
    cout<<*rit<<"\t";//500,400,300,200,100
}
cout<<endl;

 (对于反向的迭代器来说,rit++意味着是从后往前去加加,如果你误以为这里要写为rit--的话,那么请你再仔细看一下我在上面所画的那一张图!看完之后相信你就能完全理解反向迭代器是如何遍历的了!)

   四、迭代器运算符(介绍常用的)

1)*it:对迭代器do解引用*iterator,返回迭代器iter所指向的元素的引用。

我们必须要保证it这个迭代器指向的是一个有效的容器元素,不能指向end(),因为end()指向的是末尾元素的下一个位置处,也即end()指向的是一个不存在的元素。

vector<int>vec{1,2,3,};
vector<int>::iterator it = vec.end();
cout << *it << endl;//报错!不能这么干的!

2)++it | it++:可以让迭代器指向容器中的下一个元素,若it已经指向end()时你不能再++了(因为再++就越界了)

vector<int>vec{1,2,3,};
vector<int>::iterator it = vec.begin();
cout << *it << endl;//1
it++;
cout << *it << endl;//2
++it;
cout << *it << endl;//3

3)--it | it--:可以让迭代器指向容器中的上一个元素,若it已经指向begin()时你不能再--了(因为再--就越界了)

vector<int>vec{1,2,3,};
vector<int>::iterator it = vec.end();
it--;
cout << *it << endl;//3
--it;
cout << *it << endl;//2
--it;
cout << *it << endl;//1

4)it1 == it2,it1 != it2 :判断两个迭代器是否相等

(若2个迭代器指向的是同一个元素,则相等,否则就不等)

vector<int>vec{1,2,3,};
vector<int>::iterator it1 = vec.begin();
vector<int>::iterator it2 = vec.begin();
if (it1 == it2)cout << "it1 == it2" << endl;
if (it1 != it2)cout << "it1 != it2" << endl;
//result:
//"it1 == it2"

5)但迭代器指向的元素类型是类的对象or结构体对象时,有2种操作:

①(*it)所指向的元素的类型就是定义容器时<里面的类型>,因此对(*it)可用.符号do事情

②it可直接理解为指向某容器元素的指针,因此对it可用->符号do事情

比如:

class Person {
public:
	int m_Age;
	string m_Name;
	Person():m_Age(0),m_Name(""){}
	Person(const int& age,const string& name) :m_Age(age), m_Name(name) {}
};

Person p(21, "lzf");
vector<Person> vp;
vp.push_back(p);
vector<Person>::iterator it = vp.begin();
//这里的*it <===>指向的是 Person 的类型
//<里面的是啥类型> *it就会指向啥类型
cout << (*it).m_Age << endl;//21
cout << (*it).m_Name << endl;//"lzf"
//可以像对象指针那样去调用一个类的成员or成员函数
cout << it->m_Age << endl;//21
cout << it->m_Name << endl;//"lzf"

  五、const_iterator 迭代器:

(类似于常量指针,也即指针所指向地址空间所存储的值的不可变的,但是指针的指向也即指针的地址值是可变的)

        const表示,迭代器所指向的元素的值是不可改变的也即const_iterator只能用以读取容器中的元素,而不能通过这个迭代器来改写该容器中的元素的值。(当迭代器是const_iterator时,begin()和end()返回的就是其对应的const迭代器了)

(5.1)介绍C++11新引入的cbegin()和cend()这2个迭代器函数的操作

C++11新引入了2种迭代器的函数,分别是cbegin()和cend(),这2种迭代器函数与begin()和end()是极为类似的,只不过cbegin和cend()这2种迭代器的返回值是常量const的迭代器。

vector<int> vec{818,288,382,};
vector<int>::const_iterator it = vec.cbegin();
for (; it != vec.cend(); it++) {
	cout << *it << endl;//818,288,382,
}// 382,288,818

当然,有cbegin和cend(),也就会有crbegin和crend()了,这2个函数其实和cbegin和cend()差不太多,只不过前者是反向迭代器,同时其函数的返回值是const常量的迭代器而已(简称为常量反向 迭代器)。

vector<int> vec{818,288,382,};
vector<int>::const_reverse_iterator it = vec.crbegin();
for (; it != vec.crend(); it++) {
	cout << *it << endl;// 382,288,818
}

总结,常量迭代器有2个注意事项:

注意事项①:并不是说该迭代器不可以指向别的元素的位置,而是说该迭代器所指向的元素的值是不变的。

//Person的定义同上述代码
Person p1(21, "lzf");
Person p2(22, "lyf");
vector<Person> vp;
vp.push_back(p1);
vp.push_back(p2);
vector<Person>::const_iterator it = vp.begin();
++it;//可见,const的迭代器指向的位置是可变的!
cout << it->m_Age << endl;//22
cout << it->m_Name << endl;//"lyf"
it->m_Age = 888;//×报错!因为it是const的迭代器,其所指向的元素的值是不可变的!

注意事项②:对于const 类的容器对象,必须要使用const_iterator常量迭代器来访问,否则就报错!

Person p1(21, "lzf");
Person p2(22, "lyf");
const vector<Person> vp{p1,p2};//常量容器对象(容器对象中的元素都是const的!不可以变的!)
vector<Person>::const_iterator it = vp.begin();//就配合常量迭代器使用
//if写为vector<Person>::iterator it = vp.begin();//×!报错!
++it;
cout << it->m_Age << endl;
cout << it->m_Name << endl;
//result:
// 22 "lyf"

  六、迭代器失效问题

在2.8小节中我们回顾到,C++11引入了范围for语句让我们可用更少的代码量来对一个序列进行遍历,那么范围for语句的底层原来是啥呢?其实范围for语句的底层原理就是迭代器遍历。

vector<int> vec{818,288,382,};
for (auto x : vec) {
	cout << x << "\t";
}
//<===> 等价于
cout << "\n--------------------------------" << endl;
for (auto beg = vec.cbegin(), end = vec.cend(); beg != end; beg++) {
	cout << *beg << "\t";
}
cout << endl;

运行结果:

vector<int> vec {1,2,3,4,5,};
auto beg = vec.begin();
auto end = vec.end();
while (beg != end) {
	cout << *beg << endl;
	vec.insert(beg, 88);//×!insert:第一个参数为插入位置,第二个参数为插入的元素
	//咱们这样一插入元素就会导致该vector容器的迭代器begin()失效,程序崩溃!
    //同理,你遍历该序列时去删除其中的迭代器,这就会导致迭代器失效,程序崩溃!    
    vec.erase(beg);//×!
	//这样一运行就会导致程序崩溃的!!!
	break;//出现这种状况我们最好的应对措施是+个break语句来结束这个遍历循环语句
	beg++;
}

注意: 对于一个序列用for/while循环做遍历时,在循环的过程中,一般情况下你都不要修改这个序列的长度,否则很可能就会直接导致程序崩溃!(若不理解请看上面贴出来的代码)

下面再给出上述代码中的.erase()操作的正确做法:

vector<int> vec {1,2,3,4,5,};
auto beg = vec.begin();
while (beg != vec.end()) {//vec.end()这个end是随时更新的!
	beg = vec.erase(beg);//删除后会返回这个被删除迭代器的下一个迭代器位置
}
cout << vec.size() << endl;//0,因为我都把他们删除掉了!!!

//<===>
while (!vec.empty()) {
	vec.pop_back();
}
cout << vec.size() << endl;

总结:千万不要写出迭代器失效的代码,否则就会导致程序崩溃的问题! 

七、范例演示

        (7.1)用迭代器遍历sring类型数据

string str = "lzfyyds!";
auto itStrBegin = str.begin();
auto itStrEnd = str.end();
while (itStrBegin != itStrEnd) {
	//toupper函数:把all字母转换为大写
	*itStrBegin = toupper(*itStrBegin) ;//这里只是手痒痒想到用大写输出而已,你正常输出也ok
	cout << *itStrBegin << "\t";
	itStrBegin++;//写while时,千万别忘了++
}
cout << endl;

    (7.2)vector容器的常用操作与内存释放

当vector中我们存储的是指针类似时,有时候我们是手动地new对应的指针并添加到vector中的,那么此时会遇到内存释放的问题,如果手动在堆区开辟内存却不手动释放的话,就会造成内存泄露!!!

so请看以下代码,以学习如何将vector中手动在heap堆区开辟的内存释放掉,并能在今后用在别的容器相应的内存释放问题上。

#include<iostream>
#include<string>
#include<vector>
using namespace std;
class conf{
public:
	string iterName;
	string iterContent;
	conf():iterName(""), iterContent("") {}
	conf(string name,string content) :iterName(name), iterContent(content) {}
	~conf() {}
};
string getInfo(vector<conf*>& conflist,const string& pitem) {
	for (int i = 0; i < (int)conflist.size(); i++) {
		if ( conflist[i]->iterName == pitem) {
			return conflist[i]->iterContent;
		}
	}
	return "";
}
int main(void) {
	//以实战程序为例子
	//ServerName = 1区;//表示服务器的名称
	//ServerID = 100000;//表示服务器的ID
	string ServerName = "1区", ServerID = "100000";
	string SN = "ServerName", SD = "ServerID";
	conf* pconf1 = new conf(SN, ServerName);
	conf* pconf2 = new conf(SD, ServerID);
	vector<conf*> conflist{ pconf1,pconf2 };
	string pitem = "ServerName";
	string res = getInfo(conflist, pitem);
	cout << res << endl;//返回ServerName 对应的值
	
	//手动在堆区开辟的内存,必须要手动释放!是这段小代码可供学习的key点!!!
	vector<conf*>::iterator pos;
	//这几行代码是必须有的!
	for (pos = conflist.begin(); pos != conflist.end(); pos++) {
		(*pos)->iterName = "";
		(*pos)->iterContent = "";
		delete (*pos);//把这个对象的指针在堆区所存放的数据都delete掉了!		
	}
	conflist.clear();//这一行有没有都可以,因为OS会在你的程序结束后帮你回收掉!
    
    //像上面这样do之后,你就不需要手动一条条的delete了,省事省时
    //(因为很可能我们会new很多堆区空间,你总不能逐条地去释放把?对吧)
	//delete pconf1;
	//delete pconf2;
	system("pause");
	return 0;
}

 好,那么以上就是这一2.9小节我所回顾的内容的学习笔记,第2章end!!!

希望你能读懂并且消化完,也希望自己能牢记这些小小的细节知识点,加油吧,我们都在coding的路上~

  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Fanfan21ya

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值