博主的博客主页——>点击这里
博主的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进行了剖析,重点拆解了迭代器的封装。望大家看到这里能有自己的感悟。

580





