💖作者:小树苗渴望变成参天大树🎈
🎉作者宣言:认真写好每一篇博客💤
🎊作者gitee:gitee✨
💞作者专栏:C语言,数据结构初阶,Linux,C++ 动态规划算法🎄
如 果 你 喜 欢 作 者 的 文 章 ,就 给 作 者 点 点 关 注 吧!
文章目录
- 前言
- 一、源码介绍
- 二、模拟实现
- 2.1list的基本框架
- 2.2 迭代器的定义
- 2.3迭代器函数
- 2.4insert和erase函数
- 2.5 头插头删和尾插尾删函数
- 2.6构造函数
- 2.7resize函数
- 2.8size和clear函数
- 2.9赋值运算符和swap函数
- 三、代码汇总
- 四、总结
前言
今天我们开始来介绍一下list的具体实现,他的难点就在迭代器的实现上,其余的对大家来说应该都是小菜一碟,因为我们不是使用原生指针,为什么要使用原生指针??
第一、它是一个内置类型(int*等)这个类型里面i你没有其他属性要描述的。
第二、它所在的结构地址是连续的,这样就不需要对++等其他运算符进行重载。
接下来我们一起来看看list具体是怎么实现的吧。
一、源码介绍
我们通过vector的模拟实现,已经大致了解看文档的顺序,我们先来看看这个list的成员变量
我们的list是一个双向带头的循环链表,所以我们将结点独立包装起来,这样方便申请结点的空间, 因为是带头的所以再构造函数里面肯定要先申请一个头结点
接下来我们来看看迭代器是怎么实现的:
按照以往我们喜欢定义一个原生指针,这是优化结构上的一些特殊性和类型本身的特性让它使用原生指针不会出错,再list中,我们的结点类型是link_type,这是一个结点的指针类型,不知道的同学可能类似于这样定义迭代器:typedef link_type iterator,这样显然是不行的,因为我们就没有办法对++等其他运算符进行重载,来重新定义其含义,所以我们要按照库里面的一样使用一个结构体将其封装起来,我们的结构体也就只有一个link_type类型的属性,这样才能对重载其他运算符,至于其他的一个模板参数啊,这个到时候再说,大致已经有了大致的了解的吧。
我们的list看这些结构大致就够了,其他的函数功能实现都是很简单的。我们来看具体模拟实现
二、模拟实现
再模拟实现之前我们要自定义一个命名空间域,防止和库里面的名字发生冲突,我们按照库里面一样,创建一个结构体类型的结点出来:
namespace xdh
{
template<class T>//定义一个规定节点的数据类型的模板
struct list_Node//将节点封装起来
{
list_Node(const T& val = T())//对结点进行初始化。因为结点都是new出来的,还没有连接关系
:_next(nullptr)
, _prev(nullptr)
, _val(val)
{}
//节点的各个属性
list_Node<T>* _prev ;//指向前一个节点
list_Node<T>* _next;//指向后一个节点
T _val; //此节点的值
};
}
2.1list的基本框架
我们要定义list,里面有属性和构造方法:
template<class T>
class list
{
typedef list_Node<T> Node;//将结点的类型进行重命名,我再这里没有重命名为指针类型,那么再属性声明的时候就要至于指针
public:
list()//默认构造器,相当于创建了一个头结点
{
createHead();
}
private:
void createHead()
{
_head = new Node();
_head->_prev = _head;
_head->_next = _head;
}
private:
Node* _head=nullptr;//定义一个带头的双向链表,给一个缺省值就不需要初始化列表了
size_t _size = 0;//统计list里面的结点个数,这个做了一下优化
};
}
此时的链表结构是这样的:
2.2 迭代器的定义
通过上面的分析,我们需要将结点封装起来。
template<class T>
struct list_iterator//将结点指针封装起来,含义还是结点指针
{
typedef list_Node<T> Node;
typedef list_iterator<T> Self;
Node* _node;//结点指针,不能直接写成typedef Node* iterator;
list_iterator(Node* node=nullptr)//给结点初始化
:_node(node)
{}
T& operator*()//重载解引用运算符
{
return _node->_val;//访问结点里面的数据
}
Self& operator++()//实现了对++运算符的重载
{
_node= _node->_next;
return *this;
}
Self& operator--()
{
_node = _node->_prev;
return *this;
}
Self operator++(int)
{
list_iterator<T> tmp(*this);//先将此结点的迭代器保留
_node = _node->_next;
return tmp;
}
Self operator--(int)
{
list_iterator<T> tmp(*this);
_node = _node->_prev;
return tmp;
}
bool operator!=(const Self&it)const
{
return _node != it._node;
}
bool operator==(const Self& it)const
{
return _node == it._node;
}
};
通过上面的代码,我们发现我们即通过了解引用获取结点里面的数据,也重载了++等其他运算符,为了可以更好找到下一个结点的指针,也可以判断结束条件,我们再list面只需要重命名为**typedef list_iterator iterator;**就可以了
const 对象的迭代器我们怎么去定义呢??
相信大部分和博主第一次一样,毫不犹豫的这样去定义
typedef const list_iterator<T,T,T*> const_iterator;
但是往往不是这样的,大家可以回想一下const int * p 和int * const p
,我们家const的目的到底是为了什么,是不是就不希望结点里面的内容被修改,而我们上面的这种定义方式是将迭代器定义为const了,而不是把迭代器指向的内容变成了const,所以我们要对指向的内容加const,那我们应该怎么做呢??
想对指向的类型进行const修饰,只需要再返回的时候加一个const就行了,那么就不能改变其内容了。但是我们不可能使用两份相似度这么搞的代码,这样太冗余了,写实可以,但是不太好,所以我们定义了第二个模板参数,来控制这个
我们的结点访问数据的形式还有一种方法就是->,那我们应该怎么去重载呢??
知识回顾:
我们发现结构体类型只能通过点来访问内容,而结构体指针可以通过箭头来访问内容,既然这样,我们取得数据的地址将其返回不就行了??也是再迭代器里面去重载
T* operator->()//重载箭头运算符,使用的时候会优化,本来是(it->)->_val,优化成it->_val
{
return &(_node->_val);
//return &(operator*());
}
我们来看一下:
可以更好的解释了上面代码里面注释的那句话了。
但是我们的箭头也是访问数据,也有const所以我们还需要再传一个模板参数过去,来控制箭头的返回值
迭代器的最终实现为:
template<class T,class Ref,class Ptr>//第二个模板参数是为了控制返回的数据是否是const类型的
//第三个模板参数是为了控制返回的数据的指针是否是const类型
struct list_iterator//将结点指针封装起来
{
typedef list_Node<T> Node;
typedef list_iterator<T,Ref,Ptr> Self;
Node* _node;//结点指针,不能直接写成typedef Node* iterator;
list_iterator(Node* node=nullptr)//给结点初始化
:_node(node)
{}
Ref& operator*()//重载解引用运算符
{
return _node->_val;
}
Ptr operator->()//重载箭头运算符,使用的时候会优化,本来是(it->)->_val,优化成it->_val
{
return &(_node->_val);
}
Self& operator++()
{
_node= _node->_next;
return *this;
}
Self& operator--()
{
_node = _node->_prev;
return *this;
}
Self operator++(int)
{
list_iterator<T> tmp(*this);
_node = _node->_next;
return tmp;
}
Self operator--(int)
{
list_iterator<T> tmp(*this);
_node = _node->_prev;
return tmp;
}
bool operator!=(const Self&it)const
{
return _node != it._node;
}
bool operator==(const Self& it)const
{
return _node == it._node;
}
};
上面的实现过后,其他的就简单了
2.3迭代器函数
说明一下:我们的头结点是end的位置,头结点的下一个是begin的位置
iterator begin()
{
return _head->_next;//单参数的隐式类型转换
//return iterator(_head->_next);
}
iterator end()
{
return _head;
//return iterator(_head);
}
const_iterator begin()const
{
return _head->_next;
//return const_iterator(_head->_next);
}
const_iterator end()const
{
return _head;
//return const_iterator(_head);
}
2.4insert和erase函数
iterator insert(iterator pos, const T& val)
{
Node* newnode = new Node(val);
Node* tail = pos._node->_prev;
newnode->_next = pos._node;
pos._node->_prev = newnode;
tail->_next = newnode;
newnode->_prev = tail;
++_size;
return newnode;
//return iterator(newnode);
}
iterator erase(iterator pos)
{
assert(pos != end());//end位置就是头节点的位置,所以不能被删除
Node* del = pos._node;//
Node* ret = del->_next;//保留被删除结点的下一个位置的结点,作为返回值,防止迭代器失效用的
del->_prev->_next = del->_next;
del->_next->_prev = del->_prev;
delete del;
--_size;
return ret;
//return iterator(ret);
}
这都是我们链表相关的知识,我就不做过多的介绍了。
2.5 头插头删和尾插尾删函数
都是复用insert和erase:
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());
}
2.6构造函数
list()//默认构造器
{
createHead();
}
list(size_t n, const T& val = T())
{
createHead();//头结点每次都要初始化的
for (size_t i = 0; i < n; i++)
{
push_back(val);
}
}
list(const list<T>& lt)//拷贝构造
{
createHead();
for (auto& e : lt)
{
push_back(e);
}
}
2.7resize函数
改变有效字符个数的。
void resize(size_t n, const T& val = T())
{
if (n < _size)//有效字符小于本身的大效
{
int ret = _size-n;//记录多出来多少,方便删除
for (int i = 0; i <ret; i++)
{
erase(--end());
}
}
else
{
int ret = n - _size;//记录多出来多少,方便插入
for (int i = 0; i < ret; i++)
{
push_back(val);
}
}
}
2.8size和clear函数
这两个函数还是比较简单的,要是没有一开始统计个数的_size,那么计算个数就要遍历链表了。
size_t size()const
{
return _size;
}
void clear()
{
int n = _size;
for (int i = 0;i < n; i++)//就相当于把有效字符都删除
{
erase(begin());
}
}
2.9赋值运算符和swap函数
//============swap函数==============
void swap(list<T>& lt)
{
std::swap(_head, lt._head);
std::swap(_size, lt._size);
}
//=============赋值运算符重载===========
list<T>& operator=(list<T> lt)
{
swap(lt);//这个大家再vector那一块应该非常的熟悉了
return *this;
}
到这里我们的list的模拟实现就结束了,就是迭代器那一块比较难理解其余的都挺简单的
三、代码汇总
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
namespace xdh
{
template<class T>//定义一个规定节点的数据类型的模板
struct list_Node//将节点封装起来
{
list_Node(const T& val = T())
:_next(nullptr)
, _prev(nullptr)
, _val(val)
{}
//节点的各个属性
list_Node<T>* _prev ;//指向前一个节点
list_Node<T>* _next;//指向后一个节点
T _val; //此节点的值
};
template<class T,class Ref,class Ptr>//第二个模板参数是为了控制返回的数据是否是const类型的
//第三个模板参数是为了控制返回的数据的指针是否是const类型
struct list_iterator//将结点指针封装起来
{
typedef list_Node<T> Node;
typedef list_iterator<T,Ref,Ptr> Self;
Node* _node;//结点指针,不能直接写成typedef Node* iterator;
list_iterator(Node* node=nullptr)//给结点初始化
:_node(node)
{}
Ref& operator*()//重载解引用运算符
{
return _node->_val;
}
Ptr operator->()//重载箭头运算符,使用的时候会优化,本来是(it->)->_val,优化成it->_val
{
return &(_node->_val);
}
Self& operator++()
{
_node= _node->_next;
return *this;
}
Self& operator--()
{
_node = _node->_prev;
return *this;
}
Self operator++(int)
{
list_iterator<T> tmp(*this);
_node = _node->_next;
return tmp;
}
Self operator--(int)
{
list_iterator<T> tmp(*this);
_node = _node->_prev;
return tmp;
}
bool operator!=(const Self&it)const
{
return _node != it._node;
}
bool operator==(const Self& it)const
{
return _node == it._node;
}
};
template<class T>
class list
{
typedef list_Node<T> Node;//将节点进行重命名
public:
typedef list_iterator<T,T,T*> iterator;//迭代器要对外暴露,所以放在public里面
typedef list_iterator<T,const T,const T*> const_iterator;//const修饰的不是迭代器本身,而是迭代器指向的那个内容不能被修改
list()//默认构造器
{
createHead();
}
list(size_t n, const T& val = T())
{
createHead();
for (size_t i = 0; i < n; i++)
{
push_back(val);
}
}
list(const list<T>& lt)//拷贝构造
{
createHead();
for (auto& e : lt)
{
push_back(e);
}
}
//============swap函数==============
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;
}
size_t size()const
{
return _size;
}
void resize(size_t n, const T& val = T())
{
if (n < _size)//有效字符小于本身的大效
{
int ret = _size-n;//记录多出来多少,方便删除
for (int i = 0; i <ret; i++)
{
erase(--end());
}
}
else
{
int ret = n - _size;//记录多出来多少,方便插入
for (int i = 0; i < ret; i++)
{
push_back(val);
}
}
}
void clear()
{
int n = _size;
for (int i = 0;i < n; i++)//就相当于把有效字符都删除
{
erase(begin());
}
}
//==============迭代器==================
iterator begin()
{
return _head->_next;//单参数的隐式类型转换
//return iterator(_head->_next);
}
iterator end()
{
return _head;
//return iterator(_head);
}
const_iterator begin()const
{
return _head->_next;
//return const_iterator(_head->_next);
}
const_iterator end()const
{
return _head;
//return const_iterator(_head);
}
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());
}
iterator insert(iterator pos, const T& val)
{
Node* newnode = new Node(val);
Node* tail = pos._node->_prev;
newnode->_next = pos._node;
pos._node->_prev = newnode;
tail->_next = newnode;
newnode->_prev = tail;
++_size;
return newnode;
//return iterator(newnode);
}
iterator erase(iterator pos)
{
assert(pos != end());//end位置就是头节点的位置,所以不能被删除
Node* del = pos._node;//
Node* ret = del->_next;//保留被删除结点的下一个位置的结点,作为返回值,防止迭代器失效用的
del->_prev->_next = del->_next;
del->_next->_prev = del->_prev;
delete del;
--_size;
return ret;
//return iterator(ret);
}
private:
void createHead()
{
_head = new Node();
_head->_prev = _head;
_head->_next = _head;
}
private:
Node* _head=nullptr;//定义一个带头的双向链表
size_t _size = 0;//统计list里面的结点个数
};
}
四、总结
大家这届一定要好好理解,因为像线性结构的不多,不是都可以使用原生指针的,后面的还有树形结构,更加的复杂,所以我们要理解其中的方法,后面学起来会得心应手,下一篇我们开始进入下一个STL容器的学习,是栈和队列,相对来说比较简单,因为是复用前面的容器,所以也叫容器适配器,到下一篇再介绍吧