C++:迭代器失效问题(vector为例)
希望文章能对你有所帮助,有不足的地方请在评论区留言指正,一起交流学习!
本篇博客将剖析vector迭代器失效问题,以vector为例子 ,以insert和erase为例子讲解,插入图解进行分析。
目录
1. vector 迭代器是什么?
2.vector 迭代器失效
场景 1: “物理失效”—— 地址直接作废
场景 2: “逻辑失效”—— 地址有效但元素错位
3.如何避免使用迭代器失效
3.1.STL中的迭代器
3.2.vector模拟仿真的改进
4.总结
1. vector 迭代器是什么?
vector 迭代器的底层是:原生指针的封装(
typedef T* iterator
),本质是 “指向 vector 元素的内存地址”。
也就是说,
vector<int>::iterator
本质就是int*
,vector<string>::iterator
本质就是string*
—— 它直接指向 vector 元素在内存中的地址。.
对比list
- list 迭代器:是 “节点指针的封装”,每个节点独立存储,迭代器
++
时是跳转到下一个节点的地址(无需地址偏移);- vector 迭代器:是 “原生指针”,依赖连续内存的地址连续性。
这就决定了:list 迭代器只有在节点被删除时才失效,而 vector 迭代器会因为 “内存地址变化” 或 “元素移位” 频繁失效。
2.vector 迭代器失效
vector 迭代器失效分两种:“物理失效”(地址无效)和 “逻辑失效”(地址有效但元素错位)。两种失效的根源不同,但都会导致程序崩溃或数据错乱。
场景的测试,我将采用之前模拟实现的vector来测试;因为在VS软件中,库中的vector迭代器失效会报错。C++:模拟实现vector的问题-CSDN博客。
场景 1: “物理失效”—— 地址直接作废
vector 的容量(capacity)是固定的,当
size == capacity
时(元素装满了),再插入元素(如push_back
、insert
)会触发扩容:
- 申请一块更大的连续内存(模拟实现为 2 倍扩容,初始容量为4);
- 把旧内存中的元素拷贝到新内存;
- 释放旧内存;
- 让 vector 的
_start
(首元素指针)指向新内存。
而迭代器是指向 “旧内存” 的指针,旧内存被释放后,迭代器就变成了 “野指针”—— 访问野指针会导致未定义行为(程序崩溃、打印乱码等)。
本小节包含push_back 扩容以及insert、erase的迭代器失效示例。(所有用到函数为模拟实现函数)
触发操作:这些操作可能导致扩容
push_back(val)
:当size == capacity
时;insert(pos, val)
:当size == capacity
时;reserve(n)
:当n > 原capacity
时(主动扩容)。
当然:erase也会导致地址直接作废的。删除尾部元素,迭代器指向尾部的时候。
vector<int> v; // 初始capacity=0,size=0
auto it = v.begin(); // it指向空地址(如0x0)// 第一次push_back:capacity从0→1(扩容),新内存地址假设为0x200
v.push_back(1);
// 此时it仍指向旧地址0x0(已被释放),但很多新手以为it还能用!
// cout << *it; // 未定义行为!大概率崩溃
示例1 push_back函数 (野指针)
说明:下述程序,采用模拟实现的vector,初始容量为0,第一次扩容为4,第二次扩容为2倍,往后扩容依次为2倍。
// 测试迭代器失效
void test_vector4()
{vector<int> v;cout << "capacity:" << v.capacity() << endl;v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);for (auto& e : v){cout << e << " ";}cout << endl;cout << "capacity:" << v.capacity() << endl; // 再次插入数据size()==capacity触发扩容vector<int>::iterator it = v.begin();v.push_back(5);cout << "capacity:" << v.capacity() << endl;while (it != v.end()){cout << *it << " ";it++;}cout << endl;
}
结果
扩容:
- 申请一块更大的连续内存(模拟实现为 2 倍扩容,初始容量为4);
- 把旧内存中的元素拷贝到新内存;
- 释放旧内存;
- 让 vector 的
_start
(首元素指针)指向新内存。也就是说原来的迭代器已经失效了,指向的位置是随机值,不再归v管理了。
图解
示例2 insert函数 (野指针)
代码
// 测试迭代器失效
void test_vector4()
{vector<int> v;cout << "capacity:" << v.capacity() << endl;v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);for (auto& e : v){cout << e << " ";}cout << endl;cout << "capacity:" << v.capacity() << endl; // 再次插入数据size()==capacity触发扩容vector<int>::iterator it = v.begin();v.insert(it, 5);cout << "capacity:" << v.capacity() << endl;while (it != v.end()){cout << *it << " ";it++;}cout << endl;
}
说明:只是将上述位置 push_back 改为 insert,也是导致扩容改变了_start。
上述是代码调用的顺序,不会执行完成出程序
运行结果
“读取访问权限冲突” 是 C++ 程序中常见的内存错误,通常由非法内存访问导致。
分析原因
越界访问了,会爆出错误
示例3 erase删除末尾元素,导致的迭代器失效。(野指针)
#include <iostream>
using namespace std;
#include <vector>
int main()
{int a[] = { 1, 2, 3, 4 };vector<int> v(a, a + sizeof(a) / sizeof(int));// 使用find查找3所在位置的iteratorvector<int>::iterator pos = find(v.begin(), v.end(), 4);// 删除pos位置的数据,导致pos迭代器失效。v.erase(pos);cout << *pos << endl; // 此处会导致非法访问return 0;
}
vector
是连续内存容器,删除元素后,被删除元素的内存会被 “回收”(或被后续元素覆盖),指向该位置的迭代器自然失去有效性 —— 它不再指向vector
中的任何有效元素。但是,删除最后一个元素,可以使用pop函数,更加高效。对于这种情况,需要重新利用end()更新迭代器。
总结:
由于扩容或者删除尾部数据导致的的迭代器指针,指向的位置,无法被有效的访问,造成越界访问的,都是地址失效的野指针。
场景 2: “逻辑失效”—— 地址有效但元素错位
删除元素时,内存没有被释放(不会触发扩容),但元素的位置变了,导致迭代器指向的 “内容” 不符合预期。
为什么会失效?
vector 是连续内存,删除
pos
位置的元素后,pos
及之后的所有元素会向前移位,填补删除后的 “空位”。此时迭代器指向的地址虽然有效,但对应的元素已经不是原来的元素了 —— 逻辑上失效了。
触发操作:erase(pos) 或者insert函数
示例1 erase函数
int a[] = { 1, 2, 2, 2, 3, 4 };
vector<int> v(a, a + sizeof(a) / sizeof(int));
cout << "原始数据:";
for (auto& e : v)
{cout << e << " ";
}
cout << endl;auto it = v.begin(); // 删除所有的2
while (it != v.end())
{if (*it == 2)v.erase(it);++it;
}
cout << "修改数据:";
for (auto& e : v)
{cout << e << " ";
}
cout << endl;
图解
遍历的时候需要需要it++,但是erase会改变元素的位置,导致本应该删除的元素和指针it错过。因此改进的时候,可以增加一个返回值,返回pos找到的位置重新对比。
示例2 insert函数
// 测试迭代器失效
void test_vector4()
{vector<int> v;cout << "capacity:" << v.capacity() << endl;v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);v.push_back(5);for (auto& e : v){cout << e << " ";}cout << endl;cout << "capacity:" << v.capacity() << endl; // 再次插入数据size()==capacity触发扩容vector<int>::iterator it = v.begin() + 2;//指向第三个元素的v.insert(it,10);//在第三个元素的位置插入一个10 此时it 指向的是10,但是//如果再次使用it 本意修改 3 但是it指向的是10,因此元素错位的情况cout << "capacity:" << v.capacity() << endl;cout << endl;
}
insert没有比较合适的例子,本来指向3的迭代器最后指向了10.使用insert的返回值也没有用,需要使用begin函数更新it。
3.如何避免使用迭代器失效
3.1.STL中的迭代器
在VS软件使用中,当迭代器失效出现的时候,程序会直接报错。
示例1:insert插入数据
// 测试迭代器失效
void test_vector4()
{std::vector<int> v;cout << "capacity:" << v.capacity() << endl;v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);v.push_back(5);v.push_back(6);for (auto& e : v){cout << e << " ";}cout << endl;cout << "capacity:" << v.capacity() << endl; vector<int>::iterator it = v.begin() + 2;v.insert(it, 10); // 会扩容cout << "capacity:" << v.capacity() << endl;cout << *it << endl;cout << endl;
}
插入操作:
若插入位置在中间,插入后该位置及之后的迭代器全部失效;若触发扩容,所有迭代器失效。
解决:插入后通过insert
的返回值获取新迭代器,或重新调用begin()
/end()
获取。删除操作:
删除位置及之后的迭代器全部失效。
解决:用erase
的返回值更新迭代器(如遍历删除元素时):
改进:库中的insert有返回值,增加了迭代器的更新
// 测试迭代器失效
void test_vector4()
{std::vector<int> v;cout << "capacity:" << v.capacity() << endl;v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);v.push_back(5);v.push_back(6);for (auto& e : v){cout << e << " ";}cout << endl;cout << "capacity:" << v.capacity() << endl; vector<int>::iterator it = v.begin() + 2;it = v.insert(it, 10); // 会扩容cout << "capacity:" << v.capacity() << endl;cout << *it << endl;cout << endl;
}
为 insert以及erase函数增加了返回值,这样就可以更新迭代器的地址了。
示例2:erase删除数据
删除数据导致的迭代器失效也会报错。
总结
在VS中,容器状态改变后,必须确保迭代器指向有效,才能访问。当容器状态改变后,
只有通过 “用操作返回值更新迭代器” 或 “重新获取迭代器”,让迭代器绑定容器最新状态,才能安全使用。
3.2.vector模拟仿真的改进
对于insert的改进有分为两步。
第一步 对于 it 指向为野指针
insert插入的数据,导致扩容 pos变为野指针,_start和_finish都更新了,当然也要更新一下pos的指向了。这样防止pos变为野指针。
void insert(iterator pos, const T& x)
{//检测参数合法性assert(pos >= _start && pos <= _finish);//检测是否需要扩容/*扩容以后pos就失效了,需要更新一下*/if (_finish == _end_of_stoage){size_t n = pos - _start;//计算pos和start的相对距离size_t newcapcacity = capacity() == 0 ? 4 : capacity() * 2;reserve(newcapcacity);pos = _start + n;//防止迭代器失效,要让pos始终指向与_start间距n的位置}//挪动数据iterator end = _finish - 1;while (end >= pos){*(end + 1) = *(end);end--;}//把值插进去*pos = x;_finish++;
}
第二步 对于 it 指向导致的元素错位,增加返回值,更新迭代器。
iterator insert(iterator pos, const T& x)
{//检测参数合法性assert(pos >= _start && pos <= _finish);//检测是否需要扩容/*扩容以后pos就失效了,需要更新一下*/if (_finish == _end_of_stoage){size_t n = pos - _start;//计算pos和start的相对距离size_t newcapcacity = capacity() == 0 ? 4 : capacity() * 2;reserve(newcapcacity);pos = _start + n;//防止迭代器失效,要让pos始终指向与_start间距n的位置}//挪动数据iterator end = _finish - 1;while (end >= pos){*(end + 1) = *(end);end--;}//把值插进去*pos = x;_finish++;return pos;
}
这种改进的原因就是,在insert插入数据之后,vector作为连续的存储方式,it以及it之后的指针全部失效了,为保证it可以继续使用,更新一下it,想要原来it指向的元素,需要begin函数再次访问指向,或者修改it。(库中的做法)
erase函数改进
iterator erase(iterator pos)
{//检查合法性assert(pos >= _start && pos < _finish);//从pos + 1的位置开始往前覆盖,即可完成删除pos位置的值iterator it = pos + 1;while (it < _finish){*(it - 1) = *it; it++;}_finish--;return pos;
}
4.总结
“
vector
迭代器失效源于连续内存特性:扩容导致所有迭代器指向旧内存(物理失效),删除元素导致pos
及之后迭代器逻辑错位(逻辑失效);需通过‘重新获取迭代器’或‘用erase
返回值更新’规避,对比list
可凸显连续内存与非连续内存的设计差异。”
如何在平时使用stl中避免迭代器失效报错:
1.如果执行了可能触发扩容的操作(如
push_back
、insert
),不要复用之前保存的迭代器,必须通过begin()
、end()
等接口重新获取。2.
erase(pos)
有一个关键的返回值:指向 “被删除元素的下一个有效元素” 的迭代器。用这个返回值更新迭代器,就能避免逻辑失效。3.迭代器的生命周期应该 “即用即取”,不要在循环外定义迭代器,再在循环内频繁修改 vector(如
push_back
、erase
)。
为什么 list 迭代器不容易失效?
很多人疑惑:同样是删除元素,为什么 list 迭代器很少失效?这还是要回到两种容器的底层结构差异:
- list 是双向链表,每个元素是独立的节点,节点间通过
prev
/next
指针连接,内存不连续;- 删除 list 的某个节点时,只需修改前后节点的指针(断开被删节点),其他节点的地址不变,因此只有被删除的迭代器失效,其他迭代器不受影响。
对比之下,vector 的连续内存特性决定了:任何导致 “内存地址变化” 或 “元素移位” 的操作,都可能引发迭代器失效 —— 这是 vector 高效随机访问的 “代价”。
vector 迭代器失效的所有问题,都可以归结为一句话:迭代器是指向连续内存的指针,当内存地址变化(扩容)或元素位置变化(删除)时,指针要么作废,要么指向错误的内容。
不要死记 “哪些操作会导致失效”,而是要理解:
- 迭代器的本质是原生指针;
- vector 的连续内存需要扩容和元素移位;
- 针对这两个特性,用 “重新获取迭代器”“用 erase 返回值更新” 等方法规避风险。