STL容器介绍——list的源码剖析和部分代码模拟实现

list

1.list概述

好说,相信能点进这篇文章的大部分人应该都了解和学习过list——链表
相较于vector——顺序表(可随机访问),list的好处就在于它不需要开辟一些可能会浪费的空间,在它每次插入和删除元素的同时,对于堆栈上的空间就只会相对应的配置和释放一个元素内存。

这也同时给它赋予另外一个好处:
插入push删除pop的运算时间都只用常数时间O(N)

关于这两个最常用容器的选择,可以去参考网上文章,这里就不做赘述。

2.list的数据结构

list是链表,我们可以先理解为是一条绳子,那么绳子就应该将东西穿起来才能最后构成链表这一数据结构。
所以node的结构和list应该是不一样的,我们需要分开来进行设计

我们来先看看开发者团队的源代码

template <class T>
struct __list_node {
  typedef void* void_pointer;
  void_pointer next;
  void_pointer prev;
  T data;
};

看到next和prev就应该知道了,这就是一个双向链表

node_struct

我们也来自己搭建一个节点node的结构类:

2.1节点模仿代码

    template <class T>
    struct _list_node//节点
    {
        T _val;
        _list_node<T> *_next;
        _list_node<T> *_prev;

        _list_node(const T &val) //要加引用,不然string vector 就会进行拷贝构造
            : _val(val), _prev(nullptr), _next(nullptr)
        {
        }
    };

这里我们和源码不同的是,我们在这里要加入一个构造函数进行初始化。

原因是:我在这构建的双向链表,是带哨兵位双向链表,所以一定要先初始化。

深受一些大佬的代码影响,本人也很喜欢并推荐大家使用_下划线来加在成员变量前面,便于之后于成员函数的区分。(尤其是初始化列表中)

2.2再来看看list的结构

开发者源代码:
template <class T,class Alloc = alloc>
class list
{
protected:
    typedef __list_node<T> list_node;
public:
    typedef list_node* link_type;

protected:
    link_type node;
...
}

这里的list是一个结构非常完美的环形双向链表,所以只要找到了end节点,就可以来回遍历整个链表。

3.list的迭代器(operator)

看过我上一篇文章的同学应该知道,在设计vector的时候,迭代器的构造其实就是原生指针的使用。

但这里不大一样,我们要了解,vector这样的顺序表,在堆栈内存空间上是连续的,所以利用原生指针可以很好的对vector进行操作(*,++,–等)。
list不同,因为其节点不保证在储存空间中连续存在,所以不能像vector那样,用原生指针作为迭代器。

设计的思路是,list的迭代器必须要有能力指向list的内存,然后还要具备能力,能够进行正确的++,--,*,&....

那么我们设计一个_list_iterator这样的类来对原生指针进行封装,并在此类里面设计一系列功能(前移,后移,运算符重载等)。

3.1开发者源代码

template<class T, class Ref, class Ptr>
struct __list_iterator {
  typedef __list_iterator<T, T&, T*>             iterator;
  typedef __list_iterator<T, const T&, const T*> const_iterator;
  typedef __list_iterator<T, Ref, Ptr>           self;

  typedef bidirectional_iterator_tag iterator_category;
  typedef T value_type;
  typedef Ptr pointer;
  typedef Ref reference;
  typedef __list_node<T>* link_type;
  typedef size_t size_type;
  typedef ptrdiff_t difference_type;

  link_type node;//节点指针

  __list_iterator(link_type x) : node(x) {}
  __list_iterator() {}
  __list_iterator(const iterator& x) : node(x.node) {}

  bool operator==(const self& x) const { return node == x.node; }
  bool operator!=(const self& x) const { return node != x.node; }
  reference operator*() const { return (*node).data; }

#ifndef __SGI_STL_NO_ARROW_OPERATOR
  pointer operator->() const { return &(operator*()); }
#endif /* __SGI_STL_NO_ARROW_OPERATOR */

  self& operator++() { //往后一个节点(前置++
    node = (link_type)((*node).next);
    return *this;
  }
  self operator++(int) { //往后一个节点(后置++
    self tmp = *this;
    ++*this;
    return tmp;
  }
  self& operator--() { //往前一个节点(前置--
    node = (link_type)((*node).prev);
    return *this;
  }
  self operator--(int) { //往前一个节点(后置--
    self tmp = *this;
    --*this;
    return tmp;
  }
};

但在这,我在第一次见源代码的时候其实就很困惑,为啥会有三个模版参数呢?!

T很好理解,但是RefPtr呢?

先模仿这个框架先搭建一个阉割版迭代器类:

template<class T>
struct _list_iterartor//用struct相当于成员全是public
{
    typedef _list_node<T> node;
    typedef _list_iterator<T> self;

    node* _pnode;
    ...
    T& operator*()
    {
        return _pnode->_val;
    }
    ...

 }

利用上面写的迭代器,那么我们的list类中,正确使用迭代器就成了我们的问题

template<class T>
class list
{
   typedef _list_node<T> node;
   public:
   typedef _list_iterator<T> iterator;
   ...
}

就拿比较通俗易懂的例子来说,如果我创建了一个printList这样的函数来访问list中的成员,利用刚刚创建的迭代器,我们是可以对类中的成员进行修改的!而这样我们的封装就失去了意义,因为有时候我们是不想用户能够修改我的成员变量。

void printList(const list<int>& lt)
{
    list<int>::iterator it = lt.begin();
    while(it != lt.end())
    {
        *it += 1;//这串代码不会报错,且顺利修改成员变量
        cout << *it << " ";
        ++it;
    }
    cout << endl;
}

所以在这里肯定是需要一个,只可读不可写的迭代器,也就是我们待会写的const_iterator

typedef _list_iterator<T> const_iterator

单单这样修改肯定还是没用的,因为就算你将刚刚printList函数中的iterator修改成const_iterator也没有用,因为在printList中,模版参数给的是const类型的list类,返回给it同样也不起作用,因为it本身就属于非const类型,只是用了一个带有const的类型名字罢了。


有两种解决办法:

  • 1.设计两个类来封装(const和非const)
  • 2.设计一个带有三个模版参数的类

先来说说传统的第一种解决思路吧:
这个好想,就是把刚刚的_list_iterator这一个类拷贝一份,设计一份新的_list_const_iterator来控制const类型的迭代器。

template<class T>
struct _list_iterartor//非const
{
    typedef _list_node<T> node;
    typedef _list_iterator<T> self;

    node* _pnode;
    ...
    T& operator*()
    {
        return _pnode->_val;
    }
    ...
 }

template<class T>//const类型
struct _list_const_iterator
{
    typedef _list_node<T> node;
    typedef _list_iterator<T> self;

    node* _pnode;
    ...
    const T& operator*()
    {
        return _pnode->_val;
    }
    ...
}

这样就可以很好控制了

但是这样的代码有点冗余了,所以介绍一下第二种方法,也就是开发者团队的设计的巧妙方法。

template <class T, class Ref, class Ptr> 
struct _list_iterator
{
    typedef _list_node<T> node;
    typedef _list_iterator<T, Ref, Ptr> self;
    node* _pnode;
    Ref iterator*()//reference
    {
        return _pnode->_val;
    }
    Ptr iterator->()//pointer
    {
        return &_pnode->_val;
    }
    ...
    
}
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;
    ...
private:
    node* _head;
}

这样的话,你传的参数不管是const也好还是非const也罢,都可传回到RefPtr进行相对应的操作。

其实实际上是让编译器去帮我们将类模版实例化出了两个类,本质和方法一没有太大区别

接下来放出自己代码中关于迭代器的接口和重载函数

    template <class T, class Ref, class Ptr> //Ref - T&
    struct _list_iterator//封装_list_node的普通指针,作用其实类似
    {
        typedef _list_node<T> node;
        typedef _list_iterator<T, Ref, Ptr> self;
        node *_pnode; //对链表指针进行封装

        //拷贝函数,operator=,析构我们不写,编译器默认生成就可以用
        //构造函数
        _list_iterator(node *node)
            : _pnode(node)
        {
        }

        Ref operator*() //读和写
        {
            return _pnode->_val;
        }
        Ptr operator->()
        {
            return &_pnode->_val;
        }
        bool operator!=(const self &s)
        {
            return _pnode != s._pnode;
        }
        bool operator==(const self &s)
        {
            return _pnode == s._pnode;
        }
        bool operator!=(const self &s) const
        {
            return _pnode != s._pnode;
        }
        bool operator==(const self &s) const
        {
            return _pnode == s._pnode;
        }
        self &operator++()
        {
            _pnode = _pnode->_next;
            return *this;
        }
        self &operator--()
        {
            _pnode = _pnode->_prev;
            return *this;
        }
        self operator++(int)
        {
            self tmp(*this);
            _pnode = _pnode->_next;
            return tmp;
        }
        self operator--(int)
        {
            self tmp(*this);
            _pnode = _pnode->_prev;
            return tmp;
        }
    };

4.list的接口设计

4.1 push_back —— 尾插

作为构造链表的重要接口之一,其实如果学过数据结构的大伙们都知道,push_back其实挺好写的,将结构牢记于心就可以慢慢搭建出这个结构来。

    void push_back(const T &x) //传引用
    {
        node *newnode = new node(x); //调用构造函数初始化
        node *_tail = _head->_prev;
        _tail->_next = newnode;
        newnode->_prev = _tail;
        newnode->_next = _head;
        _head->_prev = newnode;
    }

4.2 insert —— 插入

插入也好说,先配置并构造一个节点,然后在pos后面插入元素x

    void insert(iterator pos, const T &x) //插入数据
    {
        assert(pos._pnode);
        node *cur = pos._pnode;
        node *prev = cur->_prev;
        node *newnode = new node(x);
        prev->_next = newnode;
        newnode->_prev = prev;
        newnode->_next = cur;
        cur->_prev = newnode;
    }

这里不同于vector 插入insert并不会使得迭代器会失效,因为链表的数据本身在内存中就是随机散布开的。

4.3 erase —— 删除

erase这里的设计比较巧妙,他会将删除节点位置处的下一个迭代器返回回来

    iterator erase(iterator pos)
    {
        node *prev = pos._pnode->_prev;
        node *next = pos._pnode->_next;
        delete pos._pnode;
        prev->_next = next;
        next->_prev = prev;

        return iterator(next);
    }

4.4 其余接口 —— pop_front pop_back push_front clear ~list

其余这些,我们就疯狂复用刚刚实现的接口就行了

void pop_front()
{
    erase(begin());
}
void push_front(const T& x)
{
    insert(begin(),x);
}
void pop_back()
{
    erase(end());
    end();//别忘了调整水位
}
void clear()
{
    iterator it = begin();
    while(it != end())
    {
        it = erase(it);
    }
}
~list()
{
    clear();
    delete _head;//在list类中的成员变量
    _head = nullptr;
}

这样,基本的功能就都实现了~

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值