[C++ 从入门到精通] 5.迭代器精彩演绎、失效分析及弥补、实战

简介: [C++ 从入门到精通] 5.迭代器精彩演绎、失效分析及弥补、实战

一. 迭代器简介

迭代器是一种遍历容器内元素的数据类型(比如读String中的每个字符、Vector中的每个元素)。这种数据类型有点像指针,我们可以理解为使用迭代器不仅能指向容器中的某个元素,而且还能修改某个迭代器所指向的元素值。

前面我们学习String字符串Vector容器的时候知道,想要访问里面的数据时,可通过下标运算符[]来访问String中的字符、Vector中的元素。但是在C++中,很少使用下标运算符[]这种方式来访问它们中的数据,比较常用的访问方式是本文中介绍的迭代器方式。

此外,在C++中类似Vector的其他容器(listmap)不一定都提供了[]这种方式来访问其数据,但是这些容器基本都提供了迭代器的方式来对他们的数据进行访问,从这个角度来看,使用迭代器来访问容器中的元素也更为普遍,所以大家有必要来学习迭代器的使用方法。


二. 容器的迭代器类型

1、迭代器格式:容器名<容器类型>::iterator 对象名

2、定义迭代器:对比定义vector容器

vector<int> iv = { 100,200,300 };       //定义容器iv
vector<int>::iterator iter;             //定义迭代器iter,也必须是vector<int>类型
vector<int>::reverse_iterator riter;    //定义反向迭代器riter,也必须是vector<int>类型

Ps:大家理解的时候,就把整个vector<int>::iterator理解成一个类型,当用这个类型定义一个变量(iter)时,这个变量就是个迭代器。


三. 迭代器begin()/end()操作,反向迭代器rbegin()/rend()操作

1、begin()/end():用来返回一个迭代类型(可以理解成返回一个迭代器)

begin():如果容器中有元素,则begin返回的迭代器,指向的是容器中的第一个元素

vector<int> iv = { 100,200,300 };    //定义容器iv
vector<int>::iterator iter;          //定义迭代器iter,也必须是vector<int>类型
iter = iv.begin();                   //相当于iter指向vector容器iv中的第0个元素100,即iv[0]


end():如果容器中有元素,则end返回的迭代器,指向的并不是容器中的末端元素,而是末端元素的下一位元素(不存在)。

vector<int> iv = { 100,200,300 };    //定义容器iv
vector<int>::iterator iter;          //定义迭代器iter,也必须是vector<int>类型
iter = iv.end();                     //相当于iter指向vector容器iv中的第末尾第2个元素的下一位,所以是不存在数据的(乱码)


③ 如果一个容器为空,则begin()/end()返回的迭代器相同,都为空。

vector<int> iv2;
vector<int>::iterator iterbegin = iv2.begin();   
vector<int>::iterator iterend = iv2.end(); 
if(iterbegin == iterend)
{
    qDeubg() << "容器iv2为空";       //条件成立
}

迭代器begin()/end()遍历容器中数据的方法:

vector<int> iv = { 100,200,300 };
for (vector<int>::iterator iter = iv.begin(); iter != iv.end(); iter++)
{
  cout << *iter << endl;    //后面讲*对迭代器中的容器取值
}

ps:end()实际是作为一个结束标记的作用,表明在for循环中,判断是否已经执行到最后一步,遍历完容器中的所有元素了。


实际工作中,可能需要使用迭代器从容器的末尾开始遍历数据,直到容器的首位数据,这时候就需要使用反向迭代器rbegin()/rend()操作了。

2、rbegin()/rend():反向迭代器

rbegin():返回一个反向迭代器,指向反向迭代器的第一个元素。


rend():返回一个反向迭代器,指向反向迭代器末尾元素的下一个位置。


③ 反向迭代器rbegin()/rend()遍历容器中数据的方法:

vector<int> iv = { 100,200,300 };
for (vector<int>::reverse_iterator riter = iv.rbegin(); riter != iv.rend(); riter++)
{
  cout << *riter << endl;
}


小结:

  • 迭代器操作函数begin()/end()指向容器中的位置关系如下:
  • 反向迭代器操作函数rbegin()/rend()指向容器中的位置关系如下:

四. 迭代器相关操作

示例:

vector<int> iv = { 100,200,300 };
for (vector<int>::iterator iter = iv.begin(); iter != iv.end(); iter++)
{
  cout << *iter << endl;    //后面讲*对迭代器中的容器取值
}

*iter:返回迭代器iter所指元素的引用(必需保证这个迭代器指向的是有效的容器元素,不能指向end())。

eg:

vector<int>::iterator iter = iv.end();
//cout << *iter << endl;                  //不可以,系统会直接崩溃报错
vector<int>::iterator iter1 = iv.begin();
cout << *iter1 << endl;                   //可以,指向容器iv的第0个元素100

++iter/iter++:让迭代器指向容器中的下一位元素,已经指向end()的时候不能在++

vector<int>::iterator iter = iv.end();
//iter++;                                 //不可以,系统会直接崩溃报错
vector<int>::iterator iter1 = iv.begin();
iter1++;                                  //可以,首位元素下走一位,指向容器iv的第1个元素200

--iter/iter--:让迭代器指向容器中的上一个元素,已经指向begin()的时候不能在--

vector<int>::iterator iter = iv.begin();
//iter--;                                 //不可以,系统会直接崩溃报错
vector<int>::iterator iter = iv.end();
iter--;
cout << *iter << endl;                    //输出容器末位元素300

insert(const_iterator _Where, _Ty&& _Val):为容器插入新值,第一个参数为插入位置,第二个参数为插入的元素。

vector<int> iv = { 100,200,300 };
auto beg = iv.begin();                   //auto根据iv.begin()自动推断beg为迭代器类型,相当于vector<int>::iterator beg 
iv.insert(beg, 50);

⑤ 迭代器引用结构struct中的成员数据:引用*iter或指针iter

struct student{ int num; };
//1.结构数据存入到容器中
vector<student> iv;
student stu;
stu.num = 10;
iv.push_back(stu);              //赋值操作,iv和stu是两个内存地址
//2.迭代器读取容器中的结构数据
vector<student>::iterator iter;
iter = iv.begin();              //或两句并一句:auto iter = iv.begin();
cout << (*iter).num << endl;    //100,*iter表引用
cout << iter->num << endl;      //100, iter表指针

强调*iter表引用,iter表指针。


五. const iterator迭代器

const iterator:表示迭代器指向的元素值不能改变,而不是表示这个迭代器本身不能改变(本身可以不断指向下一个元素)。

① 特性:只能从容器中读元素,不能通过迭代器改写容器中的元素值,感觉起来更像常量指针。

② 示例:

vector<int> iv = { 100,200,300 };
for (vector<int>::const_iterator iter = iv.begin(); iter != iv.end(); iter++)
{
  cout << *iter << endl;      //可以正常读容器中元素
  //*iter = 4;                //报错
}

③ 适用场合:

  • 容器是常量const的时候,对应的迭代器也必须是const iterator
  • 容器中的元素值不想被迭代器改写,可以使用const iterator
5.1. cbegin()和cend()操作

cbegin()/cend()C++11引用的新函数,和begin()/end()类似,只不过cbegin()/cend()返回的常量迭代器(指向的元素值不能被改变)。

vector<int> iv = { 100,200,300 };
for (auto iter = iv.cbegin(); iter != iv.cend(); iter++)
{
  cout << *iter << endl;      //可以正常读容器中元素
  //*iter = 4;                //报错,cbegin返回的是常量迭代器,*iter指向的元素值被赋值改变
}

ps:即使未定义const iterator,但是通过cbegin()/cend()返回常量迭代器仍不可以改变容器中的值。不考虑容器是否为const的时候,两者的作用相类似(容器中的元素值不想被迭代器改写)。


六. 迭代器失效及弥补

示例: 范围for语句和迭代器iterator遍历容器中的数据

vector<int> iv = { 100,200,300 };
//范围for
for (auto vecitem : iv){
    iv.push_back(555);      //加上这句后,范围for语句遍历容器数据失效,程序崩溃
  cout << vecitem << endl;
}
//迭代器iterator
for (auto iter = iv.begin(); iter != iv.end(); iter++){
    iv.push_back(555);       //加上这句后,迭代器遍历容器数据失效,程序崩溃
  cout << *iter << endl;
}

小结1:

  • 范围for语句遍历容器中数据 == 迭代器操作遍历容器中数据,两者等价。
  • 在迭代器遍历容器内元素的过程中,使用往容器中增加元素或删除容器中的元素等操作,会使指向容器元素的指针、引用、迭代器失效,轻则程序乱码,重则程序崩溃。参考:范围for进一步讲解

解决:

如果我们在for循环或迭代器遍历容器数据的时候,一定要在容器中插入/删除某个数据时,记得操作完之后要立马break出去,然后在重新遍历容器数据。

vector<int> iv = { 100,200,300 };
for (auto iter = iv.begin(); iter != iv.end(); iter++){
    iv.push_back(555);      
  break;
}
for (auto iter = iv.begin(); iter != iv.end(); iter++){
    //...
}

小结2: 最明智防止迭代器失效的方法:break

6.1. 灾难程序演示

大家在程序运行结束之前,可能会习惯释放掉容器中的数据,有人可能会写出如下代码进行释放:

vector<int> iv = { 100,200,300 };
//...
for (auto iter = iv.begin(); iter != iv.end(); iter++)
{
  iv.erase(iter);      //erase函数,移除iter位置上的元素,返回下一个位置元素
}

运行直接崩溃:迭代器失效

正确的释放方法:

vector<int> iv = { 100,200,300 };
//...
//释放方式一:
auto iter = iv.begin();
while (iter != iv.end())
{
  iter = iv.erase(iter);
}
//释放方式二:首先判断容器是否为空,个人更常用
while (!iv.empty())
{
    auto iter = iv.begin();
  iter = iv.erase(iter);
}

小结3:

  • 这种释放方式一般适用于容器中存放的不是整数100,200,300,而是指针的话,需要把指针指向的元素一个一个提取出来并释放掉(下面范例演示)。
  • 如果容器中存放的是数据,使用iv.clear释放会方便。

七. 范例演示

7.1 用迭代器遍历string数据
string str("I Love BlackSilk!");   //我爱黑丝
for (auto iter = str.begin(); iter != str.end(); iter++)
{
  *iter = toupper(*iter);        //toupper函数:小写字母转换成大写字母
}
cout << str << endl;


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

实例程序:容器中存储的是指针的话,必须自己手动释放内存

1、容器中存储指针数据

struct conf
{
    char itemname[40];       //条目名
    char itemcontent[100];   //条目内容
};
//SeverName = 1区;    //表示服务器名称
//SeverID = 10000;    //表示服务里ID
conf *pconf1 = new conf;
strcpy_s(pconf1->itemname,sizeof(pconf1->itemname),"SeverName");
strcpy_s(pconf1->itemcontent,sizeof(pconf1->itemcontent),"1区");
conf *pconf2 = new conf;
strcpy_s(pconf2->itemname,sizeof(pconf2->itemname),"SeverID");
strcpy_s(pconf2->itemcontent,sizeof(pconf2->itemcontent),"100000");
vector<conf *> conflist;
conflist.push_back(pconf1);    //这里conflist[0] = pconf1指向的内容,两者指针拷贝,内存地址相同
conflist.push_back(pconf2);    //这里conflist[1] = pconf2指向的内容,两者指针拷贝,内存地址相同

2、读取容器中的数据

下面我们增加一个函数功能,我希望输出服务器名称SeverName时,函数返回的是1区;输出服务器名称SeverID时,函数返回的是100000

char *getinfo(vector<conf *>&conflist, const char *pitem)
{
  for (auto pos = conflist.begin(); pos != conflist.end(); pos++)
  {
    if (_stricmp((*pos)->itemname, pitem) == 0)   //_stricmp函数:判断两个字符串是否相等
    {
      return (*pos)->itemcontent;
    }
  }
  return nullptr;
}
char *p_tmp = getinfo(conflist, "ServerName");  //返回1区

3、手动释放掉容器中存储的指针指向的数据

//释放上面蓝色部分我们自己new分配出来的内存,否则会造成内存泄露
vector<conf *>::iterator pos;
for (auto pos = conflist.begin(); pos != conflist.end(); pos++)//遍历容器内指向右边蓝色内存的指针
{   
  delete (*pos);  //释放的指针*pos代表容器中指向右边蓝色内存的指针*pconf1、*pconf2,没有破坏(增/减)迭代器本身数据
}
conflist.clear();   //清空迭代器数据(写不写都行,程序结束系统会自动清空)

下雨天,最惬意的事莫过于躺在床上静静听雨,雨中入眠,连梦里也长出青苔。


目录
相关文章
|
3月前
|
监控 Linux 测试技术
C++零拷贝网络编程实战:从理论到生产环境的性能优化之路
🌟 蒋星熠Jaxonic,技术宇宙中的星际旅人。深耕C++与零拷贝网络编程,从sendfile到DPDK,实战优化服务器性能,毫秒级响应、CPU降60%。分享架构思维,共探代码星辰大海!
|
6月前
|
C语言 C++
【实战指南】 C/C++ 枚举转字符串实现
本文介绍了在C/C++中实现枚举转字符串的实用技巧,通过宏定义与统一管理枚举名,提升代码调试效率并减少维护错误。
429 64
|
6月前
|
程序员 编译器 C++
【实战指南】C++ lambda表达式使用总结
Lambda表达式是C++11引入的特性,简洁灵活,可作为匿名函数使用,支持捕获变量,提升代码可读性与开发效率。本文详解其基本用法与捕获机制。
260 53
|
10月前
|
监控 Linux C++
【实战指南】4步实现C++插件化编程,轻松实现功能定制与扩展(2)
本文是《4步实现C++插件化编程》的延伸,重点介绍了新增的插件“热拔插”功能。通过`inotify`接口监控指定路径下的文件变动,结合`epoll`实现非阻塞监听,动态加载或卸载插件。核心设计包括`SprDirWatch`工具类封装`inotify`,以及`PluginManager`管理插件生命周期。验证部分展示了插件加载与卸载的日志及模块状态,确保功能稳定可靠。优化过程中解决了动态链接库句柄泄露问题,强调了采纳用户建议的重要性。
403 86
【实战指南】4步实现C++插件化编程,轻松实现功能定制与扩展(2)
|
10月前
|
人工智能 程序员 C++
【实战经验】C/C++右移高位补0还是1?
本文探讨了C/C++中右移运算时高位补0还是补1的问题。通过示例代码分析,揭示了右移规则:无符号类型高位补0;有符号类型根据正负决定(正数补0,负数补1)。文中列举了可能导致错误的场景,并提供了两种规避措施——使用无符号类型和掩码校正,确保结果符合预期。最后总结指出,右移运算虽常见,但若处理不当易引发隐晦Bug,需谨慎对待。
573 76
|
7月前
|
存储 安全 编译器
c++入门
c++作为面向对象的语言与c的简单区别:c语言作为面向过程的语言还是跟c++有很大的区别的,比如说一个简单的五子棋的实现对于c语言面向过程的设计思路是首先分析解决这个问题的步骤:(1)开始游戏(2)黑子先走(3)绘制画面(4)判断输赢(5)轮到白子(6)绘制画面(7)判断输赢(8)返回步骤(2) (9)输出最后结果。但对于c++就不一样了,在下五子棋的例子中,用面向对象的方法来解决的话,首先将整个五子棋游戏分为三个对象:(1)黑白双方,这两方的行为是一样的。(2)棋盘系统,负责绘制画面。
113 0
|
11月前
|
存储 缓存 C++
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
C++ 标准模板库(STL)提供了一组功能强大的容器类,用于存储和操作数据集合。不同的容器具有独特的特性和应用场景,因此选择合适的容器对于程序的性能和代码的可读性至关重要。对于刚接触 C++ 的开发者来说,了解这些容器的基础知识以及它们的特点是迈向高效编程的重要一步。本文将详细介绍 C++ 常用的容器,包括序列容器(`std::vector`、`std::array`、`std::list`、`std::deque`)、关联容器(`std::set`、`std::map`)和无序容器(`std::unordered_set`、`std::unordered_map`),全面解析它们的特点、用法
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
|
10月前
|
存储 分布式计算 编译器
C++入门基础2
本内容主要讲解C++中的引用、inline函数和nullptr。引用是变量的别名,与原变量共享内存,定义时需初始化且不可更改指向对象,适用于传参和返回值以提高效率;const引用可增强代码灵活性。Inline函数通过展开提高效率,但是否展开由编译器决定,不建议分离声明与定义。Nullptr用于指针赋空,取代C语言中的NULL。最后鼓励持续学习,精进技能,提升竞争力。
|
10月前
|
C++
|
存储 算法 安全
基于哈希表的文件共享平台 C++ 算法实现与分析
在数字化时代,文件共享平台不可或缺。本文探讨哈希表在文件共享中的应用,包括原理、优势及C++实现。哈希表通过键值对快速访问文件元数据(如文件名、大小、位置等),查找时间复杂度为O(1),显著提升查找速度和用户体验。代码示例展示了文件上传和搜索功能,实际应用中需解决哈希冲突、动态扩容和线程安全等问题,以优化性能。