第十四章:迭代器模式——在C语言中实现迭代器接口
迭代器模式,说白了就是给容器装上一个统一的“遥控器”。
你想想看,我们平时写嵌入式代码,经常要遍历数组、链表、环形缓冲区。每个容器的遍历方式都不一样——数组用下标,链表用指针跳转,环形缓冲区还得算模。每次换个容器,遍历代码就得重写。这合理吗?
我个人习惯是:只要容器数量超过两个,就上迭代器。别等到项目后期再重构,那时候改起来就痛苦了。
14.1 为什么需要迭代器?
先看一个真实场景。我在做智能家居网关项目时,需要同时管理传感器数据队列、设备列表和事件日志。这三个容器类型完全不同:
- 传感器数据:环形缓冲区
- 设备列表:双向链表
- 事件日志:动态数组
如果没有迭代器,每个遍历操作都要写三套代码。更麻烦的是,如果后续要增加一种容器(比如哈希表),所有遍历代码都得改一遍。
迭代器要解决的核心问题就两个:
- 统一遍历接口——不管底层是什么容器,遍历方式都一样
- 解耦算法与容器——算法只依赖迭代器,不依赖具体容器
核心思想:将“如何访问元素”与“元素本身”分离。迭代器负责访问逻辑,容器只负责存储逻辑。
14.2 迭代器接口设计
在C语言中实现迭代器,我建议用结构体+函数指针的方式。这样既灵活,又不会像C++模板那样增加代码体积。
先定义迭代器的操作接口:
// iterator.h
#ifndef ITERATOR_H
#define ITERATOR_H
#include <stdbool.h>
// 前向声明
struct iterator;
// 迭代器操作函数指针类型
typedef void* (*iterator_next_fn)(struct iterator* it);
typedef bool (*iterator_has_next_fn)(struct iterator* it);
typedef void (*iterator_reset_fn)(struct iterator* it);
typedef void (*iterator_destroy_fn)(struct iterator* it);
// 迭代器结构体
typedef struct iterator {
void* context; // 迭代器内部状态(指向具体容器的遍历上下文)
iterator_next_fn next; // 获取下一个元素
iterator_has_next_fn has_next; // 是否还有下一个
iterator_reset_fn reset; // 重置到起始位置
iterator_destroy_fn destroy; // 销毁迭代器
} iterator_t;
#endif // ITERATOR_H
嗯,这里要注意:context字段是关键。它指向一个“遍历上下文”结构体,里面保存了当前遍历位置等信息。不同的容器,这个上下文结构体完全不同。
14.3 为数组实现迭代器
先来个最简单的——数组迭代器。我在项目中经常用这个来遍历配置表。
// array_iterator.c
#include "iterator.h"
#include <stdlib.h>
// 数组迭代器的上下文
typedef struct {
int* data; // 数组指针
int size; // 数组长度
int index; // 当前索引
} array_context_t;
// 获取下一个元素
static void* array_next(iterator_t* it) {
array_context_t* ctx = (array_context_t*)it->context;
if (ctx->index >= ctx->size) return NULL;
return &ctx->data[ctx->index++];
}
// 是否还有下一个
static bool array_has_next(iterator_t* it) {
array_context_t* ctx = (array_context_t*)it->context;
return ctx->index < ctx->size;
}
// 重置迭代器
static void array_reset(iterator_t* it) {
array_context_t* ctx = (array_context_t*)it->context;
ctx->index = 0;
}
// 销毁迭代器
static void array_destroy(iterator_t* it) {
if (it->context) free(it->context);
free(it);
}
// 创建数组迭代器
iterator_t* array_iterator_create(int* data, int size) {
iterator_t* it = (iterator_t*)malloc(sizeof(iterator_t));
if (!it) return NULL;
array_context_t* ctx = (array_context_t*)malloc(sizeof(array_context_t));
if (!ctx) {
free(it);
return NULL;
}
ctx->data = data;
ctx->size = size;
ctx->index = 0;
it->context = ctx;
it->next = array_next;
it->has_next = array_has_next;
it->reset = array_reset;
it->destroy = array_destroy;
return it;
}
个人经验:迭代器的next函数返回void*,这样任何类型的数组都能用。调用方拿到指针后自己强转回去。虽然有点“不安全”,但C语言嘛,灵活性和性能优先。
14.4 为链表实现迭代器
链表迭代器稍微复杂一点。我记得有一次调试一个设备管理模块,链表遍历总是漏掉最后一个节点,查了半天发现是has_next判断条件写错了。
// list_iterator.c
#include "iterator.h"
#include <stdlib.h>
// 链表节点
typedef struct list_node {
int data;
struct list_node* next;
} list_node_t;
// 链表迭代器上下文
typedef struct {
list_node_t* head; // 链表头
list_node_t* current; // 当前节点
} list_context_t;
// 获取下一个元素
static void* list_next(iterator_t* it) {
list_context_t* ctx = (list_context_t*)it->context;
if (!ctx->current) return NULL;
void* data = &ctx->current->data;
ctx->current = ctx->current->next;
return data;
}
// 是否还有下一个
static bool list_has_next(iterator_t* it) {
list_context_t* ctx = (list_context_t*)it->context;
return ctx->current != NULL;
}
// 重置迭代器
static void list_reset(iterator_t* it) {
list_context_t* ctx = (list_context_t*)it->context;
ctx->current = ctx->head;
}
// 销毁迭代器
static void list_destroy(iterator_t* it) {
if (it->context) free(it->context);
free(it);
}
// 创建链表迭代器
iterator_t* list_iterator_create(list_node_t* head) {
iterator_t* it = (iterator_t*)malloc(sizeof(iterator_t));
if (!it) return NULL;
list_context_t* ctx = (list_context_t*)malloc(sizeof(list_context_t));
if (!ctx) {
free(it);
return NULL;
}
ctx->head = head;
ctx->current = head;
it->context = ctx;
it->next = list_next;
it->has_next = list_has_next;
it->reset = list_reset;
it->destroy = list_destroy;
return it;
}
14.5 统一遍历——算法与容器解耦
现在,不管底层是数组还是链表,遍历代码完全一样:
// 统一的遍历函数——完全不关心底层容器
void print_all(iterator_t* it) {
it->reset(it); // 从头开始
while (it->has_next(it)) {
int* value = (int*)it->next(it);
printf("%d ", *value);
}
printf("\n");
}
// 使用示例
void demo() {
// 遍历数组
int arr[] = {1, 2, 3, 4, 5};
iterator_t* arr_it = array_iterator_create(arr, 5);
print_all(arr_it);
arr_it->destroy(arr_it);
// 遍历链表
list_node_t n1 = {10, NULL};
list_node_t n2 = {20, NULL};
list_node_t n3 = {30, NULL};
n1.next = &n2;
n2.next = &n3;
iterator_t* list_it = list_iterator_create(&n1);
print_all(list_it);
list_it->destroy(list_it);
}
你看,print_all函数完全不知道底层是数组还是链表。它只依赖迭代器接口。这就是解耦——算法只跟接口打交道,不跟具体容器打交道。
14.6 迭代器模式的核心结构
下面这张图展示了迭代器模式的整体架构:
14.7 避坑指南与最佳实践
我在多个项目中用过迭代器模式,踩过不少坑。这里分享几个关键点:
我曾经犯过的错:
- 迭代器失效问题:遍历过程中如果容器被修改(比如删除了当前节点),迭代器就废了。解决方案:要么禁止遍历时修改,要么让迭代器自己管理容器锁。
- 内存泄漏:迭代器内部malloc了context,调用方用完必须调用destroy。我建议在文档里明确标注“谁创建谁销毁”。
- 多线程安全:如果迭代器被多个任务同时使用,context里的状态会乱掉。要么加锁,要么每个任务创建自己的迭代器实例。
我的建议:
- 迭代器接口尽量精简——只保留next和has_next,reset和destroy按需提供
- 对于嵌入式系统,考虑用静态分配代替malloc,避免堆碎片
- 如果容器元素类型固定,可以用宏模板生成类型安全的迭代器,避免void*的强制转换
14.8 迭代器模式的适用场景
| 场景 | 推荐使用 | 原因 |
|---|---|---|
| 多种容器需要统一遍历 | ✅ 强烈推荐 | 解耦算法与容器,代码复用率高 |
| 容器类型可能变化 | ✅ 推荐 | 切换容器不影响遍历代码 |
| 只有一种容器且固定 | ⚠️ 按需使用 | 引入迭代器会增加代码复杂度 |
| 性能极端敏感(如中断处理) | ❌ 不推荐 | 函数指针调用有额外开销 |
说白了,迭代器模式不是银弹。如果你的项目里只有一种容器,而且永远不会换,那直接写遍历代码更简单。但如果你像我一样,经常要跟多种容器打交道——嗯,迭代器能省下你大量时间。
我个人习惯是:在模块接口层使用迭代器,在模块内部实现层直接用原生遍历。这样既保持了接口的统一性,又避免了内部性能损失。
最后说一句:迭代器模式的核心价值不是“遍历”,而是“解耦”。你想想看,当你的算法不再依赖具体容器时,代码的灵活性和可维护性会提升一个档次。
公众号:蓝海资料掘金营,微信deep3321