文章目录
一、需要实现的三个类模板
通过观察源码,我们能发现,要实现list我们需要设计三个类,主要框架如下:
cpp
namespace mine
{
template<class T>
struct list_node
{
T _data;
list_node<T>* _next;
list_node<T>* _prev;
};
template<class T, class Ref, class Ptr>
struct list_iterator
{
typedef list_node<T> Node;
typedef list_iterator<T, Ref, Ptr> Self;// 引用 指针
Node* _node;
};
template<class T>
class list
{
public:
typedef list_node<T> Node;
private:
Node* _head;
int _size = 0;
};
}
关于list以及list_node 这两个类模板的设计我想我们应该很容易理解,但是我们为什么需要实现迭代器模板?我们之前实现string以及vector时,我们只是使用typedef利用对应类型的指针实现迭代器的功能。其实究其原因还是因为存储机制不同,string以及vector对象中的元素存储在一片连续的内存空间,我们利用指针的变换就可以实现迭代器的功能,但是list的各个节点的地址不是连续的,利用指针变换得到的不会是我们想要的节点,那我们怎么实现对应的功能呢?不要忘记,节点同样存储了指向前一个节点以及后一个节点的指针,我们利用这两个指针就可以实现迭代器的功能,我们后面会根据上述模板逐步实现对应的功能,其实list模板类我们只会选择常用的函数接口
二、list_node类模板的完善
我们只需要写一个带参构造就好了
cpp
template<class T>
struct list_node//成员默认为公有,便于后面的访问
//存在其他方法,可以利用友元或者内部类解决,但是我们观察源码发现是分别封装的,所以我们也分开来写
{
list_node(const T& data=T())//若构造结点时未传入数据,则默认以list容器所存储类型的默认构造函数所构造出来的值为传入数据。
:_data(data),_next(nullptr),_prev(nullptr)
{ }
//成员变量
T _data;
//指针域
list_node<T>* _next;//前驱指针
list_node<T>* _prev;//后继指针
};
三、迭代器模板的完整实现以及思路
我们已经能给出了模板的大致框架,但是我们看到会感到很疑惑?这是什么我怎么看不懂?因为这是我们二次优化后的代码,我们给出这个接口应该就可以看懂了
cpp
template<class T>
struct list_iterator //普通迭代器的实现
{
list_iterator(Node* node)
:_node(node)
{}
typedef list_node<T> Node;
typedef list_iterator<T> Self;
//成员变量
Node* _node;
};
那我们先完善一下这个模板的功能:
我们常使用迭代器进行对象的遍历,那我们就需要实现 如下接口:
cpp
T& operator*();
T* operator->();
Self& operator++();
Self& operator--();
bool operator!=(const Self& s) const;
bool operator==(const Self& s) const;
解引用的重载
对于第一个,我们对迭代器进行解引=引用主要是为了获取迭代器对应节点的数据,那么很容易我们就能实现:
cpp
T& operator*()
{
return _node->_data;
}
对于第二个,可能会存在如下的场景当list容器当中的每个结点存储的不是内置类型,而是自定义类型,例如日期类,那么当我们拿到一个位置的迭代器时,我们可能会使用->运算符访问Date的成员:
cpp
list<Date> lt;
Date d1(2021, 8, 10);
Date d2(1980, 4, 3);
Date d3(1931, 6, 29);
lt.push_back(d1);
lt.push_back(d2);
lt.push_back(d3);
list<Date>::iterator pos = lt.begin();
cout << pos->_year << endl; //输出第一个日期的年份
->运算符的重载
我们直接返回结点当中所存储数据的地址即可。
cpp
T* operator->()
{
return &_node->_data;
}
讲到这里,可能你会觉得不对,按照这种重载方式的话,这里使用迭代器访问日期类当中的成员变量时不是应该用两个->吗?
这里本来是应该有两个->的,第一个箭头是pos ->去调用重载的operator->返回Date* 的指针,第二个箭头是Date* 的指针去访问对象当中的成员变量_year。但是一个地方出现两个箭头,程序的可读性太差了,所以编译器做了特殊识别处理,为了增加程序的可读性,省略了一个箭头。
前置++的重载
前置++原本的作用是将数据自增,然后返回自增后的数据。我们的目的是让结点指针的行为看起来更像普通指针,那么对于结点指针的前置++,我们就应该先让结点指针指向后一个结点,然后再返回"自增"后的结点指针即可。
cpp
Self& operator++()
{
_node = _node->_next;
return *this;
}
前置--的重载
cpp
Self& operator--()
{
_node = _node->_prev;
return *this;
}
!=以及==的重载
cpp
bool operator!=(const Self& s) const
{
return _node != s._node;
}
bool operator==(const Self& s) const
{
return _node == s._node;
}
就是判断两个节点的数据是否相同,应该是很容易理解并实现了。
思考:如果我们要实现const_iterator迭代器应该怎么实现?
cpp
template<class T>
struct list_const_iterator //const迭代器的实现
{
typedef list_node<T> Node;
typedef list_const_iterator<T> Self;
list_iterator(Node* node)
:_node(node)
{}
const T& operator*()
{
return _node->_data;
}
const T* operator->()
{
return &_node->_data;
}
const Self& operator++()
{
_node = _node->_next;
return *this;
}
const Self& operator--()
{
_node = _node->_prev;
return *this;
}
bool operator!=(const Self& s) const
{
return _node != s._node;
}
bool operator==(const Self& s) const
{
return _node == s._node;
}
//成员变量
Node* _node;
};
我们对比一下发现其实就是对部分接口进行了一些修改,利用const进行了修饰,那对于我们的程序来说就会导致有些笼余以及臃肿,那么我们应该怎么实现?想不出来,我们看看源码,那些大佬是怎么实现的,其实就最前面我们给出的接口:
cpp
struct list_iterator
{
typedef list_node<T> Node;
typedef list_iterator<T, Ref, Ptr> Self;// 引用 指针
Node* _node;
};
意思是在该类模板中可以使用三种不同类型的数据类型,这有什么用?能为我们解决问题吗?
我们观察上面实现的代码,可以发现会使用 T T& T*这三种类型,恰恰对应着源码接口的三种类型。
具体功能的实现我们已经在上文中进行了详细的讲解,那下面我们直接给出迭代器最终版本的代码:
cpp
template<class T, class Ref, class Ptr>
struct list_iterator
{
typedef list_node<T> Node;
typedef list_iterator<T, Ref, Ptr> Self;//引用 指针
list_iterator(Node* node)
:_node(node)
{}
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
Self& operator++()//前置++
{
_node = _node->_next;
return *this;
}
Self& operator--()
{
_node = _node->_prev;
return *this;
}
Self operator++(int)//后置++,加int形参
{
Self tmp(*this);
_node = _node->_next;
return tmp;
}
Self& operator--(int)
{
Self tmp(*this);
_node = _node->_prev;
return tmp;
}
bool operator!=(const Self& s) const
{
return _node != s._node;
}
bool operator==(const Self& s) const
{
return _node == s._node;
}
Node* _node;
};
其实本质上我们是将第一种类模板进行了更深层次的细化,得到的结果本质上还是类模板最终都要实例化成对象。
当我们使用普通迭代器时,编译器就会实例化出一个普通迭代器对象;当我们使用const迭代器时,编译器就会实例化出一个const迭代器对象。若该迭代器类不设计三个模板参数,那么就不能很好的区分普通迭代器和const迭代器。
四、list类模板的完善
cpp
class list
{
public:
typedef list_node<T> Node;
typedef list_iterator<T, T&, T*> iterator;
typedef list_iterator<T, const T&, const T*> const_iterator;
private:
Node* _head;
int _size;
};
我们将在此基础上进行功能的完善。
默认成员函数的实现
无参构造
cpp
list()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
_size = 0;
}
我们需要为头结点开辟一个空间,然后构建成双向循环链表,就是让头节点的指向前一个节点以及后一个节点的指针指向自己。
拷贝构造实现
cpp
list(const list<T>& lt)
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
_size = 0;
auto it = lt.begin();
while (it != lt.end())
{
push_back(*it);
it++;
}
}
我们可以想成先利用无参构造创建一个list,然后将母本中的元素尾插进该表中。
赋值运算符重载函数
传统写法:
cpp
list<T>& operator= (const list& lt)
{
if (< != this)
{
clear();
auto it = lt.begin();
while (it != lt.end())
{
push_back(*it);
it++;
}
}
return *this;
}
我们首先要保证母本不是他本身,否则会造成混乱。传统写法我们看代码应该很容易理解,那么我们看看现代写法:
cpp
void swap(list<T>& lt)
{
std::swap(_head, lt._head); //交换两个容器当中的头指针
std::swap(_size, lt._size);
}
list<T>& operator=(list<T> lt) //编译器接收右值的时候自动调用其拷贝构造函数
{
swap(lt); //交换这两个对象
return *this; //支持连续赋值
}
首先利用编译器机制,故意不使用引用接收参数,通过编译器自动调用list的拷贝构造函数构造出来一个list对象,然后调用swap函数将原容器与该list对象进行交换即可。
析构函数
cpp
~list()
{
clear();
delete _head;
}
我们先利用clear函数清楚表中所有的有效节点,只保留头结点,最后利用delete销毁头结点。
迭代器相关函数
cpp
iterator begin()
{
return _head->_next;
}
const_iterator begin() const
{
return _head->_next;
}
iterator end()//最后一个元素之后
{
return _head;
}
const_iterator end() const
{
return _head;
}
其中我们最需要注意的是end返回的是最后一个有效节点的下一个位置,由于编译器会自动调用构造,所以我们直接返回节点是没有问题的。
访问容器相关函数
front和back
front和back函数分别用于获取第一个有效数据和最后一个有效数据,因此,实现front和back函数时,直接返回第一个有效数据和最后一个有效数据的引用即可。
cpp
T& front()
{
assert(_size!=0);//链表不能只有一个哨兵位的头结点
return _head->_next->_data;
}
const T& front() const
{
assert(_size != 0);
return _head->_next->_data;
}
T& back()
{
assert(_size != 0);
return _head->_prev->_data;
}
const T& back() const
{
assert(_size != 0);
return _head->_prev->_data;
}
插入、删除函数
insert可以在指定位置之前插入确值的节点:

cpp
iterator insert(iterator pos, const T& val)
{
Node* newnode = new Node(val);//我们首先需要创建一个新的节点
Node* cur = pos._node;//存储
newnode->_next = cur;
newnode->_prev = cur->_prev;
cur->_prev->_next = newnode;
cur->_prev = newnode;
++_size;
return iterator(newnode);
}
我们首先需要创建一个新的节点,再指明目标位置的节点,进行操作。
erase
erase函数可以删除所给迭代器位置的结点。

先根据所给迭代器得到该位置处的结点指针cur,然后通过cur指针找到前一个位置的结点指针prev,以及后一个位置的结点指针next,紧接着释放cur结点,最后建立prev和next之间的双向关系即可。
cpp
iterator erase(iterator pos)//返回抹除元素的后一个
{
assert(pos != end());
Node* prev = pos._node->_prev;
Node* next = pos._node->_next;
prev->_next = next;
next->_prev = prev;
delete pos._node;
--_size;
return next;
}
push_back和pop_back
push_back和pop_back函数分别用于list的尾插和尾删,在已经实现了insert和erase函数的情况下,我们可以通过复用函数来实现push_back和pop_back函数。
push_back函数就是在头结点前插入结点,而pop_back就是删除头结点的前一个结点。
cpp
void push_back(const T& val)
{
insert(iterator(_head), val);
}
void pop_back()
{
erase(iterator(_head->prev));
}
push_front和pop_front
当然,用于头插和头删的push_front和pop_front函数也可以复用insert和erase函数来实现。
push_front函数就是在第一个有效结点前插入结点,而pop_front就是删除第一个有效结点。
cpp
void push_front(const T& val)
{
insert(begin(), val);
}
void pop_front()
{
erase(begin());
}
其他函数
size
我们为了减少遍历,所以我们直接创建了_size成员变量,当我们需要时,直接通过size()函数返回该成员变量的值。
cpp
size_t size() const
{
return _size;
}
empty
判空函数,我们判断size是否为0即可。
cpp
bool empty() const
{
return _size == 0;
}
clear
clear函数用于清空容器,我们通过遍历的方式,逐个删除结点,只保留头结点即可。
cpp
void clear()
{
if (_size == 0)
return;
auto it = begin();
while (it != end())
{
it=erase(it);
}
}