C++迭代器模式深度解析:分类、实现原理与失效陷阱
如果你在 C 里写过稍微复杂一点的代码肯定绕不开迭代器这个概念。不管你是遍历一个std::vector还是给std::sort传区间参数背后都是迭代器在起作用。面试的时候迭代器模式、迭代器失效、iterator_traits这些点也几乎是必考内容。这个模式背后的设计思想可以说是理解 STL 泛型架构的一把钥匙。这篇文章我不会只讲概念而是从实际代码出发把迭代器模式拆开揉碎先看它解决什么问题再剖析它的分类和实现原理然后手写一个完整的迭代器最后把迭代器失效这种高发坑点一次性讲清楚。无论你是刚接触 C 的初学者还是被迭代器失效折磨过的老手这篇文章都值得花十五分钟读完。1. 迭代器模式到底解决了什么问题1.1 没有迭代器的时候遍历代码长什么样在没有迭代器的世界里遍历一个容器是件很别扭的事情。假如你有一个自己写的数组容器想遍历它只能写一个专门针对这个容器的函数访问它的内部数组、获取长度然后一个for循环从 0 数到size()。更麻烦的是如果你换一个数据结构比如链表遍历逻辑就得重写一套。// 自己实现的数组容器暴露了内部细节 class MyArray { public: int* getData() { return data; } size_t getSize() { return size; } private: int data[100]; size_t size; }; // 自己实现的链表容器遍历方式完全不同 class MyList { public: Node* getHead() { return head; } private: Node* head; }; // 调用方要分别写两套遍历逻辑 void processMyArray(MyArray arr) { for (size_t i 0; i arr.getSize(); i) { // 处理 arr.getData()[i] } } void processMyList(MyList list) { for (Node* p list.getHead(); p; p p-next) { // 处理 p-value } }这段代码最致命的问题不是代码重复而是调用方被迫知道了容器的“内部结构”。数组用下标链表用指针每来一种新容器就得给所有操作它的算法写一份配套代码。这个局面就是耦合——遍历算法和容器实现死死绑在了一起。1.2 迭代器的核心思想把“怎么遍历”和“遍历什么”拆开迭代器模式解决的就是上面那个问题。它的核心思想用一个词概括解耦。容器负责管理数据迭代器负责提供“一个统一的遍历接口”。调用方不需要关心你底层是数组、链表还是哈希表只需要拿着迭代器往前走、取值、比较是否到达末尾。// 只要容器提供 begin() 和 end() // 不管它内部是什么结构遍历逻辑都一样 templatetypename Container void processContainer(Container c) { for (auto it c.begin(); it ! c.end(); it) { // 处理 *it } }这段代码可以同时处理std::vector、std::list、std::map、std::set只要它们都提供了符合规范的迭代器。这就是解耦的价值算法是算法容器是容器迭代器是它们之间的“通用插座”。这跟设计模式里的迭代器模式是一致的它定义了一个接口通过这个接口顺序访问聚合对象中的元素而不暴露聚合对象的内部表示。特定领域里STL 把这种思想发挥到了极致后面的内容会深入这一点。1.3 为什么 C 的 STL 把它推向了极致C 里的迭代器不是简单的一个对象它更像一套严谨的“规范”。STL 之所以能用一套sort、copy、find打遍天下靠的就是迭代器提供的统一抽象。而且 C 的迭代器还分了好几个能力等级不同的容器提供不同能力的迭代器算法可以根据迭代器的能力选择最高效的实现。举个例子std::sort要求随机访问迭代器所以它能用在vector和deque上但不能直接用在list上。而std::find只要求前向迭代器所以几乎所有容器都能用它。这种能力分级不是限制而是让算法在不同容器上都跑得最好的关键设计。还有一个很关键点在 C 中原生指针本身就是一种迭代器。int*天然满足随机访问迭代器的要求所以 STL 算法直接兼容 C 风格的数组。这体现了 C “零成本抽象”的思想——迭代器在底层可以退化成裸指针不留一丝运行时开销。2. 迭代器的分类与实现原理2.1 五种迭代器分类各自能力边界在哪C 标准把迭代器分成五类能力从弱到强排列如下迭代器分类支持的操作使用场景输入迭代器*it读取、it、it ! end单向读取如std::istream_iterator输出迭代器*it value写入、it单向写入如std::ostream_iterator前向迭代器输入 输出能力支持多趟遍历单向链表std::forward_list双向迭代器前向能力 --it双向链表std::list、std::map、std::set随机访问迭代器双向能力 it n、it - it、it[n]、比较大小连续内存容器std::vector、std::deque、数组这五类迭代器不是 C 强制要求每个容器必须实现的而是算法选择策略的依据。比如std::distance(it1, it2)这个函数如果传入的是随机访问迭代器它可以直接返回it2 - it1复杂度是 O(1)如果是双向迭代器只能一步步往前数复杂度是 O(n)。2.2 为什么随机访问迭代器那么特殊随机访问迭代器之所以是“能力天花板”是因为它支持O(1)时间跳到任意位置。这意味着二分查找、排序、随机访问这些经典算法才能在上面运行。std::vectorint vec {1, 2, 3, 4, 5}; auto it vec.begin(); auto mid it 2; // O(1)直接跳到第三个元素 auto diff vec.end() - it; // O(1)计算距离 std::listint lst {1, 2, 3, 4, 5}; auto lit lst.begin(); // auto lmid lit 2; // 编译错误list的迭代器不支持 // 只能一个个走lit; lit;个人体会理解迭代器分类最简单的方式是把它想象成交通工具。随机访问迭代器是私家车想到哪个路口直接开过去双向迭代器是只能前进后退的火车不能跳站前向迭代器是单向步行道只能往前走。算法选择迭代器类型就像选交通工具不能拿步行道跑高速。2.3 迭代器 Traits 机制算法是怎么知道迭代器类型的这里有个关键问题算法是模板写的时候不知道你传进来的是什么迭代器。它怎么知道it n能不能用怎么知道*it返回什么类型答案就是std::iterator_traits。这是一个特征提取类专门从迭代器类型中取出它的“身份信息”。每个标准迭代器都定义了五种关联类型value_type迭代器指向的元素的类型difference_type两个迭代器之间距离的类型pointer元素指针类型reference元素引用类型iterator_category迭代器分类标签// 手写一个简单的 distance 实现思路 templatetypename Iterator typename std::iterator_traitsIterator::difference_type my_distance(Iterator first, Iterator last) { using category typename std::iterator_traitsIterator::iterator_category; return my_distance_impl(first, last, category{}); } // 随机访问迭代器版本直接相减 templatetypename Iterator auto my_distance_impl(Iterator first, Iterator last, std::random_access_iterator_tag) { return last - first; } // 双向迭代器版本手动走 templatetypename Iterator auto my_distance_impl(Iterator first, Iterator last, std::bidirectional_iterator_tag) { typename std::iterator_traitsIterator::difference_type n 0; while (first ! last) { first; n; } return n; }借助iterator_category和标签分发机制同一个函数在不同迭代器上会自动选择最优实现。这就是 C 泛型编程里“编译期多态”的典型范例——没有运行时虚函数开销一切在编译期决定。2.4 指针为什么天生就是迭代器原生指针之所以能直接作为迭代器使用是因为它满足随机访问迭代器的全部要求支持*解引用、支持/--、支持n/-n、支持两个指针相减得到元素个数。C 标准库利用这一点让数组一遍遍地融入泛型算法中。int arr[] {5, 2, 8, 1, 9}; std::sort(arr, arr 5); // 指针即迭代器 std::find(arr, arr 5, 8); // 范围for循环也兼容数组 for (int x : arr) { std::cout x ; }这也解释了为什么老牌 C 程序员喜欢用指针操作数组——因为指针在底层就是最高效的迭代器。不过现代 C 更推荐用std::begin(arr)/std::end(arr)获取迭代器或者直接用范围 for这样代码即便以后改成std::vector也几乎不用改动。3. 手写一个迭代器从零实现的核心环节3.1 先定义一个简单的动态数组容器纸上得来终觉浅。为了把迭代器原理真正用到实处这里我带你从零写一个简化版的动态数组容器然后手把手实现它的正向迭代器、常量迭代器和反向迭代器。#include iostream #include memory #include stdexcept templatetypename T class SimpleArray { public: using value_type T; using size_type size_t; using difference_type ptrdiff_t; using pointer T*; using reference T; explicit SimpleArray(size_type n 0) : size_(n), cap_(n), data_(new T[n]) {} ~SimpleArray() { delete[] data_; } SimpleArray(const SimpleArray other) { // 拷贝构造的完整实现标准做法是拷贝数据 size_ other.size_; cap_ other.cap_; data_ new T[cap_]; for (size_type i 0; i size_; i) { data_[i] other.data_[i]; } } SimpleArray operator(const SimpleArray other) { if (this ! other) { delete[] data_; size_ other.size_; cap_ other.cap_; data_ new T[cap_]; for (size_type i 0; i size_; i) { data_[i] other.data_[i]; } } return *this; } void push_back(const T value) { if (size_ cap_) { grow(); } data_[size_] value; } void grow() { size_type newCap cap_ 0 ? 1 : cap_ * 2; T* newData new T[newCap]; for (size_type i 0; i size_; i) { newData[i] data_[i]; } delete[] data_; data_ newData; cap_ newCap; } size_type size() const { return size_; } size_type capacity() const { return cap_; } reference operator[](size_type index) { if (index size_) throw std::out_of_range(index out of range); return data_[index]; } const T operator[](size_type index) const { if (index size_) throw std::out_of_range(index out of range); return data_[index]; } pointer data() { return data_; } private: T* data_; size_type size_; size_type cap_; };注意这里我没有写移动构造和移动赋值真实项目里一定要补上。先简单点把迭代器的概念讲清楚再谈优化。3.2 实现 begin() / end() 和迭代器类最核心、最关键的部分是迭代器类的设计。一个标准的随机访问迭代器至少要支持以下操作解引用*、成员访问-、前置和后置、前置和后置--、算术加减、复合赋值、两个迭代器相减、以及和!比较。templatetypename T class SimpleArrayT::iterator { public: using iterator_category std::random_access_iterator_tag; using value_type T; using difference_type ptrdiff_t; using pointer T*; using reference T; iterator() : ptr_(nullptr) {} explicit iterator(T* ptr) : ptr_(ptr) {} reference operator*() const { return *ptr_; } pointer operator-() const { return ptr_; } iterator operator() { ptr_; return *this; } iterator operator(int) { iterator tmp *this; (*this); return tmp; } iterator operator--() { --ptr_; return *this; } iterator operator--(int) { iterator tmp *this; --(*this); return tmp; } iterator operator(difference_type n) { ptr_ n; return *this; } iterator operator-(difference_type n) { ptr_ - n; return *this; } iterator operator(difference_type n) const { iterator tmp *this; tmp n; return tmp; } iterator operator-(difference_type n) const { iterator tmp *this; tmp - n; return tmp; } difference_type operator-(const iterator other) const { return ptr_ - other.ptr_; } reference operator[](difference_type n) const { return *(ptr_ n); } bool operator(const iterator other) const { return ptr_ other.ptr_; } bool operator!(const iterator other) const { return ptr_ ! other.ptr_; } bool operator(const iterator other) const { return ptr_ other.ptr_; } bool operator(const iterator other) const { return ptr_ other.ptr_; } bool operator(const iterator other) const { return ptr_ other.ptr_; } bool operator(const iterator other) const { return ptr_ other.ptr_; } private: T* ptr_; };这里有几个细节要特别说明。实现operator(int)时返回的是一个旧的副本而operator()返回自身引用因为后置增量需要保存修改前的状态而前置增量不需要。这也是为什么前置效率高于后置尤其在非平凡对象上差别更明显。另外我特意定义了iterator_category为std::random_access_iterator_tag这样 STL 的算法就能按照随机访问迭代器来处理它。接下来把begin()和end()加到SimpleArray中iterator begin() { return iterator(data_); } iterator end() { return iterator(data_ size_); }这样这个自己写的容器已经能配合范围 for 循环和 STL 的std::sort了。实际上有了迭代器这个简易容器立刻可以用std::sort排序、用std::copy输出。整个模式这里可以看到价值写一套遍历接口免费获得一堆算法。3.3 常量迭代器和反向迭代器两个必须处理的细节真实容器还会提供const_iterator和reverse_iterator否则const对象无法遍历或者遍历方向受限。const_iterator的核心差异是operator*返回const Toperator-返回const T*本质上只需要把T*换成const T*再复制一遍。templatetypename T class SimpleArrayT::const_iterator { public: using iterator_category std::random_access_iterator_tag; using value_type T; using difference_type ptrdiff_t; using pointer const T*; using reference const T; const_iterator() : ptr_(nullptr) {} explicit const_iterator(const T* ptr) : ptr_(ptr) {} reference operator*() const { return *ptr_; } pointer operator-() const { return ptr_; } const_iterator operator() { ptr_; return *this; } const_iterator operator(int) { const_iterator tmp *this; (*this); return tmp; } const_iterator operator--() { --ptr_; return *this; } const_iterator operator--(int) { const_iterator tmp *this; --(*this); return tmp; } const_iterator operator(difference_type n) { ptr_ n; return *this; } const_iterator operator-(difference_type n) { ptr_ - n; return *this; } const_iterator operator(difference_type n) const { const_iterator tmp *this; tmp n; return tmp; } const_iterator operator-(difference_type n) const { const_iterator tmp *this; tmp - n; return tmp; } difference_type operator-(const const_iterator other) const { return ptr_ - other.ptr_; } reference operator[](difference_type n) const { return *(ptr_ n); } bool operator(const const_iterator other) const { return ptr_ other.ptr_; } bool operator!(const const_iterator other) const { return ptr_ ! other.ptr_; } // 比较操作符与普通iterator类似这里不重复列出 private: const T* ptr_; };反向迭代器则更巧妙它的操作实际上是“反向移动”内部适配正向迭代器。标准库std::reverse_iterator已经做好了这层适配我们直接用就行。using reverse_iterator std::reverse_iteratoriterator; using const_reverse_iterator std::reverse_iteratorconst_iterator; reverse_iterator rbegin() { return reverse_iterator(end()); } reverse_iterator rend() { return reverse_iterator(begin()); }注意一下这里容易搞混的点rbegin()传入的是end()rend()传入的是begin()。std::reverse_iterator内部会把“当前位置”往前挪一格再接住正向迭代器所以解引用时取的是正确的那个元素。这个细节如果你自己写反向迭代器十个有九个会栽在这里。3.4 用概念Concept约束迭代器C20 的现代做法C20 引入了 Concepts可以用更简洁的方式在编译期约束迭代器的能力。如果你有条件使用 C20代码会比传统的iterator_category方式干净不少#include iterator #include concepts templatetypename Iterator requires std::random_access_iteratorIterator void sort_safe(Iterator first, Iterator last) { std::sort(first, last); }这样写的好处是编译错误信息更友好。你传一个std::list的迭代器进来clang、gcc 会直接告诉你“不满足std::random_access_iterator约束”而不是甩给你几百行的模板报错。对于迭代器这种模板无底洞概念是救人利器。4. 迭代器失效这些坑必须清楚4.1 容器操作与迭代器失效对照表迭代器失效是指当容器发生结构变化插入、删除、扩容后原本持有的迭代器指向的内容不再合法继续使用会导致未定义行为。这是 C 面试和实际开发中最高频的坑没有之一。下面这张表是不同容器在不同操作下的失效情况建议收藏容器插入操作删除操作扩容/重新分配std::vector插入位置之后的所有迭代器失效删除位置之后的所有迭代器失效所有迭代器失效std::deque除两端外所有迭代器失效除两端外所有迭代器失效所有迭代器失效std::list当前迭代器不失效仅被删除的迭代器失效不涉及std::forward_list当前迭代器不失效仅被删除的迭代器失效不涉及std::map/std::set当前迭代器不失效仅被删除的迭代器失效不涉及std::unordered_map插入仅导致该元素迭代器失效重哈希时全部失效仅被删除的迭代器失效重哈希时全部失效记住一个大概规律节点型容器list、map、set只影响被操作的那个元素连续内存容器vector、deque会把“后面的所有迭代器”一起带走。4.2 vector 删除元素时迭代器失效的经典案例最经典的错误出现在vector的erase循环里。直接执行erase后被删元素后面的所有迭代器都失效了包括循环里的it。错误的写法很典型std::vectorint vec {1, 2, 3, 4, 5}; // 错误erase 后 it 已经失效 // for (auto it vec.begin(); it ! vec.end(); it) { // if (*it % 2 0) { // vec.erase(it); // it 失效但循环还在用 // } // } // 正确写法接收 erase 的返回值 for (auto it vec.begin(); it ! vec.end();) { if (*it % 2 0) { it vec.erase(it); // erase 返回下一个有效迭代器 } else { it; } }erase返回“被删除元素的下一个元素的迭代器”这是 STL 标准定义的目的就是让你能连续删除。从 C11 开始所有容器erase都返回迭代器使用起来非常方便。4.3 删除特定元素的两个推荐方案实际编码中我建议优先用std::remove_if搭配erase的组合。这是 STL 里的经典“删除-擦除”惯用法一行搞定避免了手写循环出错的概率std::vectorint vec {1, 2, 3, 4, 5, 6, 7, 8}; // 删除所有偶数 vec.erase(std::remove_if(vec.begin(), vec.end(), [](int x) { return x % 2 0; }), vec.end());这段代码的原理值得展开说std::remove_if并不会真的删除元素它是把所有“不该被删除”的元素往前移动到前面然后把“新逻辑末尾”迭代器返回给你。接着erase从逻辑末尾一直删到容器末尾。很多新手第一次看到这段代码会懵但理解了 remove 和 erase 的分工后你会觉得这个设计非常优美——算法只负责搬数据容器才负责释放空间。如果需要按下标删除并且保持原顺序也有一招从后往前删。因为从后往前删除时已经处理过的部分不会受到影响for (size_t i vec.size(); i 0; --i) { if (vec[i - 1] % 2 0) { vec.erase(vec.begin() i - 1); } }4.4 缩小容量别乱调用学会交换释放有一个常见的错误认识clear()之后容量并不释放。如果程序长期把大容量的容器占着内存又频繁扩容、收缩内存碎片会越来越严重。想让 vector 真正释放底层内存得用“交换清空”技巧std::vectorint vec(1000, 1); std::vectorint empty; vec.swap(empty); // vec 的 capacity 变为0内存释放这个技巧常被面试官拿来考“如何释放 vector 的容量”。相比之下clear()和resize(0)都会保留 capacity内存没有真正归还。另外要注意不要缓存end()迭代器再进入循环。end()的返回值会随insert、erase操作失效即使容器本身没有重新分配内存也不能保证end仍然指向原来的位置。每次循环用it ! vec.end()重新获取才是最稳妥的。5. 迭代器模式与设计模式的关系5.1 经典 GoF 迭代器 vs STL 迭代器GoF 设计模式里的迭代器模式定义非常标准提供一个对象让它顺序访问聚合对象中的各个元素又不暴露内部表示。STL 的迭代器思想遵循的是同一套哲学但 C 给出了比 Java、C# 更彻底的实现方式。最显著的差异是算法归属。在传统面向对象语言里比如 Java 的List接口自带Iterator()方法迭代逻辑和容器在类继承体系内绑定。而 C STL 里迭代器独立存在算法是全局模板函数容器只负责提供begin()/end()。这就让“算法、容器、迭代器”三者完全解耦也让你可以跳过容器直接使用迭代器。比如std::istream_iterator它根本没有容器它就是从输入流里一遍遍读出数据std::istream_iteratorint begin(std::cin); std::istream_iteratorint end; std::vectorint nums(begin, end);这个能力Java 和 C# 的迭代器模式很难做到。5.2 什么时候真的需要自己实现迭代器模式有一种常见疑问既然 STL 容器都已经提供了迭代器为什么还要学设计模式里的迭代器模式我的回答是当你自己在写一个“内部结构复杂、但不方便直接暴露”的类时迭代器模式依然有用。比如你有一个日志聚合类内部既有文件句柄又有数据库连接还有内存缓冲你想给外部提供统一的遍历接口但完全不想让调用方知道这些细节。这时候你可以实现一个自定义迭代器把“从三个来源取数据”的差异封装到迭代器内部。class LogAggregator { public: // 内部实现可以是文件、数据库、内存的混合 class iterator { public: std::string operator*() const; iterator operator(); bool operator!(const iterator other) const; // ... }; iterator begin(); iterator end(); };这种自自定义迭代器的核心职责就是“把三个源的读取逻辑收敛到和*中”。调用方只看到for (const auto log : aggregator)但对于日志到底来自哪里毫不知情。这就是模式的价值。6. 常见问题与排查技巧实录6.1begin()返回的到底是不是迭代器与指针怎么区分begin()返回的是迭代器。对于std::vector这种连续内存容器begin()的内部确实常常封装了一个原生指针但它仍然是一种迭代器。你返回一个T*和一个iterator对调用方来说往往行为一样但对模板推导、对类型安全、对未来改成链表影响巨大。判断一个类型是否是迭代器最简单的方法是看它是否定义了iterator_category那五个关联类型并且是否能用std::iterator_traits提取出来。原生指针之所以算随机访问迭代器就是因为std::iterator_traitsT*有对应的特化版本。6.2std::distance和std::advance使用技巧std::distance(first, last)返回两个迭代器之间的距离std::advance(it, n)让迭代器前进 n 步。它们根据迭代器能力自动选择最优路径。std::listint lst {10, 20, 30, 40}; auto it lst.begin(); std::advance(it, 2); // 链表只能一步步走O(n) // 但写起来比循环简洁得多 std::vectorint vec {1, 2, 3, 4, 5}; auto vit vec.begin(); auto d std::distance(vit, vec.end()); // O(1)直接相减如果迭代器是list的双向迭代器你可以放心用std::distance如果容器非常大又想走捷径就会出现“看似一步到位实际 O(n) 遍历”的情况。注意不要对随机访问迭代器过度使用std::distance比如每次加一都重算会白费 O(n) 的时间。6.3 调试迭代器失效的实用方法迭代器失效是最难排查的运行时错误之一因为表面症状五花八门崩溃、乱码、死循环甚至程序跑得好好的但结果不对。我的排查经验按优先级排序开启 STL 调试模式。libstdc 中定义_GLIBCXX_DEBUGMSVC 中开启迭代器调试。它们能在越界访问迭代器、解引用失效迭代器时给出断言提示早发现问题。在erase/insert后打日志打印distance(begin(), it)。如果距离变成负数或者超出 size立即知道迭代器可能失效。减少“持有长期迭代器”的习惯。除非是map/list这种节点型容器否则不要在容器结构变化后还抱着旧迭代器不放。使用地址消毒器和内存消毒器AddressSanitizer跑一遍。ASan 对越界访问、释放后使用非常敏感能快速定位问题位置。6.4 容易忽略的vectorbool迭代器特例std::vectorbool是出了名的“假容器”。它的迭代器取值时不能像正常的bool那样直接拿引用因为bool被压缩在 bit 里标准库用代理对象模拟。所以你无法写出bool ref *it这样的代码除非用一个临时变量。std::vectorbool vb {true, false, true}; auto it vb.begin(); // bool ref *it; // 编译错误不能绑定代理对象到 bool bool v *it; // 正确对绝大多数场景vectorbool的内存节省意义并不大反而会带来各种奇怪的约束。如果可能我推荐用std::vectorchar或dequebool代替。如果你想了解空间优化C 里还有std::bitset是更合适的选择。6.5 迭代器失效排查实录一个真实的崩溃案例说一个我调试过的真实案例。某接口返回std::vectorItem的常引用但在处理业务流程时我先把这个 vector 转存成了局部副本再拿副本的迭代器删了一部分元素接着又用原始接口里的另一个迭代器去访问数据直接崩溃。当时的现象断断续续崩溃出问题的指针指向的内容变成垃圾。排查过程费了好大劲因为崩溃发生在很远的后续代码里根本想不到根因是erase。后来我把代码拆开缩小到最小复现 case确认原样如此void process(const std::vectorItem source) { std::vectorItem local source; // 局部拷贝 auto it local.begin(); while (it ! local.end()) { if (condition(*it)) { it local.erase(it); } else { it; } } // 此处如果错误地继续用 local.end() 的缓存也会失效 for (auto p local.begin(); p ! local.end(); p) { // ... } }教训很简单但深刻不要在erase/insert之后保留任何旧迭代器。每一次都从begin()重新获取或者严格使用返回值。7. 写在最后的几句大实话回到迭代器模式这个主题我最后想分享一点个人体会。如果你刚开始学 C迭代器可能是第一个让你“触到模板泛型脉络”的概念。它能解释为什么 STL 能一通百通为什么std::sort能兼容数组和vector为什么接口设计要“小而精确”。花时间手写一个迭代器远比你背十条八股文收获更大。如果你已经在实际项目中写过不少 C迭代器失效和迭代器分类仍然值得反复咀嚼。尤其是从手写容器过渡到使用span、ranges这些现代特性时你会发现底层都是迭代器思想在支撑。真把迭代器的分类、traits、失效规则吃透了后面看带范围库的代码会轻松非常多。最后留一个自测题如果你有一个自定义的二叉树容器你打算提供哪一类迭代器如果要求支持中序遍历需要实现哪些操作符想清楚这个问题你对迭代器模式的理解就基本到位了。