在不暴露集合内部结构的前提下,提供统一方式顺序访问集合元素 —— C++/Qt 每日设计模式 · 2026-08-27
Day 15:迭代器模式(Iterator Pattern)
一句话定义:迭代器模式提供一种方法,顺序访问一个聚合对象(集合)中的各个元素,而又不需要暴露该对象的内部表示(底层存储结构)。
我们在第 13 天学习了责任链模式(请求沿链传递)、第 14 天学习了命令模式(请求封装为对象),它们解决的都是"请求如何流动"的问题;今天进入行为型模式第三站,解决的是"数据元素如何被访问"的问题:怎么让外部代码遍历一个集合,却完全不碰集合的底细?比如客户端遍历一棵二叉树时,它只看到"一个有序序列",根本不知道背后是树、链表还是数组——这就是迭代器要做的。
在 GoF 23 种模式中,迭代器属于行为型模式,位于责任链、命令之后,是学习顺序上的第 13 个模式。它也是标准库与 Qt 容器使用最频繁、但最容易被忽视的模式——你每天都在用 range-for、std::begin/end、QListIterator,只是没有意识到它们的背后就是迭代器模式。
先看一段"没有迭代器"的坏代码。假设公司有一个日志系统,日志目前存放在 std::vector 里,两个业务模块各自需要处理日志:
// 坏味道示例:直接暴露内部容器,业务代码与 vector 强耦合
#include <vector>
#include <string>
#include <iostream>
struct LogEntry {
int level; // 0=Info 1=Warning 2=Error
std::string msg;
};
// 业务模块 A:统计 Error 数量 —— 自己写一遍遍历
int countErrors(const std::vector<LogEntry>& logs) {
int n = 0;
for (size_t i = 0; i < logs.size(); ++i)
if (logs[i].level >= 2) ++n;
return n;
}
// 业务模块 B:打印全部日志 —— 又写一遍遍历
void printLogs(const std::vector<LogEntry>& logs) {
for (size_t i = 0; i < logs.size(); ++i)
std::cout << logs[i].msg << "\n";
}
这段代码有什么问题?
1. 强耦合:countErrors 和 printLogs 的签名直接写死 std::vector<LogEntry>。业务代码与"具体容器类型"绑死在一起。哪天日志系统要改成按天分片存储(每个文件存一天)、要存进数据库用游标读取、或者换成 std::list,所有调用方的签名和遍历代码全部要改。
2. 遍历逻辑重复:每个业务模块都自己写一套 for 循环。遍历方式(正序、倒序、分页、按时间窗口、只取 Error)无法复用,过滤条件散落在各个函数里。将来想统一"日志脱敏后输出",要改遍所有调用点。
3. 破坏封装:业务代码直接使用 operator[],等于假设了"元素在内存里连续排列、可以随机访问"。容器想换成链表、二叉树、分片文件,这种假设立刻崩塌。
4. 违反 SOLID:违反依赖倒置原则 DIP——高层业务模块依赖了低层具体容器,而不是依赖抽象的遍历接口;也违反开闭原则 OCP——增加一种存储结构,就要修改所有调用方,而不是新增一个迭代器了事。
5. 无法扩展遍历方式:想加"倒序遍历""分页遍历",只能在每个调用方再写一遍,容器类本身也会被各种遍历需求撑得越来越臃肿。
需求继续增加时,这些问题会指数级放大:日志模块换存储 → 10 个业务函数编译失败;加一种遍历需求 → 10 个业务函数各写 10 遍。结论:"遍历"这件事不应该由每个业务模块各写一遍,也不应该绑死在具体容器上,它需要被抽象出来、独立成对象——这就是迭代器模式。
生活类比:博物馆导游。你逛一座大型博物馆,藏品仓库里有画、有雕塑、有化石,摆放得乱七八糟,但你完全不需要知道它们怎么存放——你只需要跟着导游走,导游说"下一件展品是 X",你就看 X。想看不同路线(只逛名画、按年代逛),换一个导游就行,你作为游客的体验完全不变。这里:游客 = 客户端,导游 = 迭代器,博物馆藏品仓库 = 集合内部结构。
软件工程本质:迭代器模式把两个本来纠缠在一起的责任拆开:
hasNext()/next(),或 STL 风格的 begin()/end())。迭代器就是"访问协议"与"存储实现"之间的绝缘层:客户端只认识迭代器这个"导游",导游认识仓库(内部结构)。于是存储随便换、遍历方式随便加,客户端一行代码都不用动。
核心口诀:遍历算法从容器中剥离,调用方只与迭代器接口对话。容器负责"存什么",迭代器负责"怎么走"。C++ 中 STL 的 begin()/end() + range-for,就是这一思想的标准落地。
迭代器模式只有四个角色,结构非常简单:
┌────────────────┐ 使用 ┌─────────────────┐
│ Client │ ───────────▶ │ Iterator │ (抽象接口)
│ (业务调用方) │ │ hasNext() │
└────────────────┘ │ next() │
│ └────────┬────────┘
│ 通过 createIterator() 获取 ▲ 实现
▼ │
┌────────────────┐ ┌─────────────────┐
│ Aggregate │ ◀──────────── │ ConcreteIterator│
│ (抽象聚合) │ 返回具体迭代器 │ 持有集合引用 │
│ createIterator()│ │ + 当前游标位置 │
└────────────────┘ └─────────────────┘
角色职责:
begin()/end()/operator++/operator*;GoF 经典风格则是 hasNext()/next()(Qt 的 Java 风格迭代器就是这种)。createIterator(),返回迭代器。createIterator(),返回自己的具体迭代器,把"访问权"交出去,但内部存储依然保密。谁依赖谁:Client 只依赖 Iterator 接口和 Aggregate 接口,对 Concrete 类一概不知;ConcreteIterator 依赖 ConcreteAggregate 的内部结构(二者同属一个集合体系,这种耦合是合理的)。扩展点在哪里:新增一种集合,只需新增 ConcreteAggregate + 配套 ConcreteIterator,客户端零改动——这就是开闭原则。
下面的例子实现一棵二叉搜索树,并用显式栈实现中序遍历迭代器(不递归,因为迭代器的意义就是"把遍历变成可以随时暂停/继续的顺序访问")。客户端只看到"有序序列",完全不知道树的存在:
// iterator_demo.cpp —— C++17 完整可编译示例
#include <iostream>
#include <memory>
#include <stack>
// 二叉搜索树节点:用 unique_ptr 管理子节点,RAII 自动释放,无内存泄漏
struct TreeNode {
int value;
std::unique_ptr<TreeNode> left;
std::unique_ptr<TreeNode> right;
explicit TreeNode(int v) : value(v) {}
};
// 中序遍历迭代器:隐藏"树"的内部结构,对外提供"有序序列"的视角
class InOrderIterator {
public:
// 构造时从根节点开始,一路向左压栈(找到最小的元素)
explicit InOrderIterator(TreeNode* root) {
pushLeft(root);
}
bool hasNext() const { return !stack_.empty(); }
// 取出当前最小元素,然后把指针转向其右子树(同样一路向左)
int next() {
TreeNode* cur = stack_.top();
stack_.pop();
int result = cur->value;
pushLeft(cur->right.get());
return result;
}
private:
std::stack<TreeNode*> stack_; // 显式栈代替递归,遍历可随时暂停/恢复
void pushLeft(TreeNode* node) {
while (node) {
stack_.push(node);
node = node->left.get();
}
}
};
int main() {
// 构建一棵树: 4
// / \
// 2 6
// / \ / \
// 1 3 5 7
auto root = std::make_unique<TreeNode>(4);
root->left = std::make_unique<TreeNode>(2);
root->left->left = std::make_unique<TreeNode>(1);
root->left->right = std::make_unique<TreeNode>(3);
root->right = std::make_unique<TreeNode>(6);
root->right->left = std::make_unique<TreeNode>(5);
root->right->right = std::make_unique<TreeNode>(7);
// 客户端只与迭代器对话:拿到的是"1 2 3 4 5 6 7"的有序序列
// 它根本不知道数据存在一棵树里 —— 这就是封装
InOrderIterator it(root.get());
std::cout << "中序遍历结果: ";
while (it.hasNext()) {
std::cout << it.next() << " ";
}
std::cout << std::endl;
return 0;
}
逐段解释:
TreeNode 用 std::unique_ptr 持有左右孩子,树析构时整棵子树自动释放,符合 RAII,无手动 delete、无泄漏。InOrderIterator 是核心:它内部持有一个 std::stack<TreeNode*> 作为"显式调用栈",把递归中序遍历改写成循环——这就是迭代器比递归强的地方:遍历状态(栈)独立保存在迭代器对象里,随时可以暂停,下次接着走(递归做不到暂停恢复,除非用协程)。pushLeft 一路向左压栈,栈顶就是当前最小元素;next() 弹出栈顶,输出它,再对其右子树执行 pushLeft——这正是中序遍历"左-根-右"的迭代版。main() 里客户端只做两件事:构造迭代器、循环 hasNext()/next()。客户端对树的内部结构零感知,将来树换成"平衡树 + 线程安全版本",只要迭代器接口不变,main 一行不改。程序输出:
中序遍历结果: 1 2 3 4 5 6 7
顺带一提:标准库已经把这件事做到了极致——任何提供 begin()/end() 的容器都可以用 range-for 遍历,std::vector、std::map 的迭代器各自隐藏了"连续数组"和"红黑树"两种完全不同的内部结构。你的日常代码,就是迭代器模式最广泛的应用现场。
先讲 Qt 自身:Qt 容器(QList/QVector/QMap/QHash)内置了三种迭代方式,每一种都是迭代器模式的不同风格:
begin()/end() 返回迭代器,配合 std::find、std::sort 等标准算法,性能最优。QListIterator、QVectorIterator、QMapIterator 等,接口是 hasNext()/next()/peekNext(),语义清晰、不易写错,遍历中可通过 QMutableXXXIterator 安全增删元素。foreach 宏在 Qt 6 已移除,统一改用 C++11 range-for)。特别值得一提的是 QMap:它内部是红黑树,但它的迭代器按键升序输出——树的中序遍历被完整封装在迭代器里,调用方完全无感,和今天第⑤节的例子如出一辙。
下面做一个完整的 Qt6 Widgets 程序:日志查看器。用 Java 风格迭代器正序遍历并过滤,用可变迭代器在遍历中安全删除 Error 日志:
mainwindow.h
#pragma once
#include <QMainWindow>
#include <QVector>
class QListWidget;
class QPushButton;
// 日志条目结构体
struct LogEntry {
int level; // 0=Info 1=Warning 2=Error
QString message;
};
class MainWindow : public QMainWindow {
Q_OBJECT
public:
explicit MainWindow(QWidget* parent = nullptr);
private slots:
void onIterateClicked(); // 按钮点击:遍历并展示日志
private:
QVector<LogEntry> logs_; // 内部数据集合(调用方永远不直接操作它)
QListWidget* listWidget_ = nullptr;
QPushButton* iterateButton_ = nullptr;
};
mainwindow.cpp
#include "mainwindow.h"
#include <QListWidget>
#include <QPushButton>
#include <QVBoxLayout>
#include <QVectorIterator> // Qt Java 风格只读迭代器
#include <QMutableVectorIterator> // Qt Java 风格可变迭代器
MainWindow::MainWindow(QWidget* parent)
: QMainWindow(parent) {
// 初始化数据集合:模拟 6 条日志
logs_ = {
{0, QStringLiteral("程序启动")},
{1, QStringLiteral("配置文件缺失,已使用默认值")},
{0, QStringLiteral("数据库连接成功")},
{2, QStringLiteral("磁盘写入失败")},
{1, QStringLiteral("内存占用偏高")},
{0, QStringLiteral("任务完成")},
};
auto* central = new QWidget(this);
auto* layout = new QVBoxLayout(central);
listWidget_ = new QListWidget(central);
iterateButton_ = new QPushButton(
QStringLiteral("遍历:过滤 Warning 以上日志,并删除 Error"), central);
layout->addWidget(listWidget_);
layout->addWidget(iterateButton_);
setCentralWidget(central);
connect(iterateButton_, &QPushButton::clicked,
this, &MainWindow::onIterateClicked);
}
void MainWindow::onIterateClicked() {
listWidget_->clear();
// 方式一:只读 Java 风格迭代器 —— 客户端不知道 QVector 内部如何存储
QVectorIterator<LogEntry> it(logs_);
while (it.hasNext()) {
const LogEntry& entry = it.next(); // next():返回当前元素并前移游标
if (entry.level >= 1) { // 只展示 Warning / Error
listWidget_->addItem(
QStringLiteral("[%1] %2").arg(entry.level).arg(entry.message));
}
}
// 方式二:可变迭代器 —— 遍历过程中安全删除所有 Error 日志
QMutableVectorIterator<LogEntry> mut(logs_);
while (mut.hasNext()) {
if (mut.next().level >= 2) {
mut.remove(); // 通过迭代器删除:不破坏遍历,不会迭代器失效
}
}
listWidget_->addItem(
QStringLiteral("--- 清理后剩余日志条数: %1 ---").arg(logs_.size()));
}
main.cpp
#include <QApplication>
#include "mainwindow.h"
int main(int argc, char* argv[]) {
QApplication app(argc, argv);
MainWindow w;
w.resize(520, 380);
w.show();
return app.exec();
}
CMakeLists.txt(Qt 6,C++17)
cmake_minimum_required(VERSION 3.16)
project(IteratorDemo VERSION 1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON) # 自动处理 Q_OBJECT 元对象编译
find_package(Qt6 REQUIRED COMPONENTS Widgets)
add_executable(IteratorDemo
main.cpp
mainwindow.cpp
mainwindow.h)
target_link_libraries(IteratorDemo PRIVATE Qt6::Widgets)
Qt 5 / Qt 6 区别说明:① Qt 5 中 QVector 是独立容器,Qt 6 中 QVector<T> 只是 QList<T> 的别名,二者统一;② Qt 5 的 foreach 宏在 Qt 6 已删除,请使用 range-for;③ Java 风格迭代器头文件在 Qt 6 中仍然完整可用(<QListIterator>、<QVectorIterator>、<QMapIterator>、<QMutableVectorIterator> 等)。本示例在 Qt 6.2+ 下可直接用 CMake 构建运行。
这段示例展示了什么:按钮回调里,业务代码完全通过迭代器访问 logs_——它不知道元素存在连续内存里;删除操作通过 mut.remove() 完成,迭代器内部保证不产生悬空迭代器,这正是"遍历逻辑独立于存储结构"的实战价值。
纯 C++ 示例(中序遍历迭代器):
① main() 构建二叉搜索树:make_unique 依次创建 7 个节点并挂接左右子树,形成 4 为根、1~7 为叶的完整树;② 创建 InOrderIterator it(root.get()),构造时 pushLeft 从根一路向左,把 4、2、1 依次压入栈(栈顶 = 最小元素 1);③ 循环体:hasNext() 判断栈非空 → next() 弹出栈顶并输出,再对其右子树执行 pushLeft(例如弹出 2 后压入 3;弹出 4 后对右子树压入 6、5)——栈的"后进先出"特性天然保证了左-根-右顺序;④ 栈空时循环结束,输出 "1 2 3 4 5 6 7",树随 unique_ptr 自动析构。
Qt 示例(日志查看器):
① 程序启动,MainWindow 构造:初始化 6 条日志到 logs_,创建 QListWidget 与按钮,connect 连接按钮 clicked 信号到 onIterateClicked 槽;② 用户点击按钮 → 槽函数被调用,先清空列表;③ 创建 QVectorIterator,hasNext()/next() 循环取出每条日志,level >= 1 的(4 条)逐条 addItem 显示到列表;④ 再创建 QMutableVectorIterator 遍历删除 level >= 2 的日志(1 条 Error 被删除,剩余 5 条),最后在列表底部追加剩余条数提示——整个过程 logs_ 从未被外部直接触碰。
解耦发生在哪里?迭代器模式把两个变化方向不同的责任拆开:存储结构(QVector、红黑树、分片文件……)和遍历算法(正序、倒序、过滤、分页……)。它们各自独立变化,中间只通过 Iterator 接口对话。以今天的代码为例:logs_ 是 QVector,但按钮回调里没有任何一行代码直接操作 QVector 的下标或迭代器之外的东西——将来把 QVector 换成 QMap 按时间索引、甚至换成数据库游标,回调函数几乎不用动。
扩展点在哪里?符合 OCP:新增一种集合 = 新增一个 ConcreteAggregate + 一个配套 ConcreteIterator;新增一种遍历方式 = 新增一个迭代器类(如第⑤节的 InOrderIterator 旁边再加一个 PreOrderIterator)。所有新增都以"加类"的方式完成,已有的客户端代码和容器代码都不修改——这正是开闭原则"对扩展开放、对修改关闭"的教科书式实现。
依赖倒置 DIP:客户端依赖的是 Iterator 抽象接口(第⑤节是 hasNext()/next(),Qt 里是迭代器类型),而不是某个具体容器。高层模块不再依赖低层细节,低层(容器)反过来通过迭代器接口服务于高层——依赖方向被倒置了。
组合优于继承:迭代器持有集合的引用(组合),而不是继承集合。组合让迭代器可以独立于集合存在、可以被单独构造和测试,也避免了多重继承带来的菱形问题。
对测试的帮助:① 迭代器可以脱离容器单独单元测试——用一个固定形状的树验证 hasNext/next 序列是否符合中序;② 业务代码只依赖迭代器接口,测试时可以传入"造好的小集合 + 迭代器"或 Mock 迭代器,不需要真的构造海量数据;③ 容器的实现细节被隔离后,换存储结构不影响业务测试——今天 Qt 例子里"清理后剩余 5 条"的断言,在存储结构换成 QList 后依然成立。
反例一:类型判断 + 分支处理不同容器,if-else 无限膨胀。当系统里同时存在 vector、list、树三种存储时,没有统一迭代器接口的代码会长这样:
// 反例:按容器类型分支遍历 —— 每加一种容器/一种遍历方式,这里就多一层
void dumpLogs(const void* storage, ContainerType type) {
if (type == CT_VECTOR) {
auto* v = static_cast<const std::vector<LogEntry>*>(storage);
for (const auto& e : *v) print(e);
} else if (type == CT_LIST) {
auto* l = static_cast<const std::list<LogEntry>*>(storage);
for (const auto& e : *l) print(e);
} else if (type == CT_TREE) {
// 中序遍历逻辑被迫在这里再写一遍,且无法与业务解耦
}
// else if (type == CT_FILE) ... 需求一来,继续加分支
}
这种代码违反开闭原则到了极点:加一种存储,所有遍历点都要加一个 else if;static_cast 还埋着类型不安全的地雷。
反例二:遍历逻辑写进业务代码导致不可复用、不可暂停。把第⑤节的树遍历写成递归函数放在业务模块里:一是每个用到它的模块都要复制一份;二是递归深度 = 树高,树退化成链表时直接栈溢出;三是无法"遍历到一半停下来,下次继续"(比如分页浏览、流式处理),而迭代器用显式栈把状态保存在对象里,天然支持暂停/恢复。
反例三:容器接口被遍历需求撑爆。没有迭代器时,开发者往往往容器类里不断加成员函数:getSize()、getAt(i)、getReverse()、getFiltered(level)、getPage(n)……容器类越来越臃肿,职责混乱,任何一次改动都可能波及所有调用方——这就是"类膨胀"坏味道。
什么时候算"出现设计模式需求"?当你发现:①同一个集合被多个模块用不同方式遍历;②容器类型可能更换;③遍历方式经常新增——这时就该引入迭代器了。
适合使用(3~5 个场景):
QVector,明天可能换 QMap/树/分片文件,希望调用方不感知变化。QDirIterator、游标包装类)给它们补上统一遍历接口。QMutableVectorIterator::remove())比在 for 循环里手动删元素安全得多。不适合使用(2~4 个场景):
range-for 就够了,别造迭代器类)。operator[] 比迭代器更直接高效。过度设计提醒:标准库容器自带迭代器,99% 的场景用 range-for + STL 算法即可,不要"为每个类都造迭代器"。只有当遍历方式会变或容器实现可能变、且调用方很多时,自定义迭代器才划算。模式是解决问题的工具,不是装饰。
最容易混淆的是 Composite 和 Visitor:
| 维度 | 迭代器 Iterator | 组合 Composite | 访问者 Visitor |
|---|---|---|---|
| 回答的问题 | 怎么把集合"走一遍" | 树形结构"长什么样" | 对每个元素"做什么" |
| 关注点 | 遍历算法与控制权 | 整体-部分的递归结构 | 把操作从元素类中剥离 |
| 谁控制遍历 | 客户端(拿到一个处理一个) | 通常是递归(结构自带) | 访问者内部驱动遍历 |
| 典型关系 | 与 Composite 常配合:遍历组合树 | 与 Iterator/Visitor 常配合 | 内部常用迭代器实现遍历 |
| Qt 中的例子 | QListIterator / begin()-end() | QObject 对象树、QTreeWidgetItem | QStyleOption / 事件分发可类比 |
1. 容器迭代器体系(最直接的体现):Qt 所有容器都提供 STL 风格 begin()/end() 与 Java 风格 QXXXIterator 两套迭代器。QMap 内部是红黑树,但其迭代器按键升序输出——树的中序遍历被完整封装,调用方无感,与今天第⑤节 BST 示例原理完全一致。这就是 Qt "为什么这样设计":让容器与算法解耦,算法(std::find、std::sort)可以作用于任何提供迭代器的 Qt 容器。
2. 隐式共享(Implicit Sharing)与迭代器:Qt 容器是写时复制(COW)的。遍历中若通过非 const 引用修改容器,容器会 detach,而旧迭代器仍指向旧数据——所以 Qt 文档明确要求:遍历期间不要修改容器,需要修改请用 QMutableXXXIterator。这是"迭代器封装内部状态"带来的经典陷阱,面试常考。
3. QDirIterator:遍历文件系统目录时逐条产出条目,不把整个目录一次性载入内存——迭代器"惰性求值"思想的典范。
4. QAbstractItemModel + QModelIndex:Model/View 架构中,view 从不直接接触模型内部数据,而是通过 QModelIndex(可视为轻量迭代器句柄)调用 index()/parent()/child 在数据树上移动。这是"迭代器思想"在架构层面的延伸:数据如何存储(数组/树/数据库)完全被 Model 接口隔离。
5. QTextDocument 的 QTextBlock / QTextCursor、QJsonArray / QJsonObject 的 begin()/end():同样采用迭代式访问,保证文档/JSON 内部表示可以自由变化而不影响访问方。
6. foreach 宏的兴衰:Qt 5 曾提供 foreach(variable, container) 宏(本质就是对容器迭代器的语法糖封装),Qt 6 将其移除,统一使用 C++11 range-for——但无论哪种写法,底层都是迭代器。
Q1:STL 迭代器分哪几类?为什么算法需要区分它们?
A:按能力分为五类:输入、输出、前向(forward_list)、双向(list/map/set)、随机访问(vector/deque/array)。算法通过 iterator_category 标签分发(tag dispatch)选择最优实现:比如 std::advance(it, n) 对随机访问迭代器是 O(1) 直接 +=,对双向迭代器只能 O(n) 循环 ++。分类本质是"容器能力不同,算法按能力分级优化"。
Q2:什么是迭代器失效?vector 和 map 的失效规则有何不同?
A:迭代器失效指容器修改后,旧迭代器指向的位置不再有效(悬空)。vector:插入/删除导致元素搬移或 realloc,所有迭代器失效;deque 插入首尾之外也失效;list/map/set:只有指向"被删除元素"的迭代器失效,其他迭代器依然有效(节点独立分配)。所以遍历中删除元素要用 it = c.erase(it)(C++11 返回下一个有效迭代器),Qt 里对应 QMutableXXXIterator::remove()。
Q3:手写一个迭代器类需要提供哪些类型别名?iterator_traits 有什么用?
A:需要 value_type、difference_type、pointer、reference、iterator_category 五个类型别名。它们通过 std::iterator_traits 统一提取——好处是原生指针也可以充当迭代器(iterator_traits<int*> 有特化),算法无需区分"真迭代器"还是"裸指针"。
Q4:Qt 的 Java 风格迭代器与 STL 风格迭代器有什么区别?
A:① 接口:Java 风格是 hasNext()/next()/peekNext()(及可变版 remove()/setValue()),STL 风格是 begin()/end()/operator++/operator*;② 易用性:Java 风格更不容易写错,适合业务代码;③ 性能与生态:STL 风格能配合 std:: 算法、参与泛型编程,且少一层封装;④ 失效行为:两者在容器被修改后都可能失效,Qt 明确要求遍历时通过可变迭代器修改容器。
Q5:Iterator 模式和 C++20 ranges 是什么关系?
A:ranges 是迭代器模式的现代演进:传统迭代器是"起点 + 终点"两个对象,ranges 引入 begin/end 哨兵(sentinel)统一为"范围"概念,并提供 views::filter / transform / take 等惰性组合视图——遍历算法从"手写循环"升级为"声明式管道",但底层依然是迭代器。面试可以答:ranges 让迭代器模式更强大、更安全(避免 it != end 写错),核心思想一脉相承。
需求(15~30 分钟):模拟一个大数据集:std::vector<int> 存放 10000 条记录。实现一个 PagedIterator(分页迭代器),满足:
pageSize(如 100);bool hasNext() 和 std::vector<int> next()(返回下一批元素);设计目标:体会"遍历状态(当前页码/偏移量)由迭代器持有,调用方零负担"——这正是迭代器模式的核心价值。扩展挑战:让它支持 range-for(提供 begin()/end() 包装,返回一个"页"的视图)。
提示:① 内部维护两个成员:size_t offset_(游标)和 size_t pageSize_;② hasNext() 判断 offset_ < 数据总量,next() 取 [offset_, min(offset_+pageSize_, 总量)) 区间后前移游标;③ 注意最后一页可能不足 pageSize,next() 要返回实际大小。