相比于vector简单的连续线性结构,list就稍显复杂了。
list是一个带头双向循环链表,其结构如图所示。
list的使用较为简单,不再赘述。这里直接放出list的使用文档,以供参考。
list使用文档
本文的重点在于通过list迭代器初步了解STL六大组件之一 —— 迭代器的设计理念
学习vector的时候,我们粗略地提过迭代器,当时我们说将其视作一个原生指针来使用(事实也是如此,因为vector迭代器的底层就是一个原生指针)。然而,迭代器不一定是一个指针,也可能是一个类(class type),为什么呢?想搞清楚迭代器的设计思维,要先搞懂迭代器的模式定义。
iterator的模式定义:(引用自《STL源码剖析》)
“提供一种方法,使之能够依序巡访某个聚合物(容器)所含的各个元素,而又无需暴露该聚合物的内部表述方式。”
通俗的理解,迭代器设计的本意就是为了给所有容器提供一种通用的访问方式,让使用者无需关注底层的数据结构。
了解了迭代器的定义,我们现在要着手于迭代器的实现。我们拿list的迭代器来举例子。
list不再能够像vector一样直接用原生指针作为迭代器,因为list的节点不保证在存储空间中连续存在。list迭代器必须有能力指向list的节点,并有能力进行正确的递增、递减、取值、成员访问等操作。所谓“递增、递减、取值、成员访问”操作是指:
- 递增:指向当前节点的下一节点;
- 递减:指向当前节点的上一节点;
- 取值:读取当前节点数据域中的值;
- 成员访问:访问数据域中类对象的成员
很显然,原生指针无法满足这些操作。所以,STL中迭代器的实现再次采用了类封装的思想来解决这一问题,将指针封装成一个迭代器类,并通过运算符重载来控制迭代器的操作,以达到每一种不同的容器都有其特有的迭代器。
假设list有一个迭代器it,那么
++it
就是让it指向当前指向节点的下一节点(递增),--it
让it指向当前指向节点的上一节点(递减),*it
是读取当前指向节点数据域中的值(取值),it->member
是访问数据域中类对象的成员(这个有点特殊,后面模拟实现时详谈)
况且,即使迭代器是一个类,但由于其只有一个指针类型的成员变量,其大小依然是4个字节(32位平台下),所以我们可以将迭代器视为一种大小与指针相同,行为与指针类似的对象。
了解了迭代器的底层构造,接下来我们就可以着手list的模拟实现了!
list的实现需要三个类:
list的节点和list本身的结构是不一样的,因此要分开实现。list节点的结构图如下:
代码实现
// list的节点
template <class T>
struct _list_node
{
typedef _list_node<T>* pointer;
pointer prev; // 指向前一个节点
pointer next; // 指向下一个节点
T data;// 数据域
_list_node(const T& val = T()) // 提供一个全缺省构造函数
:data(val)
{}
};
基于2.2中的分析,我们来实现list的迭代器
⭕初步实现
// list的迭代器
template <class T>
class ListIterator
{
typedef _list_node<T>* pNode;
typedef ListIterator<T> Self;
public:
pNode _pNode; //迭代器内当然要有一个指针,指向list的节点。这也是迭代器唯一的成员变量
public:
ListIterator(pNode p = nullptr) // 用一个节点的指针构造迭代器
:_pNode(p)
{}
T& operator*() // 取值
{
return _pNode->data;
}
T* operator->() // 成员访问
{
return &_pNode->data;
}
// 递增
Self operator++() // 前置++
{
_pNode = _pNode->next;
return *this;
}
Self operator++(int) // 后置++
{
Self tmp(_pNode);
_pNode = _pNode->next;
return tmp;
}
// 递减
Self operator--() // 前置--
{
_pNode = _pNode->prev;
return *this;
}
Self operator--(int) // 后置--
{
Self tmp(_pNode);
_pNode = _pNode->prev;
return tmp;
}
bool operator==(const Self& it)
{
return _pNode == it._pNode;
}
bool operator!=(const Self& it)
{
return _pNode != it._pNode;
}
};
❓上面的代码乍一看似乎没什么问题,其实还不够完整。
我们知道,迭代器是一种行为类似指针的对象,而指针的各种行为中最常见也最重要的两种行为就是内容提领和成员访问了,因此,迭代器中最重要的就是对
operator
和operator->
进行重载工作。而上面的代码问题就出在这两个最重要的地方。怎么回事呢?我们一步一步分析。
阅读文档我们知道,list不仅有iterator类型迭代器,还有const_iterator类型的迭代器,该类型迭代器的特性是其指向对象受const保护,无法修改。
const_iterator是区别于iterator的另一种类型的迭代器,而不是简单在iterator前加上cosnt。
⭕区分
// 下面使用的是std中的list
list<int> lt(10, 1);
const list<int>::iterator it1 = lt.begin(); // 保护的是it1本身
//it1 = lt.end(); //错误,it1被const保护了,无法改变
list<int>::const_iterator it2 = lt.begin(); // 保护的是it2指向的对象
//*it2 = 2; //错误,it2指向的对象被const保护了,无法改变
由于const_iterator与iterator特性的不同,其实现方式也有所不同。上面我们实现的迭代器实际上是iterator类型的,根据const_iterator的特性,不难发现,只需在iterator的这两个地方做出调整即可变为const_iterator。
const T& operator*() // T& -> const T& // const保护*提取的返回值
{
return _pNode->data;
}
const T* operator->() // T* -> const T* // const保护->访问的值
{
return &_pNode->data;
}
那么,要重载一个迭代器类以实现const_iterator吗?不,这样的代码太过冗余,因为const_iterator与iterator的区别微乎其微。实现STL的大佬们可不会这么干。由于二者的区别只有在两个成员函数的返回值类型,因此,可以增加两个模板参数,根据传入的模板参数确定适应的返回值类型。
list迭代器最终代码
// list的迭代器类模板
template <class T, class Ref, class Ptr>
class ListIterator
{
typedef _list_node<T>* pNode;
typedef ListIterator<T, Ref, Ptr> Self;
public:
pNode _pNode;
public:
ListIterator(pNode p = nullptr)
:_pNode(p)
{}
Ref operator*()
{
return _pNode->data;
}
Ptr operator->() //返回节点数据域的指针
{
return &_pNode->data;
}
Self operator++() // 前置++
{
_pNode = _pNode->next;
return *this;
}
Self operator++(int) // 后置++
{
Self tmp(_pNode);
_pNode = _pNode->next;
return tmp;
}
Self operator--() // 前置--
{
_pNode = _pNode->prev;
return *this;
}
Self operator--(int) // 后置--
{
Self tmp(_pNode);
_pNode = _pNode->prev;
return tmp;
}
bool operator==(const Self& it)
{
return _pNode == it._pNode;
}
bool operator!=(const Self& it)
{
return _pNode != it._pNode;
}
};
这样一来,只需在list类中加入这两个typedef,即可利用同一类模板实现const_iterator与iterator两个不同的类。
typedef ListIterator<T, T&, T*> iterator;
typedef ListIterator<T, const T&, const T*> const_iterator;
实现了list的节点和list的迭代器后,list类就很容易实现了,无非就是各种结构的耦合和增删查改。根据list的结构特性,我们可以先实现insert和erase成员函数,其它成员函数只需复合使用即可,大大提高代码的复合性。废话不多说,直接上代码。
// 链表
template <class T>
class list
{
typedef _list_node<T> Node;
typedef Node* pNode;
// 迭代器
typedef ListIterator<T, T&, T*> iterator;
typedef ListIterator<T, const T&, const T*> const_iterator;
private:
// 创建一个空链表,即头结点next和prev都指向自己
void empty_initialize()
{
_head = new Node;
_head->next = _head;
_head->prev = _head;
}
pNode _head; // 指向链表的头结点
long _size;// 记录链表的长度,解决了size()成员函数O(N)时间复杂度的问题
public:
// 迭代器
// begin指向头节点的下一位置,end指向头节点
iterator begin()
{
return _head->next;
}
const_iterator begin() const
{
return _head->next;
}
iterator end()
{
return _head;
}
const_iterator end() const
{
return _head;
}
// 构造函数
// 1.无参构造函数
list()
{
empty_initialize();
}
// 2.fill
list(size_t n, const T& val = T())
{
empty_initialize();
while (n--)
{
push_back(val);
}
}
list(int n, const T& val = T())
{
empty_initialize();
while (n--)
{
push_back(val);
}
}
// 3.拷贝构造
//传统写法
//list(const list& lt)
//{
// empty_initialize();
// for (const auto& e : lt) // 范围for的底层是迭代器
// {
// push_back(e);
// }
//}
//现代写法
list(const list<T>& lt)
{
empty_initialize(); // 防止交换后析构野指针
list<T> tmp(lt.begin(), lt.end());
swap(tmp);
}
// 4.range
template <class InputIterator>
list(InputIterator first, InputIterator last)
{
empty_initialize();
while (first != last)
{
push_back(*(first++));
}
}
//"="
list<T> operator=(list<T> lt)
{
swap(lt);
return *this;
}
// Destructor
~list()
{
//clear()是释放除头节点之外的所有节点,析构是释放所有节点,这里复合使用即可
clear();
delete _head;
_head = nullptr;
}
size_t size() const
{
return _size;
}
bool empty() const
{
return _head->next == _head;
}
// Modifiers
void clear()
{
while (!empty())
{
pop_back();
}
}
void swap(list<T>& lt) // 交换两个list的_head指向和_size即可
{
std::swap(_head, lt._head);
std::swap(_size, lt._size);
}
// 重点
iterator insert(iterator pos, const T& val = T())
{
// 开新节点
pNode newNode = new Node(val);
// 链接
pNode pPrev = pos._pNode->prev;
pNode pNext = pos._pNode;
pPrev->next = newNode;
newNode->prev = pPrev;
newNode->next = pNext;
pNext->prev = newNode;
++_size;
return iterator(newNode);
}
iterator erase(iterator pos)
{
assert(pos != end());
pNode pPrev = pos._pNode->prev;
pNode pNext = pos._pNode->next;
delete pos._pNode;
pPrev->next = pNext;
pNext->prev = pPrev;
--_size;
return iterator(pNext);
}
void push_back(const T& val)
{
insert(end(), val);
}
void pop_back()
{
erase(--end());
}
void push_front(const T& val)
{
insert(begin(), val);
}
void pop_front()
{
erase(begin());
}
};
list和vector是我们最常用的两个容器,它们各有优缺点,相辅相成。由于两个容器的底层结构不同,导致其特性以及应用场景不同,其主要不同如下:
补充一点,在空间利用率方面,vector的扩容机制往往会导致空间浪费,而list则不存在这个问题,因为它是按需开辟空间的,对空间的利用十分精准。