list部分接口模拟实现(c++)

简介: list简介

list简介

- list可以在常数范围内在任意位置进行插入和删除的序列式容器,并且容器可以双向迭代。
- list底层是一个带头双向链表结构,通过指针连接前一个和后一个元素。
- list支持在任意位置进行插入、删除元素,效率更好。
- list不支持随机访问

list基本框架

namespace xty
{
  //带头双向循环链表
  template<class T>
  struct list_node
  {
    list_node<T>* _prev;
    list_node<T>* _next;
    T _data;
  };
  template<class T>
  class list
  {
    typedef list_node<T> node;
  private:
    node* _head;//头指针
  };
}

list构造函数

我们实现一个无参构造函数,但是在这之前我们需要做一些准备工作,先实现一个申请list_node的构造函数,在struct类里面实现。

list_node结构体的默认构造

  //带头双向循环链表
  template<class T>
  struct list_node
  {
    list_node<T>* _prev;
    list_node<T>* _next;
    T _data;
    //在创建list_node变量时,自动调用构造
    list_node(const T& val = T())
      :_prev(nullptr)
      ,_next(nullptr)
      ,_data(val)
    {}
  };

为什么不使用class,而使用struct呢?

因为我们想达到这样一个目的:想要让别人自由的访问自己的成员函数,不做任何限制,就用struct。而list_node,list是要经常使用的,因此使用struct修饰list_node比较方便。

list类的默认构造

仅仅申请一个空的头结点,next都指向头

f72de92a78ca452cbcb9eaaadd7986c5.png

    list()
    {
      //两种申请方法都可以
      //_head = new list_node<T>
      _head = new node;
      _head->next = _head;
      _head->prev = _head;
      //_head->_data已经在new的时候调用构造了
    }

push_back()

先记录一下尾结点,插入更简单。

    void push_back(const T& x)
    {
      //留记录尾结点
      node* tail = _head->_prev;
      node* new_node = new node(x);//传入x值
      tail->_next = new_node;
      new_node->_next = _head;
      _head->_prev = new_node;
      new_node->_prev = tail;
    }

teartor迭代器

整体框架:

  //iteartor
  template<class T>
  struct __list_iterator
  {
    typedef list_node<T> node;
    node* _node;//成员变量
    //构造函数
    __list_iterator(node* x)
      :_node(x)
    {}
    T& operator*()
    {
      return _node->_data;
    }
    //++返回相应的迭代器
    __list_iterator<T> operator++()
    {
      _node = _node->_next;
      return *this;
    }
    //是位置是否相等,不是内容是否相等。
    bool operator!=(__list_iterator<T> x)
    {
      return _node != x._node;
    }
  };
  template<class T>
  class list
  {
    typedef list_node<T> node;
  public:
    //迭代器重命名
    typedef __list_iterator<T> iterator;
  public:
    //仅仅申请一个空的头结点,next都指向头
    list()
    {
      //两种申请方法都可以
      //_head = new list_node<T>
      _head = new node;
      _head->_next = _head;
      _head->_prev = _head;
      //_head->_data已经在new的时候调用构造了了
    }
    iterator begin()
    {
      iterator it(_head->_next);
      return it;
    }
    iterator end()
    {
      return iterator(_head);
    }

语法细节理解:

  1. 为什么把iterator和list进行单独封装?写一块不好吗?

首先list只会用到迭代器的begin()和end()函数。其他的像++,其实都是通过迭代器的对象调用的(并不是list的对象调用的)。把iterator从list中抽离出来,不仅可以减少list的复杂性,还能让人更加清楚,迭代器其实也是一个模板,它能被抽离出来,用以实现各种数据结构的迭代!而list内部的begin和end接口,千篇一律。这样就达到的封装的目的!所以,还是分开写的好,逻辑更清晰,更容易维护。

2.struct __list_iterator结构体里面为什么要写构造函数呢?

在c++里struct也被当做是类!类里有构造函数很正常,还可以有拷贝构造呢!只不过struct默认是public的。这样我们在声明该类型的变量时,函数会自动调用构造函数,使该结构体的数据自动是被初始化过的。

  xty::list<int>::iterator it = lt.begin();  //调用对象需要用list
  //xty::list<int>::iterator it(lt.begin());  //调用对象需要用list
  while (it != lt.end())
  {
    cout << *it << endl;
    ++it;
  }

写了构造函数之后,第二种声明方式也是可以的。而第一种方式其实调用的是拷贝构造函数,但是编译器给优化成了拷贝构造,我们没有写拷贝构造,编译器会调用默认的拷贝构造,是一个浅拷贝。但是我们不是经常说浅拷贝会造成析构问题?这里为什么不会?因为我们没有写析构函数,而且析构函数。为什么不写析构函数呢?因为没有什么可以析构的,函数的结点是list里的内容,不需要迭代器的析构函数管,因此我们不写析构函数。


3.迭代器++返回的是迭代器的类型。

4.注意:_list_iterator是类名,_list_iterator才是类型!

5.list里面的begin要返回迭代器类型,然而怎么由指针转化成迭代器类型呢?要利用迭代器的构造函数来返回。

迭代器里面的其他接口

    bool operator==(const __list_iterator<T>& x)
    {
      return _node == x._node;
    }
    //i++
    __list_iterator<T> operator++(int)
    {
      __list_iterator<T> tem(*this);
      _node = _node->_next;
      return tem;
    }
    __list_iterator<T> operator--()
    {
      _node = _node->_prev;
      return *this;
    }
    __list_iterator<T> operator--(int)
    {
      __list_iterator<T> tem(*this);
      _node = _node->_prev;
      return tem;
    }


语法细节理解:

  1. 注意迭代器传进来的参数基本上都是迭代器,如果不更改,最好加上const和&。
  2. 如果命名空间冲突,注意在函数声明和定义的时候也要加上空间名。
void Print(xty::list<int>& lt);
  1. 我们发现__list_iterator 有点长,我们重命名一下。
  //iteartor
  template<class T>
  struct __list_iterator
  {
    typedef list_node<T> node;
    typedef __list_iterator<T> self;
    node* _node;//成员变量
    //构造函数
    __list_iterator(node* x)
      :_node(x)
    {}
    T& operator*()
    {
      return _node->_data;
    }
    //++返回相应的迭代器
    self operator++()
    {
      _node = _node->_next;
      return *this;
    }
    //是位置是否相等,不是内容是否相等。
    bool operator!=(const __list_iterator<T>& x)
    {
      return _node != x._node;
    }
    bool operator==(const __list_iterator<T>& x)
    {
      return _node == x._node;
    }
    //i++
    self operator++(int)
    {
      self tem(*this);
      _node = _node->_next;
      return tem;
    }
    self operator--()
    {
      _node = _node->_prev;
      return *this;
    }
    self operator--(int)
    {
      self tem(*this);
      _node = _node->_prev;
      return tem;
    }
  };

const迭代器

要实现const迭代器只需要再写一个const类即可。

记住是指针可以修改,但是内容不可以修改,因此不能在this那加const。

    //迭代器重命名
    typedef __list_iterator<T> iterator;
    typedef const__list_iterator<T> const_iterator;
  public:
    const_iterator begin()const
    {
      const_iterator it(_head->_next);
      return it;
    }
    const_iterator end()const
    {
      return const_iterator(_head);
    }

list里面的迭代器修改仅仅需要,typedef一下,然后将构造函数改成所需要的const类型即可。

而我们需要再实现一个const类型的iterator

  template<class T>
  struct const__list_iterator
  {
    typedef list_node<T> node;
    typedef const__list_iterator<T> self;
    node* _node;//成员变量
    //构造函数
    const__list_iterator(node* x)
      :_node(x)
    {}
    const T& operator*()  //值不仅可以修改!!
    {
      return _node->_data;
    }
    //++返回相应的迭代器
    self operator++()  //指针可以修改!!
    {
      _node = _node->_next;
      return *this;
    }
    //是位置是否相等,不是内容是否相等。
    bool operator!=(const self& x)const
    {
      return _node != x._node;
    }
    bool operator==(const self& x)const
    {
      return _node == x._node;
    }
    //i++
    self operator++(int)  //指针可以修改!!!
    {
      self tem(*this);
      _node = _node->_next;
      return tem;
    }
    self operator--()  //指针可以修改!!!
    {
      _node = _node->_prev;
      return *this;
    }
    self operator--(int)  //指针可以修改!!!
    {
      self tem(*this);
      _node = _node->_prev;
      return tem;
    }
  };

问题:代码写完之后,我们发现实际上只有operator*()的返回值加了const,其他的地方没有改,因此,我们利用模板参数赋用解决问题。

通过模板参数实现复用

  template<class T,class Ref>
  struct __list_iterator
  {
    typedef list_node<T> node;
    typedef __list_iterator<T,Ref> self;
    node* _node;//成员变量
    //构造函数
    __list_iterator(node* x)
      :_node(x)
    {}
    Ref operator*()
    {
      return _node->_data;
    }
  ...
  }
  template<class T>
  class list
  {
    typedef list_node<T> node;
  public:
    //迭代器重命名
    typedef __list_iterator<T, T&> iterator;
    typedef __list_iterator<T,const T&> const_iterator;
  public:
    //仅仅申请一个空的头结点,next都指向头
    list()
    {
      //两种申请方法都可以
      //_head = new list_node<T>
      _head = new node;
      _head->_next = _head;
      _head->_prev = _head;
      //_head->_data已经在new的时候调用构造了了
    }
  }


3bdf5c66651348a0b47a29fc3e0c9eed.png


通过增加类模板参数,这种问题被很巧妙的解决了。请好好体会!

operator->()

当我们遇到自定义类型数据链表时,访问数据就会比较麻烦。

  struct AA
  {
    int _a1;
    int _a2;
    AA(int a1 = 0, int a2 = 0)
      :_a1(a1)
      , _a2(a2)
    {}
  };
  void test_aa()
  {
    xty::list<AA> lt;
    lt.push_back(AA(1, 1));
    lt.push_back(AA(2, 2));
    lt.push_back(AA(3, 3));
    lt.push_back(AA(4, 4));
    xty::list<AA>::iterator it = lt.begin();
    while (it != lt.end())
    {
      cout << (*it)._a1 << ":" << (*it)._a2 << endl;
      ++it;
    }
  }

如上例子所示,cout方式,在这里很是别扭,因为it是迭代器,我们能不能通过重载->来访问这样的数据呢?

显然是可以的!如下:

    T* operator->()
    {
      return &(_node->_data);
    }

所以我们通过如下方式打印链表的数据:

    xty::list<AA>::iterator it = lt.begin();
    while (it != lt.end())
    {
      //cout << (*it)._a1 << ":" << (*it)._a2 << endl;
      cout << it->_a1 << ":" << it->_a2 << endl;
      ++it;
    }

但是这个代码是不是有一点别扭?没看出来的话,我再打印一次:

      //两种打印方式一样!!!
      cout << it->_a1 << ":" << it->_a2 << endl;
      cout << it.operator->()->_a1 << ":" << it.operator->()->_a2 << endl;


26bb0f51190a4728b8913d942863af5b.png

==解释:==之所以出现这样的原因,是因为编译器自动把两个连续的->优化成一个->,防止观感上的差异,这样设计能让人立马看出这个其实是在访问AA的数据。

为了适应const和非const两种迭代器,我们再设计一个模板参数。如下实例:

  //iteartor
  template<class T,class Ref, class Ptr>
  struct __list_iterator
  {
    typedef list_node<T> node;
    typedef __list_iterator<T,Ref,Ptr> self;
    node* _node;//成员变量
    Ptr operator->()
    {
      return &(_node->_data);
    }
...................
  }
template<class T>
  class list
  {
    typedef list_node<T> node;
  public:
    //迭代器重命名
    typedef __list_iterator<T, T&, T*> iterator;
    typedef __list_iterator<T,const T&,const T*> const_iterator;
  public:

反向迭代器

库里面反向迭代器是用正向迭代器实现的!即把正向迭代器的++,重载成反向迭代器的–。

242688a428af41ba90719aa1d978481c.png

并且迭代器的rbegin()和rend(),恰好和正向迭代器相反!

具体实现

namespace xty
{
  template<class Iterator, class Ref, class Ptr >
  struct RverseIterator
  {
    typedef RverseIterator<Iterator, Ref, Ptr> Self;
    Iterator _cur;  //正向迭代器的对象
    RverseIterator(Iterator it)
      :_cur(it)
    {}
    Ref operator*()
    {
      Iterator tem = _cur;
      --tem;
      return *tem;
    }
    //Iterator& operator++()
    //{
    //  _cur--;
    //  return *this;
    //}
    Self& operator++()
    {
      _cur--;
      return *this;
    }
    Self& operator--()
    {
      _cur++;
      return *this;
    }
    bool operator!=(const Self& it)
    {
      return _cur != it._cur;
    }
   };
}

其中list类里面再添加typedef和rbegin和rend()内容!

其中list类里面再添加typedef和rbegin和rend()内容!

这样使用正向迭代器封装的反向迭代器就实现了

insert()

    //在pos位置前插入,返回插入位置的迭代器
    iterator insert(iterator pos, const T& x)
    {
      node* new_node = new node(x);
      node* cur = pos._node;
      node* prev = cur->_prev;
      prev->_next = new_node;
      new_node->_prev = prev;
      new_node->_next = cur;
      cur->_prev = new_node;
      return iterator(cur);
    }

erase()

返回删除元素的下一个位置的迭代器。

    iterator erase(iterator pos)
    {
      //不能删头
      assert(pos._node!=_head);
      node* cur = pos._node;
      node* prev = cur->_prev;
      prev->_next = cur->_next;
      cur->_next->_prev = prev;
      delete cur;
      return iterator(prev->_next)
    }

注意:删除元素后,pos位置的数据就被删除了,会产生迭代器失效问题,如果再使用pos需要重新赋值。

clear()

清空list的内容,可以借助迭代器和erase()来删除。

    void clear()
    {
      iterator it = begin();
      while (it != end())
      {
        it = erase(it);
        //erase(it++);
      }
    }

析构函数

结合clear()来编写。

    ~list()
    {
      clear();
      delete _head;
      _head = nullptr;
    }

迭代器区间构造

因为迭代器区间构造,也需要一个头结点,所以,我们方便复用,又定义了一个初始化函数,具体改动如下:

    list()
    {
      empty_init();
      //两种申请方法都可以
      //_head = new list_node<T>
      //_head = new node;
      //_head->_next = _head;
      //_head->_prev = _head;
      //_head->_data已经在new的时候调用构造了了
    }
    void empty_init()
    {
      _head = new node;
      _head->_next = _head;
      _head->_prev = _head;
    }
    template<class Iterator>
    list(Iterator first, Iterator last)
    {
      empty_init();
      while (first != last)
      {
        push_back(*first);
        ++first;
      }
    }

拷贝构造

提供了两种方法,第一种方法效率较低,第二种利用swap提高了效率。

    lt2(lt)
    //list(const list<T>& lt)
    //{
    //  empty_init();
    //  for (auto e : lt)
    //  {
    //    push_back(e);
    //  }
    //}
    void swap(list<T>& tem)
    {
      std::swap(_head, tem._head);
    }
    list(const list<T>& lt)
    {
      empty_init();//初始化this
      list<T> tem(lt.begin(), lt.end());
      swap(tem);
    }

operator=()

实现较为简单。

      //注意这里不能传引用
      list<T>& operator=(list<T> lt)
      {
        swap(lt);
        return *this;
      }

最后一个问题:

const list<int> lt;

这行代码能调用构造函数吗?

答案是肯定的,因为变量在最开始是没有const属性的,定义好了以后,才有const属性。否则const都没法定义出来了。

目录
相关文章
|
5月前
|
存储 Java 测试技术
滚雪球学Java(57):解密Java中List接口底层实现原理
【6月更文挑战第11天】🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
43 2
滚雪球学Java(57):解密Java中List接口底层实现原理
|
4月前
|
文字识别 Java
文本,文字识别07,SpringBoot服务开发-入参和返回值,编写接口的时候,要注意识别的文字返回的是多行,因此必须是List集合,Bean层,及实体类的搭建
文本,文字识别07,SpringBoot服务开发-入参和返回值,编写接口的时候,要注意识别的文字返回的是多行,因此必须是List集合,Bean层,及实体类的搭建
|
5月前
|
存储 算法 C++
C++一分钟之-容器概览:vector, list, deque
【6月更文挑战第21天】STL中的`vector`是动态数组,适合随机访问,但插入删除非末尾元素较慢;`list`是双向链表,插入删除快但随机访问效率低;`deque`结合两者优点,支持快速双端操作。选择容器要考虑操作频率、内存占用和性能需求。注意预分配容量以减少`vector`的内存重分配,使用迭代器而非索引操作`list`,并利用`deque`的两端优势。理解容器内部机制和应用场景是优化C++程序的关键。
64 5
|
5月前
|
编译器 C++ 容器
【C++/STL】:list容器的深度剖析及模拟实现
【C++/STL】:list容器的深度剖析及模拟实现
45 2
|
5月前
|
存储 C++ 容器
【C++/STL】:list容器的基本使用
【C++/STL】:list容器的基本使用
40 1
|
5月前
|
编译器 C语言 C++
C++ STL中list迭代器的实现
C++ STL中list迭代器的实现
C++ STL中list迭代器的实现
|
5月前
|
存储 C++
C++初阶学习第十一弹——探索STL奥秘(六)——深度刨析list的用法和核心点
C++初阶学习第十一弹——探索STL奥秘(六)——深度刨析list的用法和核心点
53 7
|
4月前
|
前端开发
若依修改,配置了一个接口路径出现了,如何放通接口{ “msg“: “请求访问:/code/list,认证失败,无法访问系统资源“, “code“: 401}
若依修改,配置了一个接口路径出现了,如何放通接口{ “msg“: “请求访问:/code/list,认证失败,无法访问系统资源“, “code“: 401}
|
4月前
|
存储 算法 程序员
C++基础知识(八:STL标准库(Vectors和list))
C++ STL (Standard Template Library标准模板库) 是通用类模板和算法的集合,它提供给程序员一些标准的数据结构的实现如 queues(队列), lists(链表), 和 stacks(栈)等. STL容器的提供是为了让开发者可以更高效率的去开发,同时我们应该也需要知道他们的底层实现,这样在出现错误的时候我们才知道一些原因,才可以更好的去解决问题。

热门文章

最新文章

下一篇
无影云桌面