【C++】STL——list模拟实现(1)

简介: 【C++】STL——list模拟实现(1)

实现思路

 List是一个类模板,实际上是一个双向循环链表。在上一篇list基本使用的博客中,可以发现list支持了像vector一样的"下标访问",也就是通过迭代器区间去访问链表的每个节点数据。本人在初学阶段也是比较好奇——list的迭代器(正向与反向)是如何实现的;本篇博客将以自己所掌握的知识,详细的介绍list是如何实现的。


list实现结构:


       1.节点设计


       2.正向迭代器设计


       3.反向迭代器设计


       4.list各个接口完善(增删查改)

一、list的节点设计

list本身和list的结点是两个不同的结构,需要分开设计。以下是list的节点结构:

1ecd1b2606ed46e9956a89f231c9802c.png

/*ListNode.h*/
namespace mlg
{
  template<class T>
  struct ListNode
  {
    ListNode<T>* _prev; //节点的前指针
    ListNode<T>* _next; //节点的后指针
    T _data;            //节点的数据
    ListNode(const T& data= T())//初始化列表进行初始化
      :_prev(nullptr)
      ,_next(nullptr)
      ,_data(data)
    {}
  };
}

首先,我们在自己的命名空间内模拟实现list(为了防止与库冲突),上面的代码就是list节点的结构。在这里是用并没有使用class,因为struct默认访问权限是public,又因为节点是需要经常访问的,所以使用struct更好。


       我们将这个类加上 template<class T> 后,就能够实现节点存储不同类型的数据,这也是C++模板的好处。

二、list的初步框架

       当我们设计好了list的结点以后,我们就需要对list的基本框架进行设计,list要能够去控制这些节点,它的成员变量就必须是这个节点类;以下是list的结构设计:

/*List.h*/
namespace mlg //命名空间保持一致
{
  template<class T> //list是一个类模板,设计时需要这个
  class list
  {
    typedef ListNode<T> Node; //将其类型重命名方便书写,在后续的类型中会有比较多的typedef
  public:
      /*
        成员函数包含了默认成员函数、迭代器和增删查改
        */
  private:
    Node* _head; //带头结点的双向循环链表
  };
}

在以上两个基本的框架搭建完毕以后,接下来重点主要是正向与反向迭代器

三、list的正向迭代器设计

1.实现原理

       list的确是支持了迭代器,但是list不在能够像vector一样以普通的指针作为迭代器,因为其节点不保证在存储空间中连续。list的迭代器必须有能力指向list的节点,并有能力进行递增、递减、取值、成员存储等操作。如何具备这样的能力呢?那就是递增时指向下一个结点,递减时指向上一个节点,取值时取的是节点的数据值,成员取用时取用的是节点的成员;

1ecd1b2606ed46e9956a89f231c9802c.png

2. 正向迭代器的结构

/*iterator.h*/
namespace mlg
{
  template<class T,class Ref,class Ptr>
  struct __list_iterator
  {
    typedef ListNode<T> Node;
    typedef __list_iterator<T, Ref, Ptr> self;
    Node* _node;
        //迭代器构造函数
    __list_iterator(Node* x)
      :_node(x)
    {}
        //重载*号 —— 实现解引用操作
    Ref operator*()
    {
      return _node->_data;
    }
        //重载->操作符 —— 实现指针访问元素
    Ptr operator->()
    {
      return &_node->_data;
    }
    //++it 重载前置++ —— 让链表能够像数组一样去++操作,访问元素
    self& operator++()
    {
      _node = _node->_next;//前置++返回的是++之后的值,直接让当前位置的结点指向下一个节点
      return *this;
    }
    //it++ 重载后置++ —— (这里需要加上int作为一个站位符,与前置++区分)
    self operator++(int)
    {
      self tmp(*this);
      _node = _node->_next;//后置++返回的是++之前的值,需要保存当前节点,再指向下一个节点
      return tmp;
    }
    //--it 重载前置-- —— 让链表能够像数组一样去--操作,访问元素
    self& operator--()
    {
      _node = _node->_prev;//前置--返回的是--之后的值,直接让当前位置的结点指向前一个节点
      return *this;
    }
    //it-- 重载后置-- ——(这里需要加上int作为一个站位符,与前置--区分)
    self operator--(int)
    {
      self tmp(*this);
      _node = _node->_prev;//后置--返回的是--之前的值,需要保存当前节点,再指向下一个节点
      return tmp;
    }
        //重载==
    bool operator==(const self& it)const
    {
      return _node == it._node;
    }
        //重载!=
    bool operator!=(const self& it)const
    {
      return _node != it._node;
    }
  };
}

在上述代码中,有三个地方没有做解释:


1. template<class T,class Ref,class Ptr>


2. typedef __list_iterator<T, Ref, Ref>  self;


       它是迭代器类模板,里面包含了三个参数:T、Ref、Ptr;这三个参数表明传给iterator类的类型是不确定的,需要根据实际的情况,将这三个参数匹配到相应的地方。


如:


       1. ++/--操作:返回的是一个对象,只要用到了对象,一般都是写成__list_iterator<T, Ref, Ref>,因为类名太长,就有了第2点的typedef __list_iterator<T, Ref, Ref>  self;


       2. operator* 操作:返回的是对象的数据的值,并且*号是具有读写操作的,我们应该返回的是这个数据类型的引用(T&);


       3. operator->操作:返回的是对象的数据的地址,我们应该返回的是这个数据类型的地址(T*);


--------------------------------------------------------------------------------------------------------------------------


3. typedef ListNode<T> Node;

   Node* _node;


       iterator类中的成员变量也是节点,我们刚刚已经解释了list迭代器的原理,它是通过重载++ / --,其内部实现上是节点中的_prev指针(找当前节点的前一个位置,相当于--操作)和_next指针(找当前节点的下一个位置,相当于++操作)的操作来实现的;


       所以,list正向迭代器的实现,本质上是用节点的两个指针的操作来代替了传统的++/--操作。再简单点说,其实就是函数调用(包括*/->)

四、list反向迭代器的设计

1.实现原理

       我们刚刚对list正向迭代器做了介绍以后,你是否会想,反向迭代器也是同样的原理呢?确实可以这样理解,但是中间还有一个过程,我们先通下面的图解了解一下双链表(list)中和数组(vector)中正向迭代器与反向迭代器的区别。

1ecd1b2606ed46e9956a89f231c9802c.png

vector迭代器的位置不需要多说,对于list的迭代器:begin( )是数据的起始位置,end( )是数据的结束位置,也就是头结点;反向迭代器不就是与之相反嘛。


那如何才能通过反向迭代器控制链表的++/--等一系列操作呢?


       方法一:我们可以重新写一个,也通过节点的指针去控制(比较麻烦:如果我想用正向迭代器区获取某个节点的位置传个反向迭代器,就需要给反向迭代器增设正向迭代器的接口)


       方法二:反向迭代器通过正向迭代器去间接控制list节点,达到想要的效果(在SLT原码中是这样子实现的,其目的是能适应其他容器,其称之为迭代器适配器)

2.反向迭代器的结构

/*reverse_iterator.h*/
namespace mlg
{
  template<class Iterator,class Ref,class Ptr>
  class __list_reverse_iterator
  {
    typedef __list_reverse_iterator<Iterator, Ref, Ptr> self;
  public:
        //反向迭代器构造函数(通过正向迭代器去构出造反向迭代器)
    __list_reverse_iterator(Iterator it)
      :_it(it)
    {}
        //重载* 
    Ref operator*()
    {
      Iterator prev = _it; //获取正向迭代器end()的位置
      return *--prev;      //返回的是当前位置执行前置--操作后的解引用
    }
        //重载->
    Ptr operator->()
    {
      return &operator*(); //调用上面一个重载函数
    }
    //++it
    self& operator++()
    {
      --_it;        //反向迭代器的前置++就是调用正向迭代器的前置--
      return *this; //
    }
    //it++
    self operator++(int)
    {
      self tmp(*this); //后置++返回的是++之前的,所有先记录当前正向迭代器的位置
      _it--;           //反向迭代器的后置++就是调用正向迭代器的后置--
      return tmp;
    }
    //--it
    self& operator--()
    {
      ++_it;        //反向迭代器的前置--就是调用正向迭代器的前置++
      return *this;
    }
    //it--
    self operator--(int)
    {
      self tmp(*this); //后置--返回的是--之前的,所有先记录当前正向迭代器的位置
      _it++;           //反向迭代器的后置--就是调用正向迭代器的后置++
      return tmp;
    }
        //重载!=
    bool operator!=(const self& rit)const
    {
      return _it != rit._it;
    }
        //重载==
    bool operator==(const self& rit)const
    {
      return _it == rit._it;
    }
  private:
    Iterator _it;//成员变量是一个正向迭代器
  };
}

1.反向迭代器的 ++ / -- 操作解析

//++it
self& operator++()
{
  --_it;           //调用正向迭代器的前置--
  return *this;
}
//it++
self operator++(int)
{
  self tmp(*this);
  _it--;           //调用正向迭代器的后置--
  return tmp;
}

1ecd1b2606ed46e9956a89f231c9802c.png

通过上图可以发现:

1.反向迭代器++(前置/后置):调用正向迭代器的--

2.反向迭代器--(前置/后置):调用正向迭代器的++

2.反向迭代器的 * / -> 操作解析

//重载* 
Ref operator*()
{
  Iterator prev = _it; //获取正向迭代器end()的位置
  return *--prev;      //返回的是当前位置执行前置--操作后的解引用
}
//重载->
Ptr operator->()
{
  return &operator*(); //调用上面一个重载函数
}

1ecd1b2606ed46e9956a89f231c9802c.png

对于->的重载,只要去复用就可以了。


目录
相关文章
|
6天前
|
算法 C语言 C++
【c++丨STL】list的使用
本文介绍了STL容器`list`的使用方法及其主要功能。`list`是一种双向链表结构,适用于频繁的插入和删除操作。文章详细讲解了`list`的构造函数、析构函数、赋值重载、迭代器、容量接口、元素访问接口、增删查改操作以及一些特有的操作接口如`splice`、`remove_if`、`unique`、`merge`、`sort`和`reverse`。通过示例代码,读者可以更好地理解如何使用这些接口。最后,作者总结了`list`的特点和适用场景,并预告了后续关于`list`模拟实现的文章。
22 7
|
14天前
|
存储 编译器 C++
C++ initializer_list&&类型推导
在 C++ 中,`initializer_list` 提供了一种方便的方式来初始化容器和传递参数,而右值引用则是实现高效资源管理和移动语义的关键特性。尽管在实际应用中 `initializer_list&&` 并不常见,但理解其类型推导和使用方式有助于深入掌握现代 C++ 的高级特性。
16 4
|
23天前
|
存储 编译器 C语言
【c++丨STL】vector的使用
本文介绍了C++ STL中的`vector`容器,包括其基本概念、主要接口及其使用方法。`vector`是一种动态数组,能够根据需要自动调整大小,提供了丰富的操作接口,如增删查改等。文章详细解释了`vector`的构造函数、赋值运算符、容量接口、迭代器接口、元素访问接口以及一些常用的增删操作函数。最后,还展示了如何使用`vector`创建字符串数组,体现了`vector`在实际编程中的灵活性和实用性。
48 4
|
25天前
|
C语言 C++ 容器
【c++丨STL】string模拟实现(附源码)
本文详细介绍了如何模拟实现C++ STL中的`string`类,包括其构造函数、拷贝构造、赋值重载、析构函数等基本功能,以及字符串的插入、删除、查找、比较等操作。文章还展示了如何实现输入输出流操作符,使自定义的`string`类能够方便地与`cin`和`cout`配合使用。通过这些实现,读者不仅能加深对`string`类的理解,还能提升对C++编程技巧的掌握。
52 5
|
25天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
38 2
|
1月前
|
存储 算法 Linux
【c++】STL简介
本文介绍了C++标准模板库(STL)的基本概念、组成部分及学习方法,强调了STL在提高编程效率和代码复用性方面的重要性。文章详细解析了STL的六大组件:容器、算法、迭代器、仿函数、配接器和空间配置器,并提出了学习STL的三个层次,旨在帮助读者深入理解和掌握STL。
47 0
|
9天前
|
存储 编译器 C语言
【c++丨STL】vector模拟实现
本文深入探讨了 `vector` 的底层实现原理,并尝试模拟实现其结构及常用接口。首先介绍了 `vector` 的底层是动态顺序表,使用三个迭代器(指针)来维护数组,分别为 `start`、`finish` 和 `end_of_storage`。接着详细讲解了如何实现 `vector` 的各种构造函数、析构函数、容量接口、迭代器接口、插入和删除操作等。最后提供了完整的模拟实现代码,帮助读者更好地理解和掌握 `vector` 的实现细节。
18 0
|
2月前
|
存储 程序员 C++
C++常用基础知识—STL库(2)
C++常用基础知识—STL库(2)
79 5
|
2月前
|
存储 算法 C++
【C++打怪之路Lv10】-- list
【C++打怪之路Lv10】-- list
23 1
|
2月前
|
存储 自然语言处理 程序员
C++常用基础知识—STL库(1)
C++常用基础知识—STL库(1)
72 1