C++中迭代器的一些理解
引言
在使用vector和string时,我其实早就见过迭代器了,insert和erase的参数都是迭代器,find这类算法的返回值也是迭代器。但迭代器到底是什么,end()指向的是哪,为什么erase之后迭代器会失效,这些一直没有认真想过。
于是我写了一个iterator的使用实验,把获取、遍历、查找、删除串在一起跑了一遍。
在此篇文章中,我将记录下自己对迭代器的理解,以供思考,如有不足之处欢迎指正。
正文
迭代器是什么
我的理解是,迭代器是统一访问容器元素的抽象指针。 它像指针一样支持解引用和自增,但又把容器底层的实现细节(数组、链表还是哈希桶)藏了起来,算法只跟迭代器打交道,不关心容器是什么。
获取迭代器的两个端点是begin()和end():
vector vec{0, 11, 22, 33, 44, 55, 66, 66, 66, 77};
auto begin{std::begin(vec)};//std::begin,也可以用vec.begin()
auto end{vec.end()};
cout << *(end - 1) << endl;//end指向最后一个元素的下一个位置
end()并不指向最后一个元素,而是指向最后一个元素的下一个位置,所以*(end - 1)才是最后一个元素。
这么看来,end()更像是一个“终点哨兵”,遍历条件p != end在遍历完所有元素后恰好成立。
与算法配合
标准库算法几乎都以迭代器对作为输入:
auto it = std::find(begin, end, 3);//查找值,找不到返回end
if (it == end) { cout << "Not found 3" << endl; }
it = std::find(begin, end, 33);
cout << *it << " is in pos" << it - begin << endl;//迭代器相减得到下标
auto f = [](const int &t) { return t == 22; };
it = std::find_if(begin, end, f);//按谓词查找
auto count = std::count(begin, end, 66);//统计出现次数
count = std::count_if(begin, end, [](int t) { return t < 40; });
std::sort(begin, end, std::greater{});//降序排序
std::for_each(std::next(begin), end, [](int t) { cout << t << ' '; });
find()找不到时返回end(),这个约定使得“未找到”的判断非常统一,判别方式就是it == end。
而it - begin能得到下标,std::next(begin)能拿到偏移后的迭代器,这些都是迭代器像指针一样支持运算的体现。
迭代器失效
这是实验里最重要的一处细节:
//必须重赋值,否则迭代器会失效,erase()返回下一个元素
it = vec.erase(it);
erase()删除元素后,指向被删元素及之后位置的迭代器都会失效,erase()的返回值指向被删元素的下一个元素,必须用它重新赋值迭代器。
以vector为例,erase()会让删除点之后的所有迭代器失效,因为后面的元素要整体前移;这与我在vector实验里提到的扩容导致迭代器失效是同一类问题,根源都是元素搬移。
补充要点
迭代器配合vector和string的使用方式基本一致,begin()、end()、erase()的语义都是共通的,这也是前面vector实验里insert()与erase()能以迭代器为参数的原因。
另外值得一提的是,c++20的range-based for底层就是靠begin()和end()工作的,像std::views::reverse(vec)生成的视图同样可以迭代,本质上还是迭代器在背后支撑。
小结
迭代器是连接容器与算法的桥梁,容器负责提供begin()和end(),算法只依赖迭代器对工作,两边的实现互不耦合。用迭代器写代码时最需要记住的两件事:end()指向最后元素的下一个位置,以及任何可能搬移元素的操作之后都要重新获取迭代器。
把迭代器当作“被抽象过的指针”来理解,大部分行为就都可以用指针的直觉去预测了。
本文所用的测试代码收录于Learncpp,如有不妥之处欢迎指正。
$ 正在加载评论…