新手必看!1篇教你写出“不闲置、真有用”的高价值list

博主的博客主页——>点击这里
博主的gitee主页——>点击这里

提示:本文难度较大,如有不明白不清楚的地方,欢迎私下与作者私信交流。


前言以及list文档入口

前面我们学习了string和vector这两个容器,这两个容器相较下,还是比较简单的。概括起来就是:元素在内存中都是连续存放的,并且迭代器的实现几乎没有遇到阻碍,迭代器较为简单。
list文档入口
强调:大家要学会自己去查文档,根据文档里面接口的实现规则自己学着去实践,这样才是正确的学习方法。


提示:以下是本篇文章正文内容,下面案例可供参考

一、list是怎样的一种容器

list就相当于我们数据结构中的带头双向链表(注意是带头并且双向),单链表并不是list的底层逻辑。
在这里插入图片描述
大家可以看到list是由一个一个独立的结点通过指针连接在一起的一种容器,它不像我们数组那样存储的都是在连续的地址,而且大家发现,list的迭代器不再像数组那样直接指向数据,而是指向了一个结点

二、模拟实现

前面我已经为大家铺垫了list的底层结构。分别需要结点,指向头结点的指针,以及迭代器的实现,其中迭代器的实现是最重要也是最有难度的。

1.结点的创建

结点需要三大元素:1.存储的数据2.指向下一个结点的指针3.指向下一个结点的指针。以及结构体还要有对结点的初始化。

template<class T>
struct list_node
{
    T _data;//数据
    list_node* _next;//指向上一个结点的指针
    list_node* _prev;//指向下一个结点的指针
    //创建一个结点时必要的初始化
    list_node(const T& x = T())
        :_data(x)
        ,_next(nullptr)
        ,_prev(nullptr)
      {}      
};

class与struct的区别?
一般来说,如果你要你的所有成员都是共有的(就是别人可以随便访问),就使用struct关键字
因为class里面有private/protected/public访问限定符,所以class一般用来区分一些成员的被访问权限。

2.链表的定义

首先我们要有头结点,然后要插入的话再创建结点,让指针去指向即可。

template<T>
class list
{
public:
    typedef list_node<T> Node;//名字太长,缩短,方便观察
    list()//头结点的初始化,就是让其两个指针均指向自己,存不存数据无所谓(一般头结点不存储数据)
    {
       _head = new Node();//让_head指向第一个创建的结点
       _head->_next = _head;
       _head->_prev = _head;
    }
private:
    Node* _head;//头指针是指向结点类型的指针
};

链表的定义已经完成了,现在让我们来实现头插头删,尾插尾删。
①尾插

void push_back(const T& x)
{
    Node* newnode = new Node(x);
    Node* cur = _head->_prev;
    newnode->_prev = cur;
    newnode->_next = _head;
    cur->_next = newnode;
    _head->_prev = newnode;
	 
}

②头插

void push_front(const T& x)
{
    Node* newnode = new Node(x);
    Node* cur = _head->_next;
    cur->_prev = newnode;
    _head->_next = newnode;
    newnode->_head = cur;
    newnode->_prev = _head;
}

③尾删

void pop_back()
{
    Node* old_node = _head->_prev;
    Node* cur = _head->_prev->_prev;
    cur->_next = _head;
    _head->_prev = cur;
    delete old_node;
}

④头删

void pop_front()
{
    Node* old_node = _head->_prev;
    Node* cur = _head->_prev->_prev;
    cur->_prev = _head;
    _head->_next = cur;
    delete old_node;
}

关于链表的基本操作我们就已经写到这里,应该还是比较好理解的吧。

3.迭代器的实现(重点)

以前我们在学习vector的时候,指向元素的指针就充当了我们的迭代器。但是指针只是特殊的迭代器(前提是指向数组的指针),vector的指针刚好指向数据,解引用就是这个数据,++指针就指向下一个数据,非常的方便,让大家误以为迭代器就是这么简单
在这里插入图片描述

那么,指向结点的指针呢?解引用只是得到这个结点,而非结点里面的数据,至于指针的加加和减减?链表的指针你怎么进行加减?它没有像数组的指针那样拥有天然的优势,加加就到指向下一个元素。

解决方法:封装类实现迭代器
我们可以定义一个类,类中唯一的变量就是指向这些结点的指针。
那么问题来了,解引用指针只是得到这个结点,那怎么办才能得到这个结点中的数据呢?
其实,operator就可以帮我们重新定义一些运算符的规则。
在这里插入图片描述
如图,list_iterator就是我们的迭代器,里面唯一的成员变量就是指向这些结点的指针
那么,怎么取到指向的结点的数据呢?运用operator关键字。
在这里插入图片描述

代码示例,如下:

template<class T>
struct list_iterator
{
      typedef list_node<T> Node;
public:
      list_iterator(Node* node)//迭代器的初始化,让迭代器的内部指针指向目标结点
                 :_node(node);
      {}          
      //接下来我们要重载*运算符,数组的迭代器*一下就取到了这个元素,而我们链表的指针*一下只是的到这个结点,要取到结点里面的数据,还得对*这个运算符进行重载。
      T& operator*()
      {
          return _node->_data;//这样就取到了结点里面的数值。
      } 
     
private:
     Node* _node;//迭代器里面有存放着指向结点的指针
};

那++与–呢?
那这其实很明显了,我们只需要让list_iterator里面的_node让它成为指向下一个(或上一个)结点的指针即可。
代码示例,如下:

template<class T>
class list_iterator
{
     typedef list_node<T> Node;
public:
     //++与--都是得到新的迭代器,也就是返回值是新的list_iterator,
     list_iterator& operator++()
     {
         _node = _node->_next;//让自己成为指向下一个结点的指针
         return *this;
     }
     list_iterator& operator--()
     {
        _node = _node->_prev;//让自己成为指向上一个结点的指针
     }
     list_iterator operator++(int)//后置++,返回没++之前地迭代器
     {
        list_iterator tmp(*this);
        _node = _node->_next;
        return tmp;
     }
     list_iterator operator--(int)//后置--
     {
        list_iterator tmp(*this);
        _node = _node->_prev;
        return tmp;
     }
     bool operator!=(list_iterator<T>& it)
     {
        return _node != it._node;
     }
private:
     Node* _node;
};

迭代器的基本规则就到这里,现在我们来搞定begin()以及end(),因为编译器只认识这两个。

4.拼接

template<class T>
class list
      typedef list_node<T> Node;
      typedef list_iterator<T> iterator;
{
public:
      iterator begin()
      {
         return iterator(_head->_next);//按值返回,临时对象会被拷贝进入
      }
      iterator end()
      {
         return iterator(_head); 
      }
private:
      Node* _head;
};

这样,我们就实现了迭代器。

三、测试

int main()
{
   list<int> lt;//定义了一个头节点(_head指向),np指针均指向自己
   lt.push_back(1);
   lt.push_back(2);
   lt.push_back(3);
   lt.push_back(4);
   //尾插了四个结点,分别存放1,2,3,4四个数据。
}

如图:
在这里插入图片描述

int main()
{
   list_iterator<int> it = lt.begin()
   while(it != lt.end())
   {
       cout<<*it<<" ";
       ++it;
   }
}

在这里插入图片描述

四、遇到的一些问题

①重载!=运算符
我的代码是这样的

bool operator!=(list_iterator<T>& it)
{
	return _node != it._node;
}
int main()
{
    while(it!=lt.begin())
    {
       //
    }
}

这里lt.begin()会返回一个临时的迭代器对象,而原参数是list_iterator& it,C++规定:临时值不能绑定到非const的左值引用(因为非const引用很可能会修改临时值,但临时之马上就没了,不能被修改,修改没意义)
而且,函数后面也要加const,因为it或者lt.begin()返回的都有可能是const迭代器,const对象只能调用const成员函数
正确修改如下

bool operator!=(const list_iterator<T>& it)const
{//lt.end()会返回一个临时的迭代器对象,用完就销毁的临时值,非const修饰可能会修改,临时对象无法被修改
	return _node != it._node;
}

②返回临时对象

list_iterator& operator++()
{
    return list_iterator(_node->_next);
}

list_iterator(_head->_next)是临时对象,临时对象的生命周期只在当前语句,函数返回后临时对象会被销毁,此时的引用就变成了悬空引用(引用一个已销毁的对象)

如果返回临时对象的引用,程序运行时会因访问销毁的对象而崩溃(未定义行为)

正确修改如下

list_iterator& operator++()
{
    _node = _node->_next;
    return *this;
}

而当你使用按值返回临时对象时:

typedef list_iterator<T> iterator
iterator begin()
{
   return iterator(_head->_next);
}

按值返回时,临时对象可以说被拷贝到了iterator中,相当于被延长了。

五、总结

本文就容器list进行了剖析,重点拆解了迭代器的封装。望大家看到这里能有自己的感悟。

评论 7
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值