当前位置: 首页 > news >正文

C++从入门到实战(二十一)List迭代器实现

C++从入门到实战(二十一)List迭代器实现

  • 前言
  • 一、为什么需要自定义list迭代器?
  • 二、list迭代器的核心:运算符重载
    • 2.1 基础准备:节点结构回顾
    • 2.2 迭代器类的初步实现(普通迭代器)
    • 2.3 关键运算符详解:operator* 与 operator->
      • 2.3.1 案例:遍历存储自定义类型的list
      • 2.3.2 用operator*访问成员
      • 2.3.3 用operator->访问成员(编译器优化)
        • 原理拆解:
      • 2.3.4 常见错误:直接用节点指针访问
    • 2.4 补充:后置++与后置--的重载
  • 三、const迭代器的实现:模板复用解决冗余
    • 3.1 痛点:直接复制代码的冗余
    • 3.2 解决方案:增加模板参数,复用迭代器类
      • 3.2.1 模板化迭代器类实现
      • 3.2.2 在list类中定义两种迭代器
      • 3.2.3 const迭代器的使用示例
  • 四、迭代器的封装与STL设计理念
  • 五、list迭代器与vector迭代器的对比
  • 六、完整模拟实现:迭代器+list集成


前言

  • 上一篇博客中,我们在模拟实现list时提到:list的迭代器并非原生指针。由于list底层是双向循环链表(离散节点),原生指针无法直接模拟“双向遍历”“节点访问”等行为——这就需要我们自定义迭代器类,通过运算符重载封装节点指针,让迭代器像原生指针一样易用。
  • 本篇将聚焦list迭代器的核心实现:从“为什么需要自定义迭代器”出发,深入讲解operator*operator->等关键运算符的重载逻辑,解决“const迭代器”的实现痛点,并通过模板复用减少代码冗余,最终理解STL迭代器“封装底层、统一接口”的设计思想。

我的个人主页,欢迎来阅读我的其他文章
https://blog.csdn.net/2402_83322742?spm=1011.2415.3001.5343
我的C++知识文章专栏
欢迎来阅读指出不足
https://blog.csdn.net/2402_83322742/category_12880513.html?spm=1001.2014.3001.5482


C++官方list迭代器文档
https://cplusplus.com/reference/list/list/iterator/

一、为什么需要自定义list迭代器?

vector的迭代器可以直接用原生指针(如int*),因为其底层是连续内存——原生指针的++*[]等操作天然匹配vector的访问需求。list完全不同

  1. 底层结构不兼容list是离散节点,每个节点包含_prev_next指针,原生指针无法通过++直接跳转到下一个节点(需访问_next成员)。
  2. 隐藏底层细节:用户无需知道list的节点结构(如ListNode_prev/_next),只需通过迭代器的统一接口(++*)操作元素。
  3. 支持const语义:需要区分“可修改元素的迭代器”(iterator)和“只读元素的迭代器”(const_iterator),原生指针无法直接满足。

简言之,list迭代器的核心目标是:封装节点指针,通过运算符重载模拟原生指针行为,同时统一容器访问接口

二、list迭代器的核心:运算符重载

自定义迭代器的本质是“重载一系列运算符”,让迭代器支持以下操作(与原生指针一致):

  • 移动:++it(向后)、--it(向前)
  • 解引用:*it(获取元素引用)、it->(获取元素指针,访问自定义类型成员)
  • 比较:it1 == it2(是否指向同一节点)、it1 != it2(是否指向不同节点)

2.1 基础准备:节点结构回顾

首先回顾list的节点结构,迭代器将围绕节点指针展开:

template <class T>
struct ListNode {ListNode<T>* _prev;  // 前驱指针ListNode<T>* _next;  // 后继指针T _data;             // 存储的数据// 节点构造函数ListNode(const T& data = T()): _prev(nullptr), _next(nullptr), _data(data){}
};

2.2 迭代器类的初步实现(普通迭代器)

先实现支持“修改元素”的普通迭代器,核心是封装ListNode<T>*并重载关键运算符:

template <class T>
struct ListIterator {typedef ListNode<T> Node;  // 简化节点类型名typedef ListIterator<T> Self;  // 简化迭代器类型名Node* _node;  // 核心:封装节点指针// 1. 迭代器构造函数(接收节点指针)ListIterator(Node* node): _node(node){}// 2. 重载operator*:解引用,返回元素引用(支持修改)T& operator*() {return _node->_data;  // 直接返回节点中的数据引用}// 3. 重载operator->:返回元素指针(访问自定义类型成员)T* operator->() {return &(_node->_data);  // 返回数据的地址}// 4. 重载operator++:前置++,向后移动到下一个节点Self& operator++() {_node = _node->_next;  // 借助节点的_next指针移动return *this;  // 返回自身(支持链式操作,如++(++it))}// 5. 重载operator--:前置--,向前移动到前一个节点Self& operator--() {_node = _node->_prev;  // 借助节点的_prev指针移动return *this;}// 6. 重载operator==:判断是否指向同一节点bool operator==(const Self& other) const {return _node == other._node;}// 7. 重载operator!=:判断是否指向不同节点bool operator!=(const Self& other) const {return _node != other._node;}
};

2.3 关键运算符详解:operator* 与 operator->

operator*operator->是迭代器访问元素的核心,尤其是operator->在访问自定义类型时容易产生疑惑,我们通过案例拆解:

2.3.1 案例:遍历存储自定义类型的list

假设我们有一个自定义类A,包含两个成员_a1_a2,用list存储A的对象:

#include <iostream>
using namespace std;// 自定义类A
struct A {int _a1;int _a2;A(int a1 = 0, int a2 = 0): _a1(a1), _a2(a2){}
};// 初始化list<A>
list<A> lt2;
lt2.push_back(A(1, 2));
lt2.push_back(A(3, 4));

2.3.2 用operator*访问成员

通过*it获取A对象的引用,再用.访问成员:

// 普通迭代器遍历
list<A>::iterator it = lt2.begin();
while (it != lt2.end()) {// *it 返回A&,通过.访问成员cout << (*it)._a1 << ":" << (*it)._a2 << endl;++it;
}
// 输出:
// 1:2
// 3:4

注意(*it)必须加括号,因为.的优先级高于*,不加括号会被解析为*(it._a1)(错误)。

2.3.3 用operator->访问成员(编译器优化)

如果用it->访问成员,代码会更简洁,但背后有编译器的特殊优化:

while (it != lt2.end()) {// it-> 返回A*,理论上需写 it->->_a1,但编译器自动省略一个->cout << it->_a1 << ":" << it->_a2 << endl;++it;
}
原理拆解:
  1. it->调用operator->(),返回&(_node->_data)(即A*类型)。
  2. 理论上,访问成员需要(it.operator->())->_a1(两次->:一次调用迭代器的operator->,一次原生指针的->)。
  3. C++编译器为了简化代码,自动省略中间的一个->,允许直接写it->_a1

这就是operator->重载的“特殊之处”——看似只写了一个->,实则隐含了两次指针访问。

2.3.4 常见错误:直接用节点指针访问

如果不封装迭代器,直接用Node*遍历,代码会暴露底层结构,且可读性差:

// 不推荐:直接操作节点指针,暴露底层实现
ListNode<A>* node = lt2._head->_next;  // 假设_head是public(实际应私有)
while (node != lt2._head) {cout << node->_data._a1 << ":" << node->_data._a2 << endl;node = node->_next;
}

显然,迭代器的封装让代码更简洁、更安全(无需访问list的私有成员_head)。

2.4 补充:后置++与后置–的重载

前面实现了前置++/–(如++it),实际开发中还会用到后置++/–(如it++)。两者的核心区别是:

  • 前置:先移动,再返回自身(返回引用)。
  • 后置:先返回当前状态,再移动(返回值,不能是引用)。

实现后置++/–时,需用int作为参数(无实际意义,仅用于区分前置和后置):

template <class T>
struct ListIterator {// ... 其他成员 ...// 后置++:参数int用于区分,返回移动前的迭代器(值返回)Self operator++(int) {Self temp(*this);  // 保存当前迭代器状态_node = _node->_next;  // 移动到下一个节点return temp;  // 返回移动前的状态}// 后置--:同理Self operator--(int) {Self temp(*this);  // 保存当前状态_node = _node->_prev;  // 移动到前一个节点return temp;  // 返回移动前的状态}
};

使用示例

list<int> l = {1,2,3};
auto it = l.begin();
auto it2 = it++;  // it2指向1(移动前),it指向2(移动后)
cout << *it2 << " " << *it << endl;  // 输出:1 2

三、const迭代器的实现:模板复用解决冗余

普通迭代器(iterator)支持修改元素,但当listconst类型时(如const list<int>),需要只读迭代器(const_iterator——即迭代器本身可移动,但指向的元素不能修改。

3.1 痛点:直接复制代码的冗余

如果直接复制ListIterator改名为ConstListIterator,并将operator*operator->的返回值改为const,会导致大量重复代码(如++--==等逻辑完全相同):

// 不推荐:重复代码多,维护成本高
template <class T>
struct ConstListIterator {typedef ListNode<T> Node;typedef ConstListIterator<T> Self;Node* _node;ConstListIterator(Node* node) : _node(node) {}// 仅修改返回值为const T&const T& operator*() { return _node->_data; }// 仅修改返回值为const T*const T* operator->() { return &(_node->_data); }// 以下代码与ListIterator完全重复Self& operator++() { _node = _node->_next; return *this; }Self& operator--() { _node = _node->_prev; return *this; }bool operator==(const Self& other) const { return _node == other._node; }// ...
};

3.2 解决方案:增加模板参数,复用迭代器类

核心思路:在迭代器类中增加两个模板参数Ref(元素引用类型)和Ptr(元素指针类型),通过传入不同的RefPtr,同时生成普通迭代器和const迭代器。

3.2.1 模板化迭代器类实现

// 模板参数:T(元素类型)、Ref(引用类型)、Ptr(指针类型)
template <class T, class Ref, class Ptr>
struct ListIterator {typedef ListNode<T> Node;typedef ListIterator<T, Ref, Ptr> Self;  // 迭代器类型自引用Node* _node;// 构造函数ListIterator(Node* node) : _node(node) {}// 1. operator*:返回Ref类型(普通迭代器是T&,const迭代器是const T&)Ref operator*() {return _node->_data;}// 2. operator->:返回Ptr类型(普通迭代器是T*,const迭代器是const T*)Ptr operator->() {return &(_node->_data);}// 3. 移动与比较:逻辑完全复用,无需修改Self& operator++() {_node = _node->_next;return *this;}Self& operator--() {_node = _node->_prev;return *this;}bool operator==(const Self& other) const {return _node == other._node;}bool operator!=(const Self& other) const {return _node != other._node;}// 后置++/--同样复用Self operator++(int) {Self temp(*this);_node = _node->_next;return temp;}Self operator--(int) {Self temp(*this);_node = _node->_prev;return temp;}
};

3.2.2 在list类中定义两种迭代器

list类中,通过指定RefPtr的类型,分别定义iteratorconst_iterator

template <class T>
class List {typedef ListNode<T> Node;
public:// 1. 普通迭代器:Ref=T&,Ptr=T*(支持修改元素)typedef ListIterator<T, T&, T*> iterator;// 2. const迭代器:Ref=const T&,Ptr=const T*(只读元素)typedef ListIterator<T, const T&, const T*> const_iterator;// 3. 获取普通迭代器iterator begin() {return iterator(_head->_next);  // 指向第一个数据节点}iterator end() {return iterator(_head);  // 指向哨兵位(结束标志)}// 4. 获取const迭代器const_iterator cbegin() const {return const_iterator(_head->_next);}const_iterator cend() const {return const_iterator(_head);}// ... 其他list接口(push_back、insert等,与上一篇一致) ...private:Node* _head;  // 哨兵位头节点(私有,隐藏底层)
};

3.2.3 const迭代器的使用示例

listconst类型时,只能使用cbegin()/cend()获取const_iterator,且无法修改元素:

void PrintConstList(const list<int>& l) {// 必须用const_iterator,因为l是constlist<int>::const_iterator it = l.cbegin();while (it != l.cend()) {// *it = 10;  // 错误!const迭代器指向的元素不可修改cout << *it << " ";++it;  // 迭代器本身可移动(允许++)}
}int main() {list<int> l = {1,2,3};PrintConstList(l);  // 输出:1 2 3return 0;
}

核心区别总结

迭代器类型Ref类型Ptr类型元素是否可修改迭代器是否可移动
iteratorT&T*
const_iteratorconst T&const T*

四、迭代器的封装与STL设计理念

list迭代器的实现,完美体现了STL的两大核心设计理念:

  1. 封装底层,解耦接口

    • 用户无需关心list的节点结构(_prev/_next),只需通过begin()/end()获取迭代器,用++/*操作元素。
    • 即使未来修改list的底层实现(如改用双向循环链表的其他结构),只要迭代器的接口不变,用户代码无需修改。
  2. 统一容器访问方式

    • 无论是vector(连续内存)、list(离散节点)还是deque(双端队列),迭代器的使用方式完全一致:
      // list遍历
      list<int> l = {1,2,3};
      for (auto it = l.begin(); it != l.end(); ++it) { ... }// vector遍历(接口完全相同)
      vector<int> v = {1,2,3};
      for (auto it = v.begin(); it != v.end(); ++it) { ... }
      
    • 这种统一性让算法(如sortfind)可以跨容器复用(前提是迭代器类型匹配)。

五、list迭代器与vector迭代器的对比

虽然两者接口一致,但底层实现和能力差异显著,呼应上一篇的“迭代器分类”:

对比维度list迭代器(双向迭代器)vector迭代器(随机访问迭代器)
底层实现封装ListNode*,重载运算符原生指针(如int*)或轻量封装
支持的移动操作仅支持++it--it(双向移动)支持++it--itit+nit-n
解引用效率需访问节点_data(间接访问)直接访问内存(效率更高)
迭代器失效场景仅删除当前节点时失效,其他节点迭代器安全扩容、插入/删除中间元素时,后续迭代器失效
适用算法仅支持双向遍历算法(如reverse支持随机访问算法(如sortbinary_search

六、完整模拟实现:迭代器+list集成

将上述内容整合,给出list迭代器与list类的完整模拟实现

#include <iostream>
#include <assert.h>
using namespace std;// 1. 节点结构
template <class T>
struct ListNode {ListNode<T>* _prev;ListNode<T>* _next;T _data;ListNode(const T& data = T()): _prev(nullptr), _next(nullptr), _data(data){}
};// 2. 模板化迭代器类
template <class T, class Ref, class Ptr>
struct ListIterator {typedef ListNode<T> Node;typedef ListIterator<T, Ref, Ptr> Self;Node* _node;ListIterator(Node* node) : _node(node) {}// 解引用Ref operator*() { return _node->_data; }Ptr operator->() { return &(_node->_data); }// 移动Self& operator++() { _node = _node->_next; return *this; }Self& operator--() { _node = _node->_prev; return *this; }Self operator++(int) { Self temp(*this); _node = _node->_next; return temp; }Self operator--(int) { Self temp(*this); _node = _node->_prev; return temp; }// 比较bool operator==(const Self& other) const { return _node == other._node; }bool operator!=(const Self& other) const { return _node != other._node; }
};// 3. list类
template <class T>
class List {typedef ListNode<T> Node;
public:// 定义两种迭代器typedef ListIterator<T, T&, T*> iterator;typedef ListIterator<T, const T&, const T*> const_iterator;// 构造函数:初始化哨兵位List() {_head = new Node();_head->_prev = _head;_head->_next = _head;}// 尾插(用于测试)void push_back(const T& data) {Node* newNode = new Node(data);Node* tail = _head->_prev;tail->_next = newNode;newNode->_prev = tail;newNode->_next = _head;_head->_prev = newNode;}// 获取迭代器iterator begin() { return iterator(_head->_next); }iterator end() { return iterator(_head); }const_iterator cbegin() const { return const_iterator(_head->_next); }const_iterator cend() const { return const_iterator(_head); }// 析构函数(避免内存泄漏)~List() {clear();delete _head;_head = nullptr;}// 清空数据节点(保留哨兵位)void clear() {iterator it = begin();while (it != end()) {it = erase(it);}}// 删除节点(用于测试)iterator erase(iterator pos) {assert(pos != end());  // 不能删除哨兵位Node* cur = pos._node;Node* prev = cur->_prev;Node* next = cur->_next;prev->_next = next;next->_prev = prev;delete cur;return iterator(next);}private:Node* _head;
};// 测试代码
int main() {// 1. 测试普通迭代器(修改元素)List<int> l;l.push_back(1);l.push_back(2);l.push_back(3);List<int>::iterator it = l.begin();while (it != l.end()) {*it *= 2;  // 修改元素(1→2,2→4,3→6)cout << *it << " ";++it;}cout << endl;  // 输出:2 4 6// 2. 测试const迭代器(只读)const List<int> cl = l;List<int>::const_iterator cit = cl.cbegin();while (cit != cl.cend()) {cout << *cit << " ";++cit;}cout << endl;  // 输出:2 4 6return 0;
}

我的个人主页,欢迎来阅读我的其他文章
https://blog.csdn.net/2402_83322742?spm=1011.2415.3001.5343
我的C++知识文章专栏
欢迎来阅读指出不足
https://blog.csdn.net/2402_83322742/category_12880513.html?spm=1001.2014.3001.5482

非常感谢您的阅读,喜欢的话记得三连哦

在这里插入图片描述

http://www.dtcms.com/a/365300.html

相关文章:

  • 行业分享丨基于SimSolid的大型汽车连续冲压模具刚度分析
  • 【Axure高保真原型】区间缩放柱状图
  • JavaScript箭头函数与普通函数:两种工作方式的深度解析
  • android studio打开Android Device Monitor
  • Java 鲁棒性:支撑企业级应用稳定运行的核心密码
  • websoket使用记录
  • 马斯克砸钱造AI,却败给最low的“让离职员工轻松拷走代码”
  • OpenLayers 入门篇教程 -- 章节三 :掌控地图的视野和交互
  • 《计算机网络安全》实验报告一 现代网络安全挑战 拒绝服务与分布式拒绝服务攻击的演变与防御策略(1)
  • 【全息投影】幻影成像技术在展厅中的创新应用
  • 求单源最短路(Dijkstra 算法-迪杰斯特拉算法,SPFA)
  • word文档封面中文件编号等标题和内容无法对齐
  • 关于QSharedPointer
  • 清理磁盘:卸载 GitLab CI/CD Multi-Runner 删除docker相关文件
  • linux服务开机自启动之二(forking方式)
  • undo-log
  • 用 “走楼梯” 讲透动态规划!4 个前端场景 + 4 道 LeetCode 题手把手教
  • MySQL的utf8 、utf8mb3 和 utf8mb4 的区别和排序规则
  • 摄像头现代实现WIFI远程实操画面移动
  • Flutter环境搭建全攻略之-Macos环境搭建
  • 【Layui】Layui Table 底部合计栏实现方案
  • CentOS安装vulhub靶场
  • 不同数据类型for循环
  • 从一道面试题开始:如何让同时启动的线程按顺序执行?
  • 物联网能源管控平台建设方案
  • PostgreSQL 技术峰会哈尔滨站活动回顾|深度参与 IvorySQL 开源社区建设的实践与思考
  • FPGA ad9248驱动
  • 计算机视觉(六):腐蚀操作
  • 生产环境中redis的SCAN命令如何替代KEYS命令?
  • 苍穹外卖项目笔记day04--Redis入门