✨✨小新课堂开课了,欢迎欢迎~✨✨
🎈🎈养成好习惯,先赞后看哦~🎈🎈
所属专栏:C++:由浅入深篇
小新的主页:编程版小新-CSDN博客
前言:
我们前面已经了解到了list的使用和常见接口了,今天我们来深入底层来看一下list的模拟实。list的模拟实现与前面我们模拟显现的string,vector有所不同,我们需要三个类才能成功模拟实现list,下面我们进入正文。
list各函数接口总览
namespace fu
{
//节点类的模拟实现
template<class T>
struct list_node
{
//成员函数
list_node(const T& val = T()); //构造函数
//成员变量
T _val; //数据域
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;
list_iterator(Node* node); //构造函数
//各种运算符重载函数
self operator++();
self operator--();
self operator++(int);
self operator--(int);
bool operator==(const self& s) const;
bool operator!=(const self& s) const;
Ref operator*();
Ptr operator->();
//成员变量
Node* _node; //一个指向结点的指针
};
//模拟实现list
template<class T>
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;
//默认成员函数
list();
list(const list<T>& lt);
list<T>& operator=(const list<T>& lt);
~list();
//迭代器相关函数
iterator begin();
iterator end();
const_iterator begin() const;
const_iterator end() const;
//容量相关函数
size_t size() const;
bool empty() const;
//访问容器相关函数
T& front();
T& back();
const T& front() const;
const T& back() const;
//增删查改
void insert(iterator pos, const T& x);
void erase(iterator pos);
void push_back(const T& x);
void pop_back();
void push_front(const T& x);
void pop_front();
void clear();
void swap(list<T>& lt);
private:
Node* _head; //指向链表头结点的指针
size_t size;//list节点的个数
};
}
注意:为了与标准库里的list产生命名冲突,我们在模拟实现的时候需要将其放在自己的命名空间域中。
一.模拟实现节点类
我们知道list其实就是带头双向循环链表。
因此我们要模拟实现list,首先就需要模拟实现一个节点类,很形象,这个节点类里要包含三个数据:1.要储存的元素 2.指向前一个节点的前驱指针 3.指向后一个节点的后继指针。 这也就是我们所谓的成员变量。
那这个节点需要的成员函数是什么呢?其实我们只需要一个构造函数即可,这个构造函数的目的就是根据所传数据构造一个节点,至于为什么不再需要析构函数,list的析构函数自己就能胜任释放空间的工作。
1.1构造函数
节点类的构造函数只需根据所给数据构造一个节点,数据域就是所给数据,前驱指针和后继指针我们初始化为空即可。
template<class T>
struct list_node
{
//成员函数
list_node(const T& val = T()); //构造函数
{
_val = val;
_next = nullptr;
_prev = nullptr;
}
//成员变量
T _val; //数据域
list_node<T>* _next; //后继指针
list_node<T>*_ prev; //前驱指针
};
二.模拟实现迭代器类
2.1迭代器类单独模拟的原因
我们在模拟实现string,vector的时候都没有单独模拟实现一个迭代器类,为什么这里到模拟实现list就要如此呢。这就要从二者的区别讲起。
我们知道string和vector都是将数据存储在一个连续的内存空间中,我们通过将指针自增,自减以及解引用就能找到对应位置的数据并对其进行相应的操作,因此此时迭代器的本质也就是指针。
我们再来看list,list其实就是带头双向循环链表,是由一个个节点构造的,而这些节点的地址是随意的,不连续的,我们想通过对指针自增,自减等一些操作来达到我们预期的功能是不行的,指针表示我做不到。
为了解决这一问题,我们只好对节点指针进行封装,并对一些操作符进行重载,使得我们可以像使用string,vector的迭代器一样来使用list的迭代器。这里也就呼应了迭代器的主要作用就是让算法能够不用关心底层数据结构,其底层实际就是一个指针,或者是对指针进行了封装。这样就可以让节点指针看起来和普通指针没有什么区别,但是底层却别有不同。
2.2迭代器类的模板参数
迭代器类的模板参数有三个。
template<class T, class Ref, class Ptr>
这里我们还需要定义两个迭代器类型,普通迭代器和const迭代器。
typedef list_iterator<T, T&, T*> iterator;
typedef list_iterator<T, const T&, const T*> const_iterator;
与模板参数一一对应,Ref就是引用类型,Ptr就是指针类型。
当我们使用普通迭代器时,编译器就会实例化出一个普通迭代器对象,该对象可以读取和修改容器中的元素。
当我们使用const迭代器时,编译器就会实例化出一个const迭代器对象,该对象可以保证不意外修改容器中的元素。
总结:
list_iterator
的三个参数分别代表元素类型、引用类型和指针类型。这样的设计提高了类型安全性和灵活性,使得 STL 容器的使用更加方便和安全。
2.3成员函数
1.构造函数
迭代器类实际上就是对结点指针进行了封装,其成员变量就只有一个,那就是结点指针,其构造函数直接根据所给结点指针构造一个迭代器对象即可。
typedef list_node<T> Node;
typedef list_iterator<T, Ref, Ptr> self;
list_iterator(Node* node)
:_node(node)
{}
2.++运算符重载
首先我们来看前置++,先让结点指针指向后一个结点,然后再返回“自增”后的结点指针即可。
self operator++()
{
_node = _node->_next;
return *this;
}
后置++,先记录当前结点指针的指向,然后让结点指针指向后一个结点,最后返回“自增”前的结点指针即可。
self operator++(int)
{
self tmp (*this);
_node = _node->next;
return tmp;
}
3.--运算符重载
首先我们先看前置--,先让节点指针指向前一个节点,然后再返回“自减”后的节点指针即可。
self operator--()
{
_node = _node->_prev;
return *this;
}
后置--,先记录点前节点指针的指向,然后让节点指针指向前一个节点,最后返回“自减”前的节点指针即可。
self operator--(int)
{
self tmp(*this);
_node = _node->_prev;
return tmp;
}
4.==运算符重载
判断这两个迭代器的节点指针的指向是否相同即可。
bool operator==(const self& s) const
{
return _node == s._node;
}
5.!=运算符重载
判断这两个迭代器的节点指针的指向是否不同即可。
bool operator!=(const self& s) const
{
return _node != s._node;
}
6.*运算符重载
直接返回当前节点指针所指向节点的数据即可。
Ref operator*()
{
return _node->data;
}
7.->运算符重载
直接返回当前节点指针所指向的节点数据的地址即可。
Ptr operator->()
{
return &_node->_val;
}
三.list的模拟实现
默认成员函数
构造函数
list是带头双向循环链表,我们在构造时,直接申请一个头结点(哨兵位)让其自己指向自己即可。这里我们也可以多添加一个_size来记录当前节点的个数,方便我们后面的操作。
typedef list_node<T> node;
typedef list_iterator<T, T&, T*> iterator;
typedef list_iterator<T, const T&, const T*> const_iterator;
list()
{
_head = new node;
_head->_next = _head;
_head->_prev = _head;
_size = 0;//记录当前节点的个数
}
拷贝构造函数
拷贝构造函数就是根据所给容器拷贝构造出一个对象,对于拷贝构造函数的实现,我们只需先创建一个头结点,让其自己指向自己,遍历原容器一个个尾插即可。
list(const list<T>& It)
{
_head = new _node;
_head->_next = _head;
_head->_prev = _head;
for (const auto& ch : It)
{
push_back(ch);
}
}
赋值运算符重载
对于赋值运算符重载函数,这里我们提供两种写法。
传统写法:如果不是自己给自己赋值,我们只需先清空原容器的数据,遍历所给容器,一个个尾插到原容器中即可。
//传统写法
list<T>& operator=(const list<T>& lt)
{
if (*this != It)
{
//先清空原来容器里的数据
clear();
//遍历所给容器,将数据一个个尾插
for (const auto& ch : It)
{
push_back(ch);
}
}
return *this;//支持连续赋值
}
现代写法:直接使用swap函数进行交换。
//现代写法
list<T>& operator=(const list<T>& lt)
{
swap(It);
return *this;//支持连续赋值
}
析构函数
我们只需先将容器清空,释放头结点,并对其置空即可。
~list()
{
//清空容器
clear();
//释放头结点
delete _head;
//置空
_head = nullptr;
}
迭代器相关的函数
begin和end
begin就是返回第一个有效节点,end直接返回头结点即可。
//迭代器相关函数
iterator begin()
{
return iterator(_head->next);//返回第一个有效节点
}
iterator end()
{
return iterator(_head);//返回头结点(哨兵位)
}
const_iterator begin() const
{
return iterator(_head->next);//返回第一个有效节点
}
const_iterator end() const
{
return iterator(_head);//返回头结点(哨兵位)
}
容量相关函数
size和empty
由此可见,我们在list类的成员变量中添加一个size,有很大的用处。
//容量相关函数
size_t size() const
{
return size;
}
bool empty() const
{
return size==0
}
容器访问相关函数
front和back
front就是返回第一个有效节点,back就是返回最后一个有效节点。
//访问容器相关函数
T& front()
{
return *begin();//返回第一个有效节点
}
T& back()
{
return *(--end());//返回最后一个有效节点
}
const T& front() const
{
return *begin();//返回第一个有效节点
}
const T& back() const
{
return *(--end());//返回最后一个有效节点
}
list的增删查改
insert
insert在所给迭代器之前插入一个新节点,我们在学习数据结构中的链表时,就是这套插入逻辑。记录好当前节点和前一个节点,用所给数据创建一个新节点,最后只需改变他们之间的指向关系即可。不要忘了给size++。
void insert(iterator pos, const T& x)
{
assert(pos._node);//检查pos位置节点指针的合法性
Node* pcur = pos._node;
Node* prev = pcur->_prev;
Node* newnode = new Node(x);
//prev newnode pcur
newnode->_next = pcur;
pcur->_prev = newnode;
newnode->_prev = prev;
prev->_next = newnode;
size++;
}
erase
erase删除所给迭代器位置的节点,我们在学习数据结构的链表时,删除节点也是这个逻辑。记录好当前节点的前一个节点和后一个节点,最后改变他们之间的指向关系即可。
void erase(iterator pos)
{
assert(pos._node);//检查pos位置节点指针的合法性
Node* pcur = pos._node;
Node* prev = pcur->_prev;
Node* next = pcur->_next;
//prev pcur next
prev->_next = next;
next->_prev = prev;
size--;
}
push_back 和pop_back
push_back是尾插一个节点,pop_back是尾删一个节点。直接复用上面的函数即可。
void push_back(const T& x)
{
insert(end(), x);
}
void pop_back()
{
erase(--end());
}
push_front 和 pop_front
push_front是头插一个节点,pop_front是头删一个节点。直接复用上面的函数即可。
void push_front(const T& x)
{
insert(begin(), x);
}
void pop_front()
{
erase(begin());
}
clear
clear是清空容器,只需遍历删除即可。
void clear()
{
iterator it = begin();
while (it != end())
{
erase(it);
}
}
swap
用库里的交换函数即可。
void swap(list<T>& lt)
{
std::swap(_head, It._head);
}
总结:
list的模拟实现与前面我们知道的string,vector的模拟实现有相似的地方,但也有所不同,我们用了三个类才成功模拟实现list。之后我们再来一起学习栈和队列的相关内容吧。
感谢各位大佬的观看,创作不易,还请各位大佬支持~