文章目录
前言
不知不觉已经二月中旬了,但是这个疫情依然没有结束。这一个月我的感受十分强烈,原来自由是那么珍贵,以为生活在非战争年代,被憋在家里日子永远不会出现。但是这个疫情让我知道了,自由也是相对的,是有人牺牲了自己的自由才换来的我们自由。这个人就是许许多多工作在平凡岗位的工作者们。
正好在家没有什么事情,就把珍藏在B站上的侯捷老师STL课程拿出来看了看。看视频还是很好理解的,但是能够深入了解其中的原理还是有所困难。俗话说:检验自己最好的方法就是教会别人。所以就有这一篇博客,今天主要是关于list的原码,这个原码是网上找到的,我在文末也会将这个原码附在最后。
list有什么特点
在C语言中我们就学过数组,但是这个数组只能将我们已知的同种类的元素放到一起。但是这个数组存在很多问题,比如出现访问越界的时候就会报错,比如空间一次性分配不能再改变等等问题。渐渐的我们也会学到数据结构,这个里面就讲到了链表的优点。但是这个链表的优点很突出突出,但是缺点也同样致命。它适合与经常插入删除的数据结构。同样的list也具有这样的功能。
插入的过程只需要O(1)的时间复杂度,这是十分有好的。
而且过程也比较容易理解
q->next = p->next;
q->prior = p;
p->next->prior = q;
p->next = q;
list有些什么功能
前面我们知道了list有什么特点,现在我们就应该知道list有一些什么功能,也叫做有一些什么函数。对于一个容器最核心的就是如何向里面存入数据,那么就有了push_back(),push_front(),insert
。存元素知道了,那么就应该是取元素了,但是这个取元素又分为出容器和读取元素,其中出容器有两个和如元素对对应的pop_back(),pop_front(),erase()
,读取元素就有front(),back()
。
这上面这几个功能主要是用于程序员进行操作,但是容器和算法结合是就不是这样几个函数了。因为算法希望得到迭代器,而这个迭代器通过萃取器可以得到算法需要的内容。那么关于迭代器就有begin(),end()
,还有两个就是细枝末节的函数了size(),empty()
list的结构
下面画一个图来表示list内部的结构。
对于这里面的每一个部分我都会分开进行解释。首先是链表的结点Node:
结点Node
struct Node
{
Object data;
struct Node *prev;
struct Node *next;
/*节点构造函数*/
Node(const Object &d = Object(), Node *p = NULL, Node *n = NULL):data(d), prev(p), next(n){}
};
这个里面的struct和C语言的struct是相同的,但是他又是和class类似的。这是C++为了和C语言兼容,刻意将struct定义为和结构体一样,但是和C语言结构体有不完全相同,里面的所有元素默认为是public,而class里面的元素默认为private。
迭代器iterator
在STL中迭代器的作用十分重要,他是将算法和容器连接起来的一个中间件。这一个中间件可以在不同的容器中表现形式个不相同,有的容器当中迭代器是指针,有的又是类的对象。所以需要一个萃取器实现不同的输入得到需要的输出。
在迭代器中需要重载几个符号:*,++,--,==,!=
这里面比较重要的是++和–,因为存在前缀和后缀两种情况。在C++中不允许出现i++++
这种情况所以我们需要对后缀进行一个特殊处理。
const_iterator & operator++ ()
{
current = current->next;
return *this;
}
const_iterator operator++(int)
{
const_iterator old = *this;
++(*this);
return old;
}
上面两个代码中,前面那个是前缀++,后面那个是后缀++。看一下这个后缀++和前缀++返回值的地方。前缀++返回的是一个引用,说明可以继续往下执行,是一个左值。但是后缀++返回的是一个值,是一个右值,右值是不允许进行操作的。这个时候系统会报错。
大家可能会疑问我们对于操作符的重载应该达到什么样的效果,大家首相想一想为什么我们要进行操作符的重载。当然是一般的操作符不能实现特殊的功能,那么最一般的数据类型是int。所以我们就是要向着int类型的操作符进行模拟,这就是我们操作符重载的目的了。
下面我就开始针对每一个函数进行单独讲解。
list函数
构造函数
List() { init(); }
void init() {
theSize = 0;
head = new Node;
tail = new Node;
head->next = tail;
tail->prev = head;
}
在这里里面我们创造两个结点,分别作为前部哨兵和后部哨兵。就如下图
析构函数
~List() {
clear();
delete head;
delete tail;
}
void clear() {
while( !empty())
pop_front();
}
这个函数相对比较简单,调用clear函数循环遍历将每个元素删除,最后将两个哨兵结点删除就可以了。
empty()&size()
bool empty()const
{
return size() == 0;
}
int size()const
{
return theSize;
}
这两个函数比较好理解都是直接返回相应的值
front()
Object & front()
{
/*采用了迭代器begin()*/
return *begin();
}
//将该函数声明为const类型
const Object &front()const
{
return *begin();
}
这两个重载函数就是返回list头元素的引用
back()
Object &back()
{
/*end()指向最后一个对象的下一个地址,因此需要--*/
return *--end();
}
const Object &back()const
{
return *--end();
}
这函数需要注意的地方在于由于STL的前闭后开规则导致end指向的是最后一个元素的下一个元素。所以返回的时候需要–,这里end()
函数返回的是一个引用所以可以--
,而且返回值是一个副本,不会影响本身的指针指向。
push_back()
void push_back(const Object &x) {
insert(end(), x);
}
iterator insert(iterator itr, const Object &x)
{
Node *p = itr.current;
theSize ++;
return iterator(p->prev=p->prev->next= new Node(x,p->prev, p));
}
这个需要注意的地方在于这个insert()
函数插入的位置是在当前位置的下一个位置。需要注意的是这一个语句return iterator(p->prev=p->prev->next= new Node(x,p->prev, p));
这一句实现和插入,我们来进行分部操作。
首先因为赋值运算符=是右结合的,所以从右向左进行运算。new Node(x,p->prev, p));
这个是调用Node的构造函数,如果不清楚翻看前面的内容。
这个就是这个构造函数所生成的结构。然后再将左右两个结点指向他即可。注意这个这个地方为什么采用前插法?
push_front()
void push_front(const Object &x) {
insert(begin(), x);
}
这里的insert
依然调用前面的push_back()
的。现在我们来回答前面的问题:因为我们在插入的过程中,是在end和head之前,这样我们就可以实现代码风格的统一性。而且因为我们在前面放置了哨兵这样我们还避免了对 head指针的移动。
pop_front()
void pop_front() {
erase(begin());
}
iterator erase(iterator itr) {
Node *p = itr.current;
iterator retVal(p->next);
p->prev->next = p->next;
p->next->prev = p->prev;
delete p;
theSize --;
return retVal;
}
这个就是删除头节点元素,下面我们来进行模拟。
pop_back()
void pop_back()
{
erase(--end());
}
pop_back()
相比于front
来说区别在于删除的元素并不是end()
,而是end()
的前一个元素。
存储图示
如图所示,红色结点就是list内部存储多增加两个哨兵结点,黄色结点是实际存储数据的数据结点。所以在后面的源代码中可以看到begin()
和end()
两个函数并不是返回head
和rail
,而是返回的head->next
和tail->prior
源代码
#ifndef __MYLIST_H_H_
#define __MYLIST_H_H_
#include<iostream>
namespace myspace
{
template<class Object>
class List
{
private:
/*封装对象,形成链表节点*/
struct Node
{
Object data;
struct Node *prev;
struct Node *next;
/*节点构造函数*/
Node(const Object &d = Object(), Node *p = NULL, Node *n = NULL):data(d), prev(p), next(n){}
};
public:
/*创建一个常量迭代器类,这是容器设计的关键*/
class const_iterator
{
public:
const_iterator():current(NULL)
{}
/*获取当前迭代器的值*/
const Object & operator*()const
{
return retrieve();
}
/*重载前向++操作符*/
const_iterator & operator++ ()
{
current = current->next;
return *this;
}
/*重载后向++操作符,因为是一个局部对象不能返回引用*/
//C++不允许出现i++++
const_iterator operator++(int)
{
//暂时保存当前的的地址
const_iterator old = *this;
++(*this);
return old;
}
/*判断迭代器是否相同,实质上就是判断指向的节点是否相同*/
bool operator==(const const_iterator &rhs) const
{
return current == rhs.current;
}
/*调用==操作符*/
bool operator!=(const const_iterator &rhs)const
{
return (!(*this == rhs));
}
/*迭代器实质就是一个节点指针*/
Node *current;
protected:
/*获得链表中的内容*/
Object & retrieve() const
{
return current->data;
}
/*基于指针参数的迭代器构造函数,保证只有List使用*/
const_iterator(Node *p):current (p)
{}
/*友元类,可以调用迭代器的私有成员*/
friend class List<Object>;
};
/*一般迭代器,直接继承const_iterator*/
class iterator : public const_iterator
{
public:
iterator():const_iterator()
{}
/*得到对象的值*/
Object &operator*()
{
return *(iterator::current);
}
/*基于const的重载*/
const Object& operator*()const
{
return const_iterator::operator*();
}
/*前向++操作符*/
iterator &operator++()
{
const_iterator::current = const_iterator::current->next;
return *this;
}
/*后向++操作符*/
iterator operator++(int)
{
iterator *old = *this;
++(*this);
return old;
}
protected:
/*基于节点的迭代器构造函数*/
iterator(Node *p):const_iterator(p)
{}
friend class List<Object>;
};
public:
List()
{
init();
}
~List()
{
clear();
delete head;
delete tail;
}
List(const List &rhs)//拷贝构造函数
{
/*创建哨兵节点*/
init();
/*复制数据*/
*this = rhs;
}
const List & operator=(const List &rhs)//拷贝赋值函数
{
if(this == &rhs)
return *this;
/*清除原有的信息*/
clear();
/*添加新的对象*/
for(const_iterator itr = rhs.begin(); itr != rhs.end(); ++ itr)
push_back(*itr);
return *this;
}
/*得到迭代器,实质上就是得到节点指针*/
iterator begin()
{
/*iterator()是构造函数*/
return iterator(head->next);
}
const_iterator begin()const
{
return const_iterator(head->next);
}
iterator end()
{
return iterator(tail);
}
const_iterator end()const
{
return const_iterator(tail);
}
int size()const
{
return theSize;
}
bool empty()const
{
return size() == 0;
}
void clear()
{
while( !empty())
pop_front();
}
/*得到第一个元素*/
Object & front()
{
/*采用了迭代器begin()*/
return *begin();
}
//将该函数声明为const类型
const Object &front()const
{
return *begin();
}
Object &back()
{
/*end()指向最后一个对象的下一个地址,因此需要--*/
return *--end();
}
const Object &back()const
{
return *--end();
}
/***********************************************
*从头插入新的节点,这时候的begin已经不再是begin
*因此插入操作会导致迭代器出错
***********************************************/
void push_front(const Object &x)
{
insert(begin(), x);
}
/*从后插入新的节点,这时候会将end后移*/
void push_back(const Object &x)
{
insert(end(), x);
}
/*从头弹出一个对象*/
void pop_front()
{
erase(begin());
}
void pop_back()
{
erase(--end());
}
/*插入对象,参数是迭代器和数据*/
iterator insert(iterator itr, const Object &x)
{
/*得到当前迭代器的指针*/
Node *p = itr.current;
theSize ++;
return iterator(p->prev=p->prev->next= new Node(x,p->prev, p));
}
/*删除迭代器处的对象,因此删除也会导致迭代器破坏*/
//是删除的itr的下一个元素
iterator erase(iterator itr)
{
Node *p = itr.current;
iterator retVal(p->next);
p->prev->next = p->next;
p->next->prev = p->prev;
delete p;
theSize --;
return retVal;
}
/*删除迭代器指向的对象*/
iterator erase(iterator start, iterator end)
{
/*for中不使用++itr的原因是erase之后
*就是下一个迭代器,因此不需要++操作*/
iterator itr;
for(itr = start; itr != end; )
{
/*该操作会导致迭代器更新到下一个*/
itr = erase(itr);
}
return itr;
}
private:
/*链表中的数据成员*/
int theSize;
Node *head;
Node *tail;
/*初始化函数*/
void init()
{
theSize = 0;
head = new Node;
tail = new Node;
head->next = tail;
tail->prev = head;
}
};
}
#endif
后记
虽然自己完整写出这部分代码还是有一定的难度,但是通过阅读别人的代码,了解容器内部的构造也能够实现技术的提升。大家一起共勉!中国加油!武汉加油!