目录
前言
重要接口实现
框架
默认成员函数
迭代器(重点)
1.引言
2.list迭代器类实现
3.list类中调用实现
增删查改
后记
前言
我们知道,stl中的vector对应数据结构中的顺序表,string类对应字符串,而今天要讲的list类对应带头双向链表,并不是对应单链表,带头双向链表的基本操作在数据结构课程中已经学过,所以今天即将要讲的常见接口并不是重点,重点是list的迭代器的实现。
我们也知道,string、vector的迭代器就是原生指针,如果使用原生指针可以实现list的迭代器吗?答案是不行,因为list的数据并不是一块连续的存储空间,无法像指针那样取访问元素,但是为了保持所有容器迭代器的使用一致,我们如何实现list的迭代器才能像原生指针那样通过++、--去控制,这里就体现出了封装的重要性,快往下看看吧!
重要接口实现
-
框架
通过下方代码可以看出,实现了一个节点类模板存储节点,一个链表类模板存储链表,
①使用类模板是因为存储元素可以自由指定,不是像以前一样通过typedef固定了每个链表的元素类型;
②节点类模板使用struct,而不使用class,是因为struct的默认权限是public,链表类模板可以自由访问其成员变量,而class默认权限是private,当然用class指定public权限也可以,
节点类模板中通过构造函数初始化元素,链表类模板成员是一个节点指针,可以在构造函数中申请一个节点充当头节点。
代码:
template <class T>
struct ListNode
{
//构造函数用来创节点
ListNode(const T& x = T())
:_data(x)
, _prev(nullptr)
, _next(nullptr)
{
}
T _data;
ListNode<T>* _prev;
ListNode<T>* _next;
};
template <class T>
class List
{
typedef ListNode<T> Lnode; //作用:下面写ListNode<T>较麻烦,typedef一下,使用Lnode较方便
public:
//...
private:
Lnode* _head;
};
-
默认成员函数
因为在所有构造函数前都需要先初始化成员变量(为头节点申请空间并将左右指针置空),所以封装了一个函数empty_init在每个构造函数前直接调用,
所有默认成员函数的实现与string、vector中的如出一辙,不太理解的可以参考一下之前的文章,不是重点这里不再赘述。
代码:
//创建并初始化头节点,放于构造函数的最前面
void empty_init()
{
_head = new Lnode();
_head->_next = _head->_prev = _head;
}
//构造函数
List()
{
empty_init();
}
//普通拷贝构造函数
List(const List& L) //注意:类外必须是List<类型名>,类里可以是List,但建议List<T>
{
empty_init();
auto it = L.begin();
while (it != L.end())
{
push_back(*it);
it++;
}
}
void swap(List<T>& L)
{
std::swap(_head, L._head);
}
//传迭代器区间构造函数
template <class InputIterator>
List(InputIterator first, InputIterator last)
{
empty_init();
while (first != last)
{
push_back(*first);
++first;
}
}
//拷贝构造函数
//现代写法
List(const List<T>& L)
{
empty_init();
List<T> tmp(L.begin(), L.end());
swap(tmp);
}
//赋值运算符重载
//现代写法
List<T>& operator=(const List<T>& L)
{
List<T> tmp(L.begin(), L.end());
swap(tmp);
return *this;
}
//更狠的现代写法
List<T>& operator=(List<T> L) //直接传一个拷贝过来,相当于上面的tmp,函数结束自动释放
{
swap(L);
return *this;
}
//清除除了头节点之外所有的节点
void clear()
{
auto it = begin();
while (it != end())
{
it = erase(it);
}
}
//析构函数
~List()
{
clear();
_head->_next = _head->_prev = nullptr;
delete _head;
_head = nullptr;
}
-
迭代器(重点)
1.引言
还记得vector、string的迭代器实现吗?typedef T* iterator; typedef const T* const_iterator; ,只是原生指针对不对,然后typedef一下,就可以使用了,因为指针可以在一块连续的地址空间++或--访问元素,但是链表中的节点指针++、--访问元素可以吗,答案是不可以,但为了保持迭代器使用一致,就应该遇到链表的迭代器使用++、--也可以达到一样的效果,因此就想到了操作符重载,将++、--、* 重载成我们希望达到的效果,而实现操作符重载就必须将其封装成一个类。
从引言中就可以看出list与之前学过的容器迭代器实现的不同之处,list迭代器是通过封装成类实现,但迭代器有两种(暂时先不提反向迭代器),一种普通迭代器,一种const迭代器,两种迭代器的实现应该大致内容都一样,小部分不一样(比如,const迭代器的解引用应该返回const不可类型的变量),那我们应该先写好普通迭代器的实现,再复制粘贴成const迭代器然后修修改改吗?漏!大漏特漏!接触过模板这个概念之后,应该可以想到这里用到模板。
2.list迭代器类实现
1)框架
见下方代码, 实现list的迭代器这个__list_iterator类模板,参数列表中的T、Ref、Ptr分别是数据类型、此类型的引用、此类型的指针(比如,T是int,Ref就是int&,Ptr就是int*)(为什么需要指针参数在操作符->重载处有说明),填入的参数不同就是不同的类,这里list的迭代器需要两个类,一个普通迭代器的类,一个const迭代器的类,在list类实现中去定义即可。
针对list迭代器类模板的实现,成员变量是节点的指针,而构造函数则是传入一个节点指针可初始化一个list迭代器,不需要自己提供析构函数。
代码:
template <class T, class Ref, class Ptr>
struct __list_iterator
{
//注意:这两个typedef只是因为ListNode<T>、__list_iterator<T, Ref, Ptr>很麻烦写,所以简化一下,也方便理解
typedef ListNode<T> Lnode;
typedef __list_iterator<T, Ref, Ptr> iterator;
//构造函数
__list_iterator(Lnode* p)
:_node(p)
{
}
//...
Lnode* _node; //链表中的迭代器表示节点的指针
};
2) 关系运算符重载
判断两个迭代器相不相等即判断作为成员变量的结点指针是不是同一个指针变量,很容易理解,加上const是无论普通list对象还是const list对象都可以调用。
代码:
bool operator==(const iterator& it) const
{
return _node == it._node;
}
bool operator!=(const iterator& it) const
{
return !(*this == it);
}
3)运算符++、--重载
针对list类,迭代器的++就是访问下一个节点的迭代器,--是访问上一个节点的迭代器,再注意好前置与后置的实现即可,不是很难。
代码:
iterator& operator++()
{
_node = _node->_next;
return *this;
}
iterator operator++(int)
{
iterator tmp(_node);
_node = _node->_next;
return tmp;
}
iterator& operator--()
{
_node = _node->_prev;
return *this;
}
iterator operator--(int)
{
iterator tmp(_node);
_node = _node->_prev;
return tmp;
}
4)操作符*重载
list类中的迭代器解引用就是访问此节点的数据,并且返回引用类型,即可操作其中的值,普通迭代器的Ref是普通引用类型,即可读可写,const迭代器的Ref是const引用类型,只可读不可写。
注意:不需要将此成员函数设置成const类型,就像本作者初学时所疑惑的,如果Ref是const T&,不应该对应const成员函数(Ref operator*() const)吗?其实不然,list类的迭代器使用了类模板,参数不同就是不同的类,直接将两种迭代器分开了,如果是普通迭代器,Ref就会传进来T&,调用解引用重载时返回引用,可读可写,如果是const迭代器,Ref就会传进来const T&,调用解引用重载时返回const引用,只可读不可写。
代码:
Ref operator*()
{
return _node->_data;
}
5)操作符->重载
正常情况下,操作符->可以解引用结构体指针再取其成员,那对于底层是节点指针的list迭代器,->就是对迭代器解引用再取其成员,所以针对_data如果是个自定义类型,那么->就可以取其成员,比如,_data是个自定义类型POS,有两个成员,一个x,一个y,那么it->x,it->y表示迭代器取的POS中的x、y,如下图测试,
仔细观察->操作符重载的实现,it->x应该写成it->->x才是对的,因为it->返回自定义类型指针,再->x返回其成员,这里其实编译器做了处理,省略了一个->,提高了可读性。
这里的Ptr就是数据类型的指针变量,将其地址返回,与引用形式一样,需要从类模板参数列表输入。
代码:
Ptr operator->()
{
return &(_node->_data);
}
测试:
3.list类中调用实现
在实现完__list_iterator这个list类迭代器类模板之后,通过在list类中输入不同的模板参数定义不同的类,这里需要普通迭代器类和const迭代器类,如下代码,begin()是返回首元节点的迭代器,而end()是返回最后一个元素节点的下一个位置迭代器,即头节点迭代器。
代码:
typedef __list_iterator<T, T&, T*> iterator; //普通迭代器类
typedef __list_iterator<T, const T&, const T*> const_iterator; //const迭代器类
iterator begin()
{
return iterator(_head->_next);
}
iterator end()
{
return iterator(_head);
}
const_iterator begin() const
{
return const_iterator(_head->_next);
}
const_iterator end() const
{
return const_iterator(_head);
}
-
增删查改
在理解了list的迭代器实现之后,对list的增删改查想必是游刃有余,这里实现一下基本的插入删除操作,结合之前在数据结构中的知识,insert、erase的实现应该可以很快写出,值得注意的是,insert返回新插入节点的迭代器,erase返回所删节点的下一位置的迭代器,而尾插、尾删、头插、头删直接复用即可。
代码:
iterator insert(iterator pos, const T& x)
{
Lnode* newNode = new Lnode(x);
pos._node->_prev->_next = newNode;
newNode->_prev = pos._node->_prev;
newNode->_next = pos._node;
pos._node->_prev = newNode;
return iterator(newNode); //返回插入位置的迭代器
}
//尾插
void push_back(const T& x)
{
insert(end(), x);
}
//头插
void push_front(const T& x)
{
insert(begin(), x);
}
iterator erase(iterator pos)
{
assert(pos != end());
Lnode* tmp = pos._node->_next;
pos._node->_prev->_next = pos._node->_next;
pos._node->_next->_prev = pos._node->_prev;
delete pos._node;
return iterator(tmp);
}
//尾删
void pop_back()
{
erase(--end());
}
//头删
void pop_front()
{
erase(begin());
}
后记
在list类的实现中,默认成员函数、操作符重载以及增删查改已不再是重点,重点是掌握迭代器的实现,因为与string、vector中的迭代器实现不同,也没有那么简单,上面总结的很清楚,不懂的可以私我或者写在评论区,加油,拜拜!