遇到的问题
上周做项目时遇到一个bug,最终定位是错误得使用了c++迭代器导致,错误代码如下:
for (auto it = frameCacheMap.begin(); it != frameCacheMap.end(); it++) {
if (it->first != videoSsrc) {
auto frame = it->second;
frameCacheMap.erase(it); //erase后it这个迭代器失效,不能继续在for循环中执行it++等操作
av_frame_free(&frame);
}
}
有效的迭代器或者指向某个元素,或者指向容器中尾元素的下一位置;其他所有情况都属于无效。而使用失效的迭代器是一种严重的程序设计错误,很可能引起与使用未初始化指针一样的问题(出自c++ primer)。
因此本帖子来探讨一下迭代器失效的情况,如有错误或者遗漏欢迎指出。旨在解决已下问题:
- 迭代器在什么情况下会失效
- 迭代器为什么会失效
迭代器失效的情况及原因
情况一:容器的存储空间被重新分配
这里以vector为例,vector的元素是连续存储的,但相比数组它的大小是可变的即会动态增长。具体来说就是vector有一个capacity和一个size,size是指它已经保存的元素的数目,而capacity则是在不分配新的内存空间的前提下它最多可以保存多少元素,当capacity不够时会重新分配一段更大的存储空间,至于vector动态增长策略,本帖不作细致讨论,可参考vector源码,vector空间的动态增长。
当vector的大小增大到capacity不够时,内部会重新分配一段连续的存储空间,原先的迭代器都会失效,所以使用push_back操作时尽量不要和迭代器沾边。如下代码就会导致运行错误:
std::vector<int> vi;
vi.push_back(1);
auto it = vi.begin();
vi.push_back(4); //错误!!!vector的capacity改变,存储空间重新分配,迭代器it失效
std::cout << *it << std::endl;
我们习惯的使用的for循环里,很容易出现这种错误(我刷题有时候都会这样写0.0),如下:
std::vector<int> vi;
vi.push_back(1);
for (auto it = vi.begin(); it != vi.end(); it++) {
if (*it == 1) {
vi.push_back(4); //错误!!!改变了vector的capacity,存储空间重新分配,迭代器it失效
}
std::cout << *it << std::endl;
}
不过也有可能出现push_back操作并没有改变vector的capacity,这种情况下没有重新分配连续存储空间,使用push_back不会导致迭代器失效,但是我不建议抱有这种心理,因为我们可能并不了解vector capacity动态增长的策略,即使知道,每次push_back时我们也并不关注它此时的capacity和size的关系。
情况二:erase操作导致迭代器失效
在c++ primer中提到,当我们从一个容器中删除元素后,指向被删除元素的迭代器、指针和引用会失效。当删除一个元素
- 对于vector和string,指向被删除元素之前的迭代器、引用和指针仍有效。
- 对于list和forward_list,指向容器其他位置的迭代器(包括尾后迭代器和首前迭代器)、引用和指针仍有效。
这个其实比较好理解,元素都没了,迭代器肯定失效,我在代码实践中确实是这样的,如下代码就运行出错:
for (int i = 0; i < 24; i++) {
vi.push_back(i);
}
for (auto it = vi.begin(); it != vi.end(); it++) {
if (*it == 20) {
vi.erase(it);
}
std::cout << *it << std::endl; //迭代器it在等于20时失效,*it或者it++都会报错
}
下面给出一些正确写法:
-
vector删除元素
for (int i = 0; i < 24; i++) { vi.push_back(i); } for(auto it = vi.begin(); it != vi.end();){ if(*it==20){ it=vi.erase(it); //vector的erase会返回指向下一个元素的有效迭代器 }else{ it++; } }
-
map删除元素
std::map<int, string> mp; mp[0] = "0"; mp[1] = "1"; mp[2] = "2"; for (auto it = mp.begin(); it != mp.end();) { if (it->second == "0") { mp.erase(it++); } else { ++it; } }
这是因为map之类的容器,使用了红黑树来实现,插入、删除一个结点不会对其他结点造成影响。erase迭代器只是被删元素的迭代器失效,但是返回值为void,所以要采用erase(iter++)的方式删除迭代器。
解析: mp.erase(it++);这句话分三步走,先把it传值到erase里面,然后it自增,然后执行erase,所以it在失效前已经自增了。
-
list删除元素
std::list<int> lst; for (int i = 0; i < 24; i++) { lst.push_back(i); } for (auto it = lst.begin(); it != lst.end();) { if (*it == 20) { lst.erase(it++); //或者 it = lst.erase(it); } else { ++it; } }
对于链表式容器(如list),删除当前的iterator,仅仅会使当前的iterator失效,这是因为list之类的容器,使用了链表来实现,插入、删除一个结点不会对其他结点造成影响。因此采用和map一样的方式删除即可。同时erase也会返回指向下一个元素的有效迭代器,也可以使用vector的删除方式。
小结
本帖主要讨论了迭代器失效的两种情况
- 对于第一种情况,注意错误情况,不要踩坑∩﹏∩
- 第二种情况按照正确的写法即可
希望对大家有帮助~有错误或者补充欢迎指出。