【设计模式精讲】21.迭代器模式(Iterator)

【设计模式精讲】21.迭代器模式(Iterator)

【摘要】:for (Node* p = list.head(); p; p = p->next_)------为了遍历,链表被迫把节点结构公开,客户端被迫知道 head 和 next 的存在;换容器、换遍历序,全部调用点重写。本文从这行经典的越界遍历讲起,给出 GoF 意图与经典四操作接口,再完成 C++ 特有的关键一跃:按 STL 形态改造自己的容器------begin()/end() 加四个运算符,range-for 与上百个算法免费到手;随后讲透迭代器五类承诺与失效规则这两个工程硬契约,展望 C++23 生成器。文末对照标准库本体、folly 的协程生成器与 POCO 的数据库游标。这是 23 种模式里唯一被语言和标准库整体接管的一个------学它,就是学 STL 的世界观。

【关键词】:迭代器、聚合、遍历、STL、迭代器分类、失效规则、生成器

【代码基准】:C++17

1. for 循环里的 Node*,把链表的内部泄给了全世界

项目里有个自研的双向/单向链表(承载历史数据结构),业务代码这样遍历它:

cpp 复制代码
// 说明性片段
// ❌ 遍历一趟,内部结构全暴露
for (Node* p = list.head(); p != 0;
     p = p->next_) {
  total += p->value;
}

这行循环看似无辜,实际完成了三次越权:客户端知道了节点类型 Node 的存在 、知道了链表用 head() 入口 、还拿到了节点间的连接字段 next_**------第三样逼得链表把 Nodenext 设为 public,封装从这一行开始漏气。代价随后到来:数据结构换成块状链表或跳表,所有遍历点重写;想要「逆序遍历」「中序遍历」,每种聚合 × 每种遍历序的组合都要单独写循环;二分查找、最大值这类与存储无关的算法**被迫在每个容器里各抄一份。

病灶:遍历逻辑与聚合的内部表示焊死。遍历本是一种可以独立变化的关注点------它只依赖「有元素、有先后」,不依赖元素具体住在链表里还是树里。把这层关注点抽出来单独成对象,就是迭代器模式;而 C++ 把这件事做到了所有语言里的极致------后面会看到,STL 整个体系就站在迭代器上。

2. 模式意图与定义

  • 一句话定义 :提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示。
    解决的问题:遍历逻辑与聚合结构解耦------客户端不认识节点、不认识容器类型,只按统一协议逐个取元素。
  • GoF 原文意图Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation. (提供一种访问聚合对象元素的方法......)注意两个关键词:sequentially (顺序性是协议的全部)与 without exposing(封装是目的本身)。
  • Refactoring Guru 的表述:迭代器模式让你能在不暴露集合底层实现(列表、栈、树等)的情况下,遍历集合中所有的元素;你可以为同一集合实现多种遍历(正向、逆向、中序),彼此互不干扰。

在 23 种模式里,迭代器享受着独一无二的地位:C++ 标准库把它变成了地基 。STL 的三分天下------容器(存数据)、算法(加工数据)、迭代器(连接两者)------中间那一极就是本模式:std::sort 不认识 vector 也不认识 deque,它只吃两个迭代器。于是「一处实现算法、所有容器受益」成为现实,本篇的核心任务之一就是把你自己的容器也接入这套体系

3. UML 图 + 结构说明

#mermaid-svg-0emteUV9aHf14pYv{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0emteUV9aHf14pYv .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0emteUV9aHf14pYv .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0emteUV9aHf14pYv .error-icon{fill:#552222;}#mermaid-svg-0emteUV9aHf14pYv .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0emteUV9aHf14pYv .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0emteUV9aHf14pYv .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0emteUV9aHf14pYv .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0emteUV9aHf14pYv .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0emteUV9aHf14pYv .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0emteUV9aHf14pYv .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0emteUV9aHf14pYv .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0emteUV9aHf14pYv .marker.cross{stroke:#333333;}#mermaid-svg-0emteUV9aHf14pYv svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0emteUV9aHf14pYv p{margin:0;}#mermaid-svg-0emteUV9aHf14pYv g.classGroup text{fill:#9370DB;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-0emteUV9aHf14pYv g.classGroup text .title{font-weight:bolder;}#mermaid-svg-0emteUV9aHf14pYv .cluster-label text{fill:#333;}#mermaid-svg-0emteUV9aHf14pYv .cluster-label span{color:#333;}#mermaid-svg-0emteUV9aHf14pYv .cluster-label span p{background-color:transparent;}#mermaid-svg-0emteUV9aHf14pYv .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-0emteUV9aHf14pYv .cluster text{fill:#333;}#mermaid-svg-0emteUV9aHf14pYv .cluster span{color:#333;}#mermaid-svg-0emteUV9aHf14pYv .nodeLabel,#mermaid-svg-0emteUV9aHf14pYv .edgeLabel{color:#131300;}#mermaid-svg-0emteUV9aHf14pYv .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-0emteUV9aHf14pYv .label text{fill:#131300;}#mermaid-svg-0emteUV9aHf14pYv .labelBkg{background:#ECECFF;}#mermaid-svg-0emteUV9aHf14pYv .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-0emteUV9aHf14pYv .classTitle{font-weight:bolder;}#mermaid-svg-0emteUV9aHf14pYv .node rect,#mermaid-svg-0emteUV9aHf14pYv .node circle,#mermaid-svg-0emteUV9aHf14pYv .node ellipse,#mermaid-svg-0emteUV9aHf14pYv .node polygon,#mermaid-svg-0emteUV9aHf14pYv .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0emteUV9aHf14pYv .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-0emteUV9aHf14pYv g.clickable{cursor:pointer;}#mermaid-svg-0emteUV9aHf14pYv g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-0emteUV9aHf14pYv g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-0emteUV9aHf14pYv .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-0emteUV9aHf14pYv .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-0emteUV9aHf14pYv .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-0emteUV9aHf14pYv .dashed-line{stroke-dasharray:3;}#mermaid-svg-0emteUV9aHf14pYv .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-0emteUV9aHf14pYv #compositionStart,#mermaid-svg-0emteUV9aHf14pYv .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-0emteUV9aHf14pYv #compositionEnd,#mermaid-svg-0emteUV9aHf14pYv .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-0emteUV9aHf14pYv #dependencyStart,#mermaid-svg-0emteUV9aHf14pYv .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-0emteUV9aHf14pYv #dependencyStart,#mermaid-svg-0emteUV9aHf14pYv .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-0emteUV9aHf14pYv #extensionStart,#mermaid-svg-0emteUV9aHf14pYv .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-0emteUV9aHf14pYv #extensionEnd,#mermaid-svg-0emteUV9aHf14pYv .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-0emteUV9aHf14pYv #aggregationStart,#mermaid-svg-0emteUV9aHf14pYv .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-0emteUV9aHf14pYv #aggregationEnd,#mermaid-svg-0emteUV9aHf14pYv .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-0emteUV9aHf14pYv #lollipopStart,#mermaid-svg-0emteUV9aHf14pYv .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-0emteUV9aHf14pYv #lollipopEnd,#mermaid-svg-0emteUV9aHf14pYv .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-0emteUV9aHf14pYv .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-0emteUV9aHf14pYv .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-0emteUV9aHf14pYv .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-0emteUV9aHf14pYv .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-0emteUV9aHf14pYv :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 游标四操作
创建
只认迭代器
仅一次创建请求
<<interface>>
Iterator
+first() : void
+next() : void
+isDone() : bool
+currentItem() : T
ListIterator
-cur_ Node
+first() : void
+next() : void
+isDone() : bool
+currentItem() : T
List
+createIterator() : Iterator
Client

GoF 的经典接口是四操作游标first() 回到起点、next() 前进一格、isDone() 询问是否到头、currentItem() 取当前元素------迭代器自己知道何时结束(到尾返回空/越界标志)。

而 STL 的接口是一对区间迭代器begin() 指向首元素、end() 指向尾后位置,前进用 operator++、取值用 operator*、相等用 operator!=,「结束」是「与 end() 相等」这个事实而不是一次询问。这个差异不是口味问题,它带来三个实质红利:子区间是一等公民 (任意两个迭代器圈出一段序列,算法直接加工半截数据);算法与容器彻底无关 (算法参数里没有容器,只有区间);哨兵可扩展 (C++17 常量表达式哨兵、C++20 无穷范围都从这里长出来)。代价是「有效区间」的维护责任落到调用者手上------end() 不是「最后元素」而是「尾后」,初学者的第一课通常栽在这里。

4. 传统 C++ 写法(C++11 之前)

GoF 原书时代的形态:聚合负责创建迭代器(createIterator),四操作游标走天下,迭代器靠 friendship 探进容器内部:

cpp 复制代码
// C++98/03 写法
#include <stdio.h>

template <typename T>
class List {
public:
  List() : head_(0) {}

  void push_front(const T& v) {
    head_ = new Node(v, head_);
  }

  class Iterator;                 // 先声明为友
  Iterator* createIterator() const;

private:
  struct Node {                   // 内部表示不外泄
    Node(const T& v, Node* n)
        : value(v), next(n) {}
    T value;
    Node* next;
  };

  Node* head_;
  friend class Iterator;          // C++98:嵌套类
};                                // 不会自动获得访问权

template <typename T>
class List<T>::Iterator {
public:
  Iterator(const List& l)
      : list_(l), cur_(l.head_) {}

  void first() {
    cur_ = list_.head_;
  }
  void next() {
    cur_ = cur_->next;
  }
  bool isDone() const {
    return cur_ == 0;
  }
  const T& currentItem() const {
    return cur_->value;
  }

private:
  const List& list_;
  typename List<T>::Node* cur_;
};

template <typename T>
typename List<T>::Iterator*
List<T>::createIterator() const {
  return new Iterator(*this);
}

int main() {
  List<int> nums;
  nums.push_front(3);
  nums.push_front(2);
  nums.push_front(1);

  // 客户端:只认迭代器,不知道 Node
  List<int>::Iterator* it =
      nums.createIterator();
  for (it->first(); !it->isDone();
       it->next()) {
    printf("%d\n", it->currentItem());
  }
  delete it;
  return 0;
}

对照第 1 节:Node 回到 private,客户端对链表内部一无所知;逆序遍历就是再写一个 ReverseIterator,聚合与已有调用点零改动。

三条传统写法的铁律:迭代器是容器授权的望远镜 ------friendship 把访问权下放给迭代器而非全世界,封装与遍历各得其所(C++98 里嵌套类不会自动获得外围类访问权,那句 friend class Iterator 必须显式写;C++11 起才改由语言默认放行);创建归容器、删除归客户 ------createIterator 返回裸指针,客户用完 delete,GoF 原书专门讨论过这处分工,也正是裸指针时代最易漏的一环;遍历中修改容器的隐患从第一天就存在 ------next() 读到的节点可能已被删除,失效问题第 5 节专门收账。

5. 现代 C++ 进阶写法

升级零:把容器接入 STL------begin()/end() 与四个运算符 。现代 C++ 里「自定义迭代器」的默认目标不是 GoF 四操作,而是 STL 形态;四个运算符到位后,range-for 与整个 <algorithm> 免费到手:

cpp 复制代码
// 节选:LinkedList 的 STL 化改造
#include <utility>

template <typename T>
class LinkedList {
public:
  void push_front(T v) {
    head_ = new Node{std::move(v), head_};
  }

  struct iterator {
    Node* node_ = nullptr;

    T& operator*() const {
      return node_->value;      // 取值
    }
    iterator& operator++() {
      node_ = node_->next;      // 前进
      return *this;
    }
    bool operator!=(const iterator& o)
        const {
      return node_ != o.node_;  // 是否到 end
    }
  };

  iterator begin() { return {head_}; }
  iterator end() { return {nullptr}; }
  // 需要只读遍历时,同型再造一套
  // const_iterator + begin() const / end() const

private:
  struct Node {
    T value;
    Node* next;
  };
  Node* head_ = nullptr;
};

// 白拿的三件套:
// for (auto& v : list) { ... }              遍历
// auto it = std::find(list.begin(),
//                     list.end(), 7);       查找
// int s = std::accumulate(list.begin(),
//                         list.end(), 0);   求和

C++11 起嵌套类自动获得外围类的私有访问权,friend 声明都不再需要;节点结构依旧深藏。这一步的哲学与第 10 篇的 std::queue 一脉相承:不要自己发明遍历协议,接入已有的那套------你的容器从此活在上百个算法的生态里。

改进一:迭代器五类------如实承诺你的能力 。STL 把迭代器按能力分档,算法按所需最低档选择重载,你的容器应承诺全能兑现的最低档

  • 输入/输出迭代器 :单遍扫描(istream_iterator 读流),前进、取值各一次;
  • 前向迭代器 :多遍扫描、只前进(forward_listunordered_map);
  • 双向迭代器 :再加 operator--listsetmap);
  • 随机访问迭代器 :再加 it + n、下标、大小比较(vectordeque);
  • 连续迭代器 (C++20):物理连续(vectorarrayspan)。

分档的直接后果落在算法上:std::sort 要求随机访问------所以 std::list 只能提供成员版 list::sortstd::reverse 要求双向;std::advance/std::distance 对低档迭代器退化为逐步走。承诺过高 (比如给链表配上 +n)会骗过算法重载、直接未定义行为------迭代器分类是能力承诺,不是简历。

改进二:失效规则------迭代器是「未托管句柄」 。迭代器本质是指针的推广,也就继承了指针的全部命门(第 16 篇的替身在这里缺席):容器一变,句柄悬垂。几条必须背下的硬契约------vector 扩容后全体失效 (迁移新家,旧地址作废);vector 中间插入/删除,之后的失效list 删除只失效指向该节点 的迭代器;unordered_map rehash 全体失效erase 只失效被删者)。两条工程对策:先改后用 (修改容器后重新取 begin()/end(),范围 for 内不要做会扩容的修改------它把 end() 缓存在了循环开始处);开迭代器调试 (libstdc++ 的 _GLIBCXX_DEBUG、MSVC 的 checked iterators 把违规从「深夜崩溃」提前成「当场断言」)。

展望 :C++20 ranges 把「视图」叠在迭代器之上(第 13 篇已见其组合语义);C++23 的 std::generator 用协程弥合了「递归结构的外遍历」------树的深度优先不再需要显式栈,写成顺序的 co_yield 即可,folly 早已给出同型实现(见第 7 节)。

6. 优缺点与适用场景

  • ✅ 优点(GoF 后果清单 + STL 证实):遍历与表示解耦 ------换容器、加遍历序,调用点不动;算法大规模复用 ------STL 上百个算法只认迭代器对,你的容器接入即全部可用;多遍历策略并存 (正向/逆向/层序,各自独立游标);支持惰性与无限序列------输入迭代器读流、生成器按需产出,「元素可以还不存在」。
  • ❌ 缺点:失效规则是文档契约而非类型契约 ------比裸指针更隐蔽,因为它看起来更安全;简单遍历也要一层间接 ------链表迭代器跳转对缓存不友好,热路径有时退回手写循环更快;自定义迭代器样板重------四运算符只是门槛,const/非 const 两套、五类承诺、异常安全都要照应(C++20 前没有生成器兜底)。
  • 🎯 适用场景:自定义聚合结构要接入标准生态;内部表示不能外泄(闭哈希、跳表、惰性加载);同一聚合需要多种遍历序;数据是流式或无限生成的。

〔辨析〕迭代器 vs 组合(第 12 篇)的 walk 内遍历(第 18 篇亦提):内遍历把递归封装在容器方法里,客户端只能「对每个元素做什么」,不能中途 break、不能套算法 ;迭代器外遍历一切皆可,代价是递归结构要显式管理栈------C++23 生成器恰好补上这块短板。迭代器 vs 访问者(第 28 篇):迭代器按位置顺序 交付元素,访问者按元素类型 分派操作,两者常组合成「迭代 + 分派」的流水线。迭代器 vs 代理(第 16 篇):代理控制对单个对象 的访问,迭代器管理对一群元素的顺序访问。

7. 开源项目中的身影

标准库:STL 本体就是迭代器的帝国<algorithm> 里上百个算法没有一个是容器的成员(sort/find/accumulate/count_if...),全部只吃区间;连「文件」与「标准输入」都能被 istream_iterator 包装成序列卷进算法:

cpp 复制代码
#include <algorithm>
#include <iostream>
#include <iterator>
#include <numeric>
#include <vector>

std::vector<int> v{3, 1, 4, 1, 5};
auto it = std::find(v.begin(), v.end(), 4);

int sum = std::accumulate(
    std::istream_iterator<int>(std::cin),
    std::istream_iterator<int>(), 0);
// 键入几个整数回车------流即序列

点评:GoF 里「一个聚合配一个遍历协议」的模式,在标准库里长成了「一切数据源共用一套区间协议」的宪法。第 17 篇说「模式成熟到看不见就成了基础设施」------迭代器是最彻底的那例。

Folly:folly::coro::Generator,生成器先于标准落地 。C++23 std::generator 的工业前辈,把「按需逐个产出」写成协程,无限与递归序列都不在话下:

cpp 复制代码
// 说明性片段(需包含 folly/coro/Generator.h)
folly::coro::Generator<int> iota() {
  for (int i = 0;; ++i)
    co_yield i;     // 无限序列,惰性产出
}

// for (int v : iota()) {
//   if (v > 3) break;   // 外遍历:可随时停
// }

点评:生成器把第 5 节「递归结构外遍历难」的历史欠账一次结清------深度优先树遍历写成几个 co_yield,调用方却仍享有 break 与算法组合的完整自由。

POCO:数据库游标是迭代器的「远程形态」Poco::Data::RecordSet 把查询结果包装成可遍历的行序列,行内再按列取值------网络往返的批次与游标语义被迭代器协议整体盖住:

cpp 复制代码
// 说明性片段(需链接 PocoData,签名有简化)
Poco::Data::Session ses("SQLite", "db.sqlite");
Poco::Data::Statement stmt(ses);
stmt << "SELECT name FROM users";
stmt.execute();

Poco::Data::RecordSet rs(stmt);
for (auto& row : rs) {
  std::string name =
      row[0].convert<std::string>();
}

点评:结果集可能大到不该一次载入------迭代器协议让「逐块拉取」与「内存 vector」用起来毫无差别。惰性,是迭代器送得起的最贵的礼物

三份代码合看:标准库以迭代器立国、Folly 用协程把迭代器推广到生成器、POCO 用它遮蔽网络游标------「顺序访问、不问表示」这八个字,值得写在每个自造容器的头文件注释里。

本篇小结

遍历是被低估的关注点:抽出来,容器才能守住内部表示,算法才能一次实现处处复用。GoF 的四操作游标解决了第一步;C++ 的答案更进一步------begin()/end() 加四运算符接入 STL 区间协议,range-for 与百般算法随之白拿,代价是背下两份硬契约:五类能力只承诺最低档,失效规则只信文档不信直觉。简单遍历偶尔仍是热路径的对手,递归结构的外遍历曾靠显式栈硬撑------生成器正在补位。下一篇中介者模式,从「依次访问一群元素」转向「让一群对象互相协作」:网状的互相引用如何收拢成星型。

本文模式定义与角色划分参考了 Refactoring Guru《设计模式》中文版「迭代器」一章,意图译文与「谁创建/谁删除迭代器」的讨论参考了 GoF《Design Patterns》第 5 章 Iterator 一节,迭代器分类与失效规则参考了 C++ 标准库的约定俗成。

相关推荐
梁辰兴1 小时前
软件工程:软件测试的基本步骤
软件测试·软件工程·环境搭建·测试设计·测试计划·梁辰兴·测试执行
YYYing.1 小时前
【C++进阶系列 (一)】关于线程堆栈的那些事 (上篇)
c语言·开发语言·c++·线程堆栈
努力努力再努力wz1 小时前
【Redis入门系列】:从 RESP 协议到 redis-plus-plus:Redis 客户端编程与 C++ 接口设计
开发语言·数据库·c++·redis·分布式·缓存·架构
知兀1 小时前
Tailwindcss报错:没有与此调用匹配的重载
前端·c++·tailwindcss
梁辰兴2 小时前
软件工程:软件测试的目的和重要性
软件测试·软件工程·梁辰兴·软件测试原则·软件测试的重要性·软件测试的目的·软件测试类型
天天喝旺仔2 小时前
gRPC 流式通信实战:从一元调用到双向流,吃透四种 RPC 模式(Go 版)
分布式·http·微服务·rpc·go
艾莉丝努力练剑3 小时前
【AI大模型接入SDK】ChatGPT API
网络·c++·人工智能·websocket·网络协议·学习·chatgpt
无小道4 小时前
C/C++——atomic小记
c++·cas·无锁
千谦阙听12 小时前
C++类和对象(中):默认成员函数、构造与析构、拷贝构造、运算符重载
开发语言·c++·学习