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

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

  • 前言
  • 一、为什么需要自定义list迭代器?
  • 二、list迭代器的核心:运算符重载
    • [2.1 基础准备:节点结构回顾](#2.1 基础准备:节点结构回顾)
    • [2.2 迭代器类的初步实现(普通迭代器)](#2.2 迭代器类的初步实现(普通迭代器))
    • [2.3 关键运算符详解:operator* 与 operator->](#2.3 关键运算符详解:operator* 与 operator->)
      • [2.3.1 案例:遍历存储自定义类型的list](#2.3.1 案例:遍历存储自定义类型的list)
      • [2.3.2 用operator*访问成员](#2.3.2 用operator*访问成员)
      • [2.3.3 用operator->访问成员(编译器优化)](#2.3.3 用operator->访问成员(编译器优化))
      • [2.3.4 常见错误:直接用节点指针访问](#2.3.4 常见错误:直接用节点指针访问)
    • [2.4 补充:后置++与后置--的重载](#2.4 补充:后置++与后置--的重载)
  • 三、const迭代器的实现:模板复用解决冗余
    • [3.1 痛点:直接复制代码的冗余](#3.1 痛点:直接复制代码的冗余)
    • [3.2 解决方案:增加模板参数,复用迭代器类](#3.2 解决方案:增加模板参数,复用迭代器类)
      • [3.2.1 模板化迭代器类实现](#3.2.1 模板化迭代器类实现)
      • [3.2.2 在list类中定义两种迭代器](#3.2.2 在list类中定义两种迭代器)
      • [3.2.3 const迭代器的使用示例](#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的节点结构,迭代器将围绕节点指针展开:

cpp 复制代码
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>*并重载关键运算符:

cpp 复制代码
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的对象:

cpp 复制代码
#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对象的引用,再用.访问成员:

cpp 复制代码
// 普通迭代器遍历
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->访问成员,代码会更简洁,但背后有编译器的特殊优化:

cpp 复制代码
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*遍历,代码会暴露底层结构,且可读性差:

cpp 复制代码
// 不推荐:直接操作节点指针,暴露底层实现
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作为参数(无实际意义,仅用于区分前置和后置):

cpp 复制代码
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;  // 返回移动前的状态
    }
};

使用示例

cpp 复制代码
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,会导致大量重复代码(如++--==等逻辑完全相同):

cpp 复制代码
// 不推荐:重复代码多,维护成本高
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 模板化迭代器类实现

cpp 复制代码
// 模板参数: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

cpp 复制代码
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,且无法修改元素:

cpp 复制代码
void PrintConstList(const list<int>& l) {
    // 必须用const_iterator,因为l是const
    list<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 3
    return 0;
}

核心区别总结

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

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

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

  1. 封装底层,解耦接口

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

    • 无论是vector(连续内存)、list(离散节点)还是deque(双端队列),迭代器的使用方式完全一致:

      cpp 复制代码
      // 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类的完整模拟实现

cpp 复制代码
#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 6

    return 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

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

相关推荐
Q741_1474 小时前
C++ 面试高频考点 力扣 162. 寻找峰值 二分查找 题解 每日一题
c++·算法·leetcode·面试·二分查找
青鱼入云4 小时前
java面试中经常会问到的多线程问题有哪些(基础版)
java·开发语言·面试
m0_552200825 小时前
《UE5_C++多人TPS完整教程》学习笔记47 ——《P48 瞄准行走(Aim Walking)》
c++·游戏·ue5
Niuguangshuo5 小时前
Python绘图动态可视化:实时音频流
开发语言·python·音视频
0wioiw05 小时前
Python基础(⑦魔法方法)
开发语言·python
EthanChou20205 小时前
rust学习之开发环境
开发语言·学习·rust
大飞pkz5 小时前
【Lua】题目小练14
开发语言·lua·练习·题目·题目小练
@HNUSTer5 小时前
基于 HTML、CSS 和 JavaScript 的智能图像灰度直方图分析系统
开发语言·前端·javascript·css·html
是店小二呀5 小时前
【ProtoBuf 】C++ 网络通讯录开发实战:ProtoBuf 协议设计与 HTTP 服务实现
网络·c++·http·protobuf