• 【 C++ 】list的模拟实现


    目录

    1、list三个基本模板类

    2、结点类接口实现

            基本框架

            构造函数

    3、迭代器类接口实现

            3.1、正向迭代器

                    基本框架

                    默认成员函数

                    构造函数

                    * 运算符重载

                    ->运算符重载

                    ++运算符重载

                    --运算符重载

                    !=运算符重载

                    ==运算符重载

            3.2、反向迭代器

    4、list类接口实现

            基本框架

            4.1、默认成员函数

                    构造函数

                    析构函数

                    拷贝构造函数

                    赋值运算符重载函数

            4.2、迭代器相关函数

                    begin和end

                    rbegin和rend

            4.3、增加的相关函数

                    insert

                    push_back尾插

                    push_front头插

            4.4、删除的相关函数

                    erase

                    pop_back尾删

                    pop_front头删

            4.5、其它函数

                    clear清除

                    empty_init空初始化

                    swap交换

    5、源码链接


    1、list三个基本函数类

    前面list的学习中我们得知,list其实就是一个带头双向循环链表:

    现在要模拟实现list,要实现下列三个类:

    1. 模拟实现结点类
    2. 模拟实现迭代器的类
    3. 模拟list主要功能的类

    这三个类的实现层层递进,就好比如我整个核心list的类的模拟实现其基本功能(增加删除……)要建立在迭代器类和结点类均已实现好的情况下才得以完成。


    2、结点类接口实现

    因为list的本质为带头双向循环链表,所以其每个结点都要确保有下列成员:

    1. 前驱指针
    2. 后继指针
    3. data值存放数据

    而结点类的内部只需要实现一个构造函数即可。

    基本框架

    1. //1、结点类的模拟实现
    2. template<class T>
    3. struct list_node//因为成员函数都是公有,就不设计成class了,直接struct
    4. {
    5. list_node* _next;//后继指针
    6. list_node* _prev;//前驱指针
    7. T _data;//记录数据
    8. };

    构造函数

    构造函数这里是用来对其成员变量的一个初始化。

    1. //构造函数
    2. list_node(const T& val = T())//给一个缺省值T()
    3. :_next(nullptr)
    4. , _prev(nullptr)
    5. , _data(val)
    6. {}

    3、迭代器类接口实现

    这里强调下,因为list的特殊性,其本质是带头双向循环链表,对于链表,我们已然得知其内存空间是不连续的,是通过结点的指针顺次链接,我们不能像先前的string和vector一样直接解引用去访问其数据,结点的指针解引用还是结点,结点指针++还是结点指针,归根结底在于list物理空间不连续。而string和vector的物理空间是连续的,所以这俩不需要实现迭代器类,可以直接使用。

    为了能让list像vector一样去解引用,++访问到下一个数据,我们需要单独写一个迭代器类的接口实现,在其内部进行封装补齐相应的功能,而这就要借助运算符重载来完成

    • 注意:

    迭代器又分为正向迭代器和反向迭代器

    3.1、正向迭代器

    基本框架

    1. //模拟实现迭代器类
    2. template<class T, class Ref, class Ptr>
    3. struct __list_iterator//因为成员函数都是公有,就不设计成class了,直接struct
    4. {
    5. typedef list_node Node;
    6. typedef __list_iterator self;//只要用自己的类型,就对其typedef成self,方便后续使用
    7. Node* _node;
    8. };
    • 注意:

    这里我迭代器类的模板参数里面包含了3个参数:

    template<class T, class Ref, class Ptr>

    而后文list类的模拟实现中,我对迭代器进行了两种typedef:

    1. typedef __list_iterator iterator;//普通迭代器
    2. typedef __list_iteratorconst T&, const T*> const_iterator;//const迭代器

    根据这里的对应关系:Ref对应的是&引用类型,Ptr对应的是*指针类型,这里如果我是普通对象传过来的迭代器,生成对应的普通迭代器,如果是const对象传递过来的迭代器,会生成对应的const迭代器。

    这样做的原因在于避免单独写一个支持不能修改迭代器指向结点数据的类而造成的复用性差。

    默认成员函数

    这里的默认成员函数我们只需要写构造函数。

    • 析构函数 -- 结点不属于迭代器,不需要迭代器释放
    • 拷贝构造 -- 默认浅拷贝即可
    • 赋值重载 -- 默认浅拷贝即可

    构造函数

    这里我们通过结点的指针即可完成构造。通过结点指针构造一个迭代器。

    1. //构造函数
    2. __list_iterator(Node* node)//通过结点指针构造一个迭代器
    3. :_node(node)
    4. {}

    * 运算符重载

    *解引用的目的是为了获取结点里的_data数据,因此我们直接return返回结点指向的_data即可。

    1. //*运算符重载
    2. Ref operator*()//结点出了作用域还在,用引用返回
    3. {
    4. return _node->_data;//返回结点指向的数据
    5. }

    ->运算符重载

    假设出现这样的场景,我链表存储的不是内置类型,而是自定义类型,如下:

    1. struct AA
    2. {
    3. AA(int a1 = 0, int a2 = 0)
    4. :_a1(a1)
    5. , _a2(a2)
    6. {}
    7. int _a1;
    8. int _a2;
    9. };
    10. void test()
    11. {
    12. cpp::list lt;
    13. lt.push_back(AA(1, 1));
    14. lt.push_back(AA(2, 2));
    15. lt.push_back(AA(3, 3));
    16. lt.push_back(AA(4, 4));
    17. }

    对于内置类型和自定义类型成员的指针,其访问方式都是不同的:

    1. int* *it
    2. AA* (*it). 或者 it->

    而这里我们应该重载一个->运算符。以便于访问自定义类型成员的指针的数据。

    1. //->运算符重载
    2. Ptr operator->()
    3. {
    4. return &(operator*());//返回结点指针所指结点的数据的地址
    5. //或者return &_node->_data;
    6. }

    实现了->运算符重载后,我们执行it->_a1,编译器将其转换成it.operator->(),此时获得的是结点位置的地址即AA*,而理应还有一个箭头->才能获取数据,也就是这样:it.operator->()->_a1

    • 总结:编译器为了可读性进行优化处理,如果不优化应该是it->->_a1,优化以后,省略了一个箭头->。

    ++运算符重载

    ++运算符分为前置++和后置++

    • 前置++

    迭代器++的返回值还是迭代器,这里的++是为了让结点指向下一个结点的指针,注意前置++是要返回自增后的结点指针。

    1. //前置++
    2. self& operator++()//迭代器++的返回值还是迭代器
    3. {
    4. _node = _node->_next;//直接让自己指向下一个结点即可实现++
    5. return *this;//返回自增后的结点指针
    6. }
    • 后置++

    为了区分前置++,后置++通常要加上一个参数以便区别。此外,后置++是返回自增前的结点指针。

    1. //后置++
    2. self operator++(int)//加参数以便于区分前置++
    3. {
    4. self tmp(*this);//拷贝构造tmp
    5. _node = _node->_next;//直接让自己指向下一个结点即可实现++
    6. return tmp;//注意返回tmp,才是后置++
    7. }

    --运算符重载

    --运算符也分前置--和后置--

    • 前置--

    前置--是让此结点指向上一个结点,最后返回自减后的结点指针即可。

    1. //前置--
    2. self& operator--()
    3. {
    4. _node = _node->_prev;//让_node指向上一个结点即可实现--
    5. return *this;
    6. }
    • 后置--

    注意传参以区分前置--,最后返回的是自减前的结点指针即可。

    1. //后置--
    2. self operator--(int)//记得传缺省值以区分前置--
    3. {
    4. self tmp(*this);//拷贝构造tmp
    5. _node = _node->_prev;
    6. return tmp;
    7. }

    !=运算符重载

    这里比较是否不等,是两个迭代器的比较,直接返回两个结点的位置是否不同即可。

    1. //!=运算符重载
    2. bool operator!=(const self& it)
    3. {
    4. return _node != it._node;//返回两个结点指针的位置是否不同即可
    5. }

    ==运算符重载

    这里直接返回俩结点指针是否相同即可。

    1. //==运算符重载
    2. bool operator==(const self& it)
    3. {
    4. return _node == it._node;//返回俩结点指针是否相同
    5. }

    3.2、反向迭代器

    反向迭代器是一个适配器模式(后文会将适配器)。相较于正向迭代器,反向迭代器有下面三种主要变化:

    • 反向迭代器的++执行的操作是正向迭代器里的--,
    • 反向迭代器里的--执行的操作是正向迭代器里的++
    • 反向迭代器的*解引用和->操作指向的是前一个数据

    总代码如下:

    1. namespace cpp
    2. {
    3. template<class Iterator, class Ref, class Ptr>
    4. struct Reverse_iterator
    5. {
    6. Iterator _it;
    7. typedef Reverse_iterator Self;
    8. //构造函数
    9. Reverse_iterator(Iterator it)
    10. :_it(it)
    11. {}
    12. //*运算符重载
    13. Ref operator*()
    14. {
    15. Iterator tmp = _it;
    16. //返回上一个数据
    17. return *(--tmp);
    18. }
    19. //->运算符重载
    20. Ptr operator->()
    21. {
    22. //复用operator*,返回上一个数据
    23. return &(operator*());
    24. }
    25. //++运算符重载
    26. Self& operator++()
    27. {
    28. --_it;
    29. return *this;
    30. }
    31. //--运算符重载
    32. Self& operator--()
    33. {
    34. ++_it;
    35. return *this;
    36. }
    37. //!=运算符重载
    38. bool operator!=(const Self& s)
    39. {
    40. return _it != s._it;//返回两个结点指针的位置是否不同即可
    41. }
    42. //==运算符重载
    43. bool operator==(const Self& s)
    44. {
    45. return _it == s._it;//返回俩结点指针是否相同
    46. }
    47. };
    48. }

    4、list类接口实现

    此接口的核心任务是为了模拟实现list类的一些核心功能,好比如插入删除,迭代器等等。

    基本框架

    在list类中的唯一成员变量即自定义类型的变量,由先前的结点类构成的头结点:

    1. /*3、模拟实现list的功能类*/
    2. template<class T>
    3. class list
    4. {
    5. typedef list_node Node;
    6. public:
    7. //正向迭代器
    8. typedef __list_iterator iterator;//普通迭代器
    9. typedef __list_iteratorconst T&, const T*> const_iterator;//const迭代器
    10. //反向迭代器适配支持
    11. typedef Reverse_iteratorreverse_iterator;
    12. typedef Reverse_iteratorconst T&, const T*> const_reverse_iterator;//const反向迭代器
    13. private:
    14. Node* _head;
    15. };

    4.1、默认成员函数

    构造函数

    • 无参构造:

    此目的是为了对哨兵位的头结点_head进行初始化:

    1. //构造函数
    2. list()
    3. {
    4. _head = new Node();//申请一个头结点
    5. _head->_next = _head;//头结点的下一个结点指向自己构成循环
    6. _head->_prev = _head;//头结点的上一个结点指向自己构成循环
    7. }
    • 传迭代器区间构造:

    先初始化,再利用循环对迭代器区间的元素挨个尾插即可。

    1. //传迭代器区间构造
    2. template <class InputIterator>
    3. list(InputIterator first, InputIterator last)
    4. {
    5. empty_init();
    6. while (first != last)
    7. {
    8. push_back(*first);
    9. first++;
    10. }
    11. }

    析构函数

    这里可以先复用clear函数把除了头结点的所有结点给删除掉,最后delete头结点即可。

    1. //析构函数
    2. ~list()
    3. {
    4. clear();
    5. delete _head;//删去哨兵位头结点
    6. _head = nullptr;
    7. }

    拷贝构造函数

    假设要用lt1拷贝构造lt2。

    • 传统写法:

    首先复用empty_init对头结点进行初始化,接着遍历lt1的元素,在遍历的过程中尾插将lt1的元素尾插到lt2上即可。这里直接利用push_back自动开辟空间完成深拷贝。

    1. //传统写法
    2. list(const list& lt)
    3. {
    4. //先初始化lt2
    5. empty_init();
    6. //遍历lt1,把lt1的元素push_back到lt2里头
    7. for (auto e : lt)
    8. {
    9. push_back(e);//自动开辟新空间,完成深拷贝
    10. }
    11. }
    • 现代写法:

    这里先初始化lt2自己,再把lt1引用传参传给lt,传lt的迭代器区间构造tmp,复用swap交换头结点指针即可完成深拷贝的现代写法。

    1. //现代写法
    2. list(const list& lt)
    3. {
    4. //初始化自己
    5. empty_init();
    6. listtmp(lt.begin(), lt.end());//借用迭代器区间去构造tmp
    7. //交换头结点指针即可完成深拷贝现代写法
    8. swap(tmp);
    9. }

    赋值运算符重载函数

    • 假设要把lt1赋值给lt2。

    这里直接给出现代写法。注意这里传值传参把lt1传给lt自定义类型传值传参调用拷贝构造,拷贝构造完成的是深拷贝生成了lt,复用swap函数交换lt1与lt的头结点指针指向即可,最后返回*this。

    1. //赋值运算符重载(现代写法)
    2. list& operator=(list lt)//套用传值传参去拷贝构造完成深拷贝
    3. {
    4. swap(lt);
    5. return *this;
    6. }

    4.2、迭代器相关函数

    begin和end

    • begin的作用是返回第一个位置的结点的迭代器,而第一个结点就是哨兵位头结点的下一个结点,因此,直接return返回_head的_next即可。
    • end的作用就是返回最后一个有效数据的下一个位置的迭代器,而这里对于list指的就是头结点_head的位置。

    begin和end均分为普通对象调用和const对象调用,因此要写两个版本。

    • 普通对象调用版
    1. //begin
    2. iterator begin()//begin返回的就是第一个有效数据,即头结点的下一个结点
    3. {
    4. return iterator(_head->_next);//构造了一个匿名对象,通过调用构造函数利用头结点指向的第一个结点作为参数,来返回头结点
    5. //return _head->_next; 也可以这样写
    6. }
    7. //end
    8. iterator end()
    9. {
    10. return iterator(_head);//end返回的是最后一个结点的下一个结点,就是头结点_head
    11. //return _head; 也可以这样写
    12. }
    • const对象调用版
    1. //begin
    2. const_iterator begin() const
    3. {
    4. return const_iterator(_head->_next);
    5. //return _head->_next;
    6. }
    7. //end
    8. const_iterator end() const
    9. {
    10. return const_iterator(_head);
    11. //return _head; 也可以这样写
    12. }

    rbegin和rend

    rbegin就是正向迭代器里end()的位置,rend就是正向迭代器里begin()的位置。

    rbegin和rend同样分为普通对象调用和const对象调用:

    • 普通对象调用:
    1. //rbegin()
    2. reverse_iterator rbegin()
    3. {
    4. return reverse_iterator(end());
    5. }
    6. //rend
    7. reverse_iterator rend()
    8. {
    9. return reverse_iterator(begin());
    10. }
    • const对象调用:
    1. //const反向迭代器
    2. const_reverse_iterator rbegin() const
    3. {
    4. return const_reverse_iterator(end());
    5. }
    6. const_reverse_iterator rend() const
    7. {
    8. return const_reverse_iterator(begin());
    9. }

    4.3、增加的相关函数

    insert

    实现insert首先创建一个新的结点存储插入的值,接着取出插入位置pos的结点为cur,记录cur的上一个结点位置prev,先链接prev和newnode,再链接newnode和cur即可。最后记得要返回新插入元素的迭代器位置。

    1. //insert,插入pos位置之前
    2. iterator insert(iterator pos, const T& x)
    3. {
    4. Node* newnode = new Node(x);//创建新的结点
    5. Node* cur = pos._node; //迭代器pos处的结点指针
    6. Node* prev = cur->_prev;
    7. //prev newnode cur
    8. //链接prev和newnode
    9. prev->_next = newnode;
    10. newnode->_prev = prev;
    11. //链接newnode和cur
    12. newnode->_next = cur;
    13. cur->_prev = newnode;
    14. //返回新插入元素的迭代器位置
    15. return iterator(newnode);
    16. }
    • 补充:list的insert不存在野指针和意义变了的迭代器失效问题。

    push_back尾插

    • 法一:

    首先,创建一个新结点用来存储尾插的值,接着找到尾结点。将尾结点和新结点前后链接构成循环,再将头结点和新结点前后链接构成循环即可。

    1. //尾插
    2. void push_back(const T& x)
    3. {
    4. Node* tail = _head->_prev;//找尾
    5. Node* newnode = new Node(x);//创建一个新的结点
    6. //_head tail newnode
    7. //使tail和newnode构成循环
    8. tail->_next = newnode;
    9. newnode->_prev = tail;
    10. //使newnode和头结点_head构成循环
    11. newnode->_next = _head;
    12. _head->_prev = newnode;
    13. }
    • 法二:

    这里也可以复用insert函数,当insert中的pos位置为哨兵位头结点的位置时,实现的就是尾插,因为insert插入是在pos位置前插入,而pos位哨兵位头结点时,在其前一个位置(尾部)插入就是实现了尾插。

    1. //尾插
    2. void push_back(const T& x)
    3. {
    4. //法二:复用insert
    5. insert(end(), x);
    6. }

    push_front头插

    直接复用insert函数,当pos位置为begin()时,获得的pos就是第一个有效结点数据,即可满足头插。

    1. //头插
    2. void push_front(const T& x)
    3. {
    4. insert(begin(), x);
    5. }

    4.4、删除的相关函数

    erase

    erase删除的是pos位置的结点,首先取出pos位置的结点为cur,记录cur上一个结点的位置为prev,再记录cur下一个结点的位置为next,链接prev和next,最后delete释放cur的结点指针即可。最后记得返回删除元素后一个元素的迭代器位置。

    1. //erase
    2. iterator erase(iterator pos)
    3. {
    4. assert(pos != end());
    5. Node* cur = pos._node;
    6. Node* prev = cur->_prev;
    7. Node* next = cur->_next;
    8. //prev cur next
    9. //链接prev和next
    10. prev->_next = next;
    11. next->_prev = prev;
    12. //delete要删去的结点
    13. delete cur;
    14. //返回删除元素后一个元素的迭代器位置
    15. //return next;
    16. return iterator(next);
    17. }

    pop_back尾删

    直接复用erase即可,当pos位置为--end()时,pos就是最后一个结点的位置,实现的就是尾删。

    1. //尾删
    2. void pop_back()
    3. {
    4. erase(--end());
    5. }

    pop_front头删

    直接复用erase即可,当pos位置为begin()时,pos就是第一个有效数据,实现的就是头删。

    1. //头删
    2. void pop_front()
    3. {
    4. erase(begin());
    5. }

    4.5、其它函数

    clear清除

    clear的作用是清除除了头结点外的所有结点,这里可以复用erase并通过遍历的方式一个一个删除。

    1. //clear清除
    2. void clear()
    3. {
    4. //复用erase
    5. iterator it = begin();
    6. while (it != end())
    7. {
    8. it = erase(it);//用it接收删除后的下一个结点的位置
    9. }
    10. }

    empty_init空初始化

    此函数的作用把哨兵位的头结点开出来,再对齐初始化。该函数是库里的。

    1. //空初始化 对头结点进行初始化
    2. void empty_init()
    3. {
    4. _head = new Node();
    5. _head->_next = _head;
    6. _head->_prev = _head;
    7. }

    swap交换

    对于链表的swap,直接交换头结点指针的指向即可完成。直接复用库函数的swap即可。

    1. //swap交换函数
    2. void swap(list& lt)
    3. {
    4. std::swap(_head, lt._head);//交换头指针
    5. }

    5、源码链接

    链接直达:list的模拟实现完整版

  • 相关阅读:
    旧版elasticsearch 2.3.4 集群部署过程
    KWin、libdrm、DRM从上到下全过程 —— drmModeAddFBxxx(20)
    Hifiasm-meta | 你没看错!基于宏基因组的完成图!!
    Gitlab 备份和恢复
    Traceroute
    时序预测 | MATLAB实现贝叶斯优化CNN-BiLSTM时间序列预测(股票价格预测)
    linux内核编程入门之proc文件读写
    【精彩回顾】 用sCrypt在Bitcoin上构建智能合约
    新华三H3CNE网络工程师认证—路由基础
    Ubuntu server 24 (Linux) 安装lua + 卸载软件
  • 原文地址:https://blog.csdn.net/bit_zyx/article/details/125796623