C++ 迭代器失效风险分析

问题背景

一段经常使用的析构list中的元素代码被静态代码检测工具报错

for (auto it = list.begin(); it != list.end(); ++it) {
	delete (*it);
  list.erase(it);
}

迭代器失效是什么问题?

对容器进行添加元素或者删除元素时,有可能会导致容器中的元素位置发生变化,迭代器所指向的是一个无效地址,因此就会出现迭代器失效。

画图释义

三个myType对象,上面是值,下面的是地址
vector中有三个地址,分别存放了三个myType的地址
迭代器中也有自己的地址,是作为vector中地址的引用
在这里插入图片描述
在这里插入图片描述
删除第一个元素后,由于原本迭代器中第一个地址存放的引用变成一个无效值了
在这里插入图片描述

迭代器为什么会失效?

迭代器失效发生的原因与容器的内部实现有关。对于 std::vector

  1. 内存连续性std::vector 在内存中存储元素的方式是连续的。这意味着,当你增加或删除元素时,可能会导致存储容器元素的内存区域重新分配或元素移动。
  2. 删除操作:在 std::vector 中,当你删除一个元素时,所有该元素之后的元素必须向前移动一位来填补空出来的位置。这个操作会改变这些元素的地址,因此所有指向被移动元素的迭代器都会失效。
  3. 内存重新分配:如果在添加元素时超出了 std::vector 的当前容量,它会进行内存重新分配以容纳更多元素。这个重新分配通常涉及申请一个更大的内存块,将现有元素复制或移动到新的内存块中,然后释放旧的内存块。在重新分配之后,所有指向旧内存块中元素的迭代器都将失效。

结论

在使用 std::vector(或其他可能重新分配内存的容器)时,进行任何可能改变容器大小或内部元素位置的操作都需要注意迭代器的有效性。如果容器内部结构发生变化,你必须确保不再使用任何可能已经失效的迭代器。在使用 erase 方法时,正确的做法是使用该方法返回的新迭代器继续迭代。这是因为 erase 方法保证返回一个指向删除元素之后元素的有效迭代器,确保迭代器的有效性和迭代的连续性。

什么时候迭代器会失效?

会让容器内元素发生移动的操作:

1.删除元素:当你删除一个元素时,所有该元素之后的元素必须向前移动一位来填补空出来的位置。

2.增加元素:如果在添加元素时超出了容器当前容量,它会进行内存重新分配以容纳更多元素。

如何防止迭代器失效?

删除元素时

for (auto it = datalist.begin(); it != datalist.end(); ) {
    delete *it;                // 假设元素是通过 new 分配的,释放指针指向的对象内存
    it = datalist.erase(it);   // 删除元素,释放容器内的内存,获取下一个元素的迭代器
}

填充元素时

填充元素时,一般都不会使用迭代器,所以这种情况一般不会出现迭代器失效。

迭代器是使用指针实现的么?

在 C++ 中,迭代器的具体实现细节取决于容器的类型和迭代器自身的设计。迭代器通常表现得像指针,因为它们支持指针类似的操作,比如解引用(*iter)、成员访问(iter->member)和迭代(++iter--iter)。但它们是否直接使用指针来实现,这完全依赖于特定的迭代器和容器的实现。

对于一些容器,如 std::vectorstd::array,迭代器通常是通过原始指针实现的,因为这些容器的元素在内存中是连续存储的,所以使用指针作为迭代器是自然且高效的。

而对于 std::list 这样的容器,由于其内部是非连续的双向链表结构,其迭代器内部可能包含一个指向链表节点的指针,并且提供了额外的逻辑来处理向前和向后遍历链表的操作。

对于更复杂的容器,如 std::mapstd::set,它们通常是基于平衡二叉树(如红黑树)实现的,其迭代器可能更加复杂,它们需要能够遍历树结构,这就需要更多的逻辑而不仅仅是一个简单的指针。

总之,迭代器可能使用指针来实现,也可能使用包含更复杂逻辑的类来实现,以适应不同容器的需求。无论内部如何实现,迭代器都提供了一个统一的接口来访问和遍历容器的元素。在 C++ 中,这种抽象隐藏了实现的细节,允许开发者使用统一的方式操作各种不同的容器。

总结

迭代器通过重载运算符,实现了迭代器的的使用类似于指针

  • 使用“!=”来比较
  • 使用“*”解引用
  • 使用“→”获取成员
  • 使用“++”或者“—”

迭代器的内部实现与指针有关

有的会使用原始指针,有的会封装一下指针。

迭代器的失效主要是当erase掉时,会释放迭代器指向元素的内存,导致迭代器不再指向一个有效的元素。因此当你再通过迭代器去获取容器的元素的时候,就会有问题。

参考

C++迭代器iterator详解__stl_verify(_first._getcont() == _last._getcont(),-CSDN博客

c++迭代器失效的rule:可以当作参考书,哪种情况下迭代器会失效。

Iterator invalidation rules for C++ containers

测试

将list中填充myType指针,两个迭代器都指向mList.begin(),然后打印每个myType对象的地址。

可以看到mylist中的三个指针存放的都是对应myType对象的地址。
两个迭代器中的地址也是存放的第一个myType对象的地址。
在这里插入图片描述
output输出:list的地址,迭代器的内存地址,list中每个对象的地址。
根据迭代器的取值(*it)与list中myType对象的地址一样,可以得出迭代器中是有记录对象的地址的。
在这里插入图片描述

第一个迭代器遍历容器

可以看到it1中存放的地址已经没有指向任何一个对象
在这里插入图片描述
output输出:*it1:返回的是迭代器指向位置的数值,&(*it)该数值所存储的地址。因此当迭代器++的时候,这个地址也会发生变化。
在这里插入图片描述

第二个迭代器遍历删除第一个元素

mylist中只有两个对象,迭代器2的存储地址变成不正常了,同时迭代器1中的地址-1.
在这里插入图片描述
output:it2的指向的容器中元素的引用本身的地址是不变的。(变的只是它存放的值)
在这里插入图片描述
再往后执行,就运行报错了。

  • 20
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值