迭代器模式(Iterator Pattern) 的核心思想就是:让用户在不知道容器内部结构的情况下,能够按顺序访问容器里的元素。
1. 场景对比:书架与书籍
假设我们有一个"书架",里面存了很多书。我们需要遍历并打印出所有书的名字。
❌ 不使用迭代器模式
在这种情况下,客户端代码必须知道书架内部是用什么存书的(比如 std::vector)。
#include <iostream>
#include <vector>
#include <string>
class BookShelf {
public:
std::vector<std::string> books; // 暴露了内部存储结构
void addBook(std::string name) { books.push_back(name); }
};
int main() {
BookShelf shelf;
shelf.addBook("C++ Primer");
shelf.addBook("Design Patterns");
// 客户端必须知道用 vector 的下标或 size() 来遍历
for (int i = 0; i < shelf.books.size(); ++i) {
std::cout << shelf.books[i] << std::endl;
}
return 0;
}
痛点: 如果哪天你想把 vector 改成 list(链表)或者 set(集合),你会发现 main 函数里的遍历代码全断了,因为不同容器的访问方式不一样。
✅ 使用迭代器模式
我们给书架配一个"导航员"(迭代器),让它来处理遍历逻辑。
#include <iostream>
#include <vector>
#include <string>
// 1. 迭代器抽象(可选,为了灵活性)
class Iterator {
public:
virtual bool hasNext() = 0;
virtual std::string next() = 0;
};
// 2. 具体的书架迭代器
class BookShelfIterator : public Iterator {
private:
std::vector<std::string>& books;
int index = 0;
public:
BookShelfIterator(std::vector<std::string>& b) : books(b) {}
bool hasNext() override { return index < books.size(); }
std::string next() override { return books[index++]; }
};
// 3. 容器类
class BookShelf {
private:
std::vector<std::string> books; // 现在是私有的了!
public:
void addBook(std::string name) { books.push_back(name); }
Iterator* createIterator() { return new BookShelfIterator(books); }
};
int main() {
BookShelf shelf;
shelf.addBook("C++ Primer");
shelf.addBook("Design Patterns");
// 客户端完全不需要知道底层是 vector 还是链表
Iterator* it = shelf.createIterator();
while (it->hasNext()) {
std::cout << it->next() << std::endl;
}
delete it;
return 0;
}
2. 优缺点分析
优点
-
封装性好:你不需要暴露容器的内部细节。哪怕以后书架改成从数据库读数据,只要迭代器接口不变,外部代码一个字都不用改。
-
单一职责原则:容器只负责"存东西",迭代器只负责"遍历东西"。逻辑清晰。
-
多态性:你可以为同一个容器提供不同的迭代器(比如:正向迭代器、反向迭代器、只读迭代器)。
缺点
-
开销增加:每增加一种容器,就要额外写一个迭代器类,代码量会变多。
-
简单场景略显复杂:如果你的数据结构非常简单(比如就是一个简单的数组),用迭代器反而觉得绕。
3. 现实中的 C++ 迭代器
其实 C++ 的标准库(STL)已经把迭代器模式玩出花来了。当你写 std::vector<int>::iterator it = v.begin(); 时,你就是在用迭代器模式。
现代 C++ (C++11及以后) 的 Range-based for loop 其实就是迭代器的语法糖:
for (auto& book : shelf) { // 底层依然是调用 begin() 和 end() 迭代器
std::cout << book << std::endl;
}
假设老板提出了一个新需求:"现在的书架太占内存了,请把底层存储从 vector 改成 list(双向链表),并且增加一个'只查找专业技术书'的过滤功能。"
4.需求变更:如果不使用迭代器模式
在之前的代码里,你的 main 函数(客户端)直接使用了下标遍历。因为 std::list 不支持像 vector 那样的随机访问(即不能直接用 shelf.books[i]),你的灾难就开始了:
// ❌ 灾难现场:所有调用书架的地方都要重写
int main() {
BookShelf shelf;
// ... 添加书 ...
// 报错!list 没有 [] 运算符,也没有简单的 size() 效率
// 你必须把这段逻辑全部删掉,改成处理 list 的逻辑
for (auto it = shelf.books.begin(); it != shelf.books.end(); ++it) {
if (it->find("C++") != std::string::npos) { // 还要在这里硬编码过滤逻辑
std::cout << *it << std::endl;
}
}
}
后果: 只要底层改了,外部所有用到书架的业务代码全部要"手术式"修改。如果项目有 100 处在遍历书架,你就得改 100 处。
5. 需求变更:如果使用了迭代器模式
在迭代器模式下,底层容器怎么换,客户端根本感知不到。
第一步:修改书架内部(客户端无感)
你只需要在 BookShelf 内部更换容器,并更新对应的 Iterator 实现即可。
第二步:增加"技术书过滤器"迭代器
你可以直接新建一个特殊的迭代器,而不需要修改原有的容器代码。这符合开闭原则(对扩展开放,对修改关闭)。
// 新增:专门过滤技术书的迭代器
class TechBookIterator : public Iterator {
private:
std::vector<std::string>& books;
int index = 0;
public:
TechBookIterator(std::vector<std::string>& b) : books(b) {}
bool hasNext() override {
// 自动跳过非技术书,只找带 "C++" 或 "Design" 字样的
while (index < books.size()) {
if (books[index].find("C++") != std::string::npos ||
books[index].find("Design") != std::string::npos) {
return true;
}
index++;
}
return false;
}
std::string next() override { return books[index++]; }
};
第三步:客户端的使用对比
客户端只需要换个"导航员"即可,遍历的逻辑(while 循环)一行都不用动:
int main() {
BookShelf shelf;
shelf.addBook("C++ Primer");
shelf.addBook("Cooking Guide"); // 生活类,会被过滤
shelf.addBook("Design Patterns");
// 哪怕换成了 TechBookIterator,下面的循环代码完全不需要改!
Iterator* it = shelf.createTechIterator();
while (it->hasNext()) {
// 这里的逻辑依然保持纯粹:只管拿,不管怎么选
std::cout << "Found Tech Book: " << it->next() << std::endl;
}
delete it;
return 0;
}