【C++】——list模拟实现(包懂的,细节满满)

2024-06-04 14:36

本文主要是介绍【C++】——list模拟实现(包懂的,细节满满),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

list的模拟实现和string和vector的有区别的,但是也有相同。

区别:list的迭代器底层和其他两个迭代器底层有很大区别,因为list的链式结构决定了与它们两个的不一样

相同:迭代器用法大致一样,其他成员函数的使用也大致一样。

本章不仅仅会模拟实现list,同时里面涉及的诸多细节也会一一解释,所以这次的模拟实现也可以是对之前的学习的总结和回顾。

目录

前言

list总体框架

一  list结点

二  list类框架

三  list迭代器

3.1 list迭代器框架

3.2  list常用迭代器

 3.2 list const迭代器

四  list类详解

4.1 插入和删除

 4.2  拷贝构造和赋值运算符重载

 4.3 list析构函数

五  list完整代码

六  细节处理

6.1迭代器的拷贝构造和析构函数

6.2  自定义类型

6.3  打印函数

总结


list总体框架

这里其实和链表一样,我们需要用一个类去把结点表示出来,同时我们还需要用一个类表示list,再者就是还需要一个类去表示迭代器。

在之前的模拟实现中,迭代器我们直接用的是指针表示,但是在这里不行,因为链表不是连续的空间,所以不能通过指针的加减来表示迭代器的进退,但是对于迭代器的操作来说我们可以用运算符重载去模拟,由由于把迭代器一起写进list里面会看起来非常冗余,所以这里采用封装的思想,把迭代器的实现封装起来,也有利于后面的操作

一  list结点

用类来封装一个一个结点,里面有两个指针,一个是指向下一个位置的指针,一个是指向前一个位置,还有一个用来存放数据的变量

template <class T>
struct List_node
{T _data;List_node<T>* _next;List_node<T>* _prev;//构造函数List_node(const T&x=T()):_data(x),_next(nullptr),_prev(nullptr){}
};

由于我们后面会在类外访问,所以为了避免麻烦就写成stuct,这样成员都是public

同时这里使用模板,为了适应各种数据类型

对于构造函数来说,我们直接用初始化列表进行初始化,对于_data的初始化来说,我们用const T&x=T() 这里的T()是一个匿名类型,如果我们没有传参,那么就会去调用相应类型的构造函数

二  list类框架

list类里面包含的是结点的指针,也就是哨兵位头节点的指针

template<class T>
class List
{public:typedef List_node<T> Node;//取别名void empty_Init(){_head = new Node;_head->_next = _head;_head->_prev = _head;}List()//构造函数{empty_Init();}private:Node*_head;//头节点
}

 由于之前封装了结点,所以这里直接用,注意因为结点的封装是一个模板,所以我们在List里面声明的时候也要带上模板,不然会显示找不到对应的构造函数的

因为是双向循环链所以一开始先把所有指针都指向自己就行(注意需要new一个结点出来,这里很容易忘记)

构造的细节并没有写在构造函数里面,而是用一个函数封装起来,因为在之后的拷贝构造中我们也需要构造一个头节点的步骤。

 

三  list迭代器

3.1 list迭代器框架

在前面说了,这里的迭代器是用封装加运算符重载来实现的,由于迭代器也会有很多类型,所以我们使用模板的形式

template<class T>struct _list_iterator{typedef list_node<T> Node;//我们需要一个结点指针去指向结点typedef _list_iterator<T> iterator;//名字太长不好写Node* _node;_list_iterator(Node* node) :_node(node)//构造函数{}}

这里也要注意,因为前面都是模板,所以记得<T>

对于这里的构造函数来说,因为我们使用迭代器都是用一个结点的指针进行构造,同时单参数的构造函数支持隐式类型的转换(如果不理解,可以往后看

3.2  list常用迭代器

list迭代器的各种操作都是用运算符重载来模拟的,所以我们可以写出下面代码

template<class T>struct _list_iterator{typedef list_node<T> Node;typedef _list_iterator<T> iterator;Node* _node;_list_iterator(Node* node) :_node(node){}iterator& operator++()//前置{_node = _node->_next;//直接往下一个结点走return *this;}iterator operator++(int)//后置{iterator tem(*this);_node = _node->_next;return tem;}iterator& operator--(){_node = _node->_prve;//往前一个结点走return *this;}iterator operator--(int){iterator tem(*this);_node = _node->_prve;return tem;}T& operator*(){return _node->_data;//取出结点的数据}T* operator->(){return &_node->_data;}bool operator!=(const iterator& s){return _node != s._node;//比较结点是否是同一个}};

迭代器的每一个操作都采用了运算符重载,其实这样看来还是指针在操作,只不过他不是直接的进行操作,而是换了一种方式

 3.2 list const迭代器

这里的const迭代器也和之前的迭代器有很大不同,之前的迭代器直接在前面加const就行,如果我们直接用 const iterator,那么这个const的作用是现在迭代器的变化,而不是限制迭代器指向的内容,所以这样使用会导致迭代器无法移动

 对于之前的string来说,const迭代器是const char* ,它修饰的是指向字符串的内容,但是这里是const iterator ,它修饰的是iterator,所以两者是不一样的

 所以我们只能再封装一个const迭代器

template<class T>struct const_list_iterator{typedef list_node<T> Node;typedef const_list_iterator<T> _iterator;Node* _node;const_list_iterator(Node* node) :_node(node){}iterator& operator++(){_node = _node->_next;return *this;}iterator operator++(int)//后置{iterator tem(*this);_node = _node->_next;return tem;}iterator& operator--(){_node = _node->_prve;return *this;}iterator operator--(int){iterator tem(*this);_node = _node->_prve;return tem;}const T& operator*()const {return _node->_data;}const T* operator->()const{return &_node->_data;}bool operator!=(const iterator& s){return _node != s._node;}};

但是我们发现const迭代器和非const迭代器的区别就是在 

这两个运算符重载的区别就在于这两个,变化很小,但是我们写了两个迭代器,这样看起来就很冗余

所以这里也可以采用模板的形式对它进行改造。

思路就是先写一个迭代器模板,用这个模板去构造两种迭代器,把任务交给编译器就行

template<class T,class Ref,class Ptr>
struct _List_iterator
{typedef _List_iterator<T,Ref,Ptr> iterator;typedef List_node<T> Node;Node* _node;//构造函数_List_iterator(Node* node):_node(node){}iterator& operator++(){_node = _node->_next;return *this;}iterator& operator++(int){iterator tmp(*this);_node = _node->_next;return tmp;}iterator& operator--(){_node = _node->_prev;return *this;}iterator& operator--(int){iterator tmp(*this);_node = _node->_prev;return tmp;}Ref operator*(){return _node->_data;}Ptr operator->(){return &_node->_data;}bool operator!=(const iterator& s){return _node != s._node;}};

由于需要改动的重载函数,有两个不同的类型一个是*,一个是&,所以模板里面再加两个参数

有了这个模板,我们再list里面可以去构造两种迭代器 。

四  list类详解

弄好了迭代器,我们就可以写相应的成员函数了。

4.1 插入和删除

对于插入和删除来说,就是数据结构中链表的插入和删除

iterator insert(iterator pos, const T& x){Node* cur = pos._node;Node*prve = cur->_prev;Node* newnode = new Node(x);prve->_next = newnode;newnode->_prev = prve;newnode->_next = cur;cur->_prev = newnode;return newnode;//这里不是pos的位置改变,这里返回的是新插入的位置//在用的时候可以不去接收返回值}iterator erase(iterator pos){Node* cur = pos._node;Node* next = cur->_next;Node* prev = cur->_prev;delete cur;prev->_next = next;next->_prev = prev;return next;//迭代器会失效}

这里的插入位置都是迭代器类型的变量

之前vector的插入和删除操作会导致迭代器失效的问题,list的插入操作不会导致迭代器失效,因为list没有扩容的操作。所以也就没有改变相应的位置,但是删除操作有迭代器失效的问题,所以我们在使用的时候记得去接收一下新的位置 

有了插入和删除这两个函数,那我们就可以轻松实现头插,尾插,头删,尾删了

void push_back(const T &x)//尾插{insert(end(), x);}void push_front(const T& x)//头插{insert(begin(), x);}void pop_back()//尾删{erase(end());}void pop_front()//头删{erase(begin());}
 4.2  拷贝构造和赋值运算符重载

对于这两个,相信我们都不会陌生了

拷贝构造:一个已经初始化的对象和一个没有初始化的对象

赋值运算符重载:两个都是已经存在的对象,所以赋值运算符重载另外一种写法就是先清理数据然后再一个个尾插。

拷贝构造

List(const List<T>& lt){empty_Init();for (auto e : lt){push_back(e);}}

 赋值运算符重载

	void swap( List<T>tmp){std::swap(_head, tmp._head);}List<T>operator=( List<T> tmp){swap(tmp);return *this;}

这里再对赋值运算符重载进行一下说明,我们在用的时候会传参给tmp,这里调用拷贝构造,创建了一个新的结点,所以交换一下,并没有说两个对象指向同一块空间,所以不存在析构两次的问题

对于赋值运算符重载来说,还有一种写法,就是先清理数据,然后再一个个尾插

 4.3 list析构函数
~List(){iterator it =begin();while(it != end()){it=erase(it);}}

就是把数据一个个删除就行。

 

五  list完整代码

#pragma once
template <class T>
struct List_node
{T _data;List_node<T>* _next;List_node<T>* _prev;//构造函数List_node(const T&x=T()):_data(x),_next(nullptr),_prev(nullptr){}
};template<class T,class Ref,class Ptr>
struct _List_iterator
{typedef _List_iterator<T,Ref,Ptr> iterator;typedef List_node<T> Node;Node* _node;//构造函数_List_iterator(Node* node):_node(node){}iterator& operator++(){_node = _node->_next;return *this;}iterator& operator++(int){iterator tmp(*this);_node = _node->_next;return tmp;}iterator& operator--(){_node = _node->_prev;return *this;}iterator& operator--(int){iterator tmp(*this);_node = _node->_prev;return tmp;}Ref operator*(){return _node->_data;}Ptr operator->(){return &_node->_data;}bool operator!=(const iterator& s){return _node != s._node;}};template<class T>
class List
{typedef List_node<T> Node;
public:typedef _List_iterator<T,T&,T*> iterator;typedef _List_iterator<T, const T&, const T*> const_iterator;iterator begin(){return _head->_next;}iterator end(){return _head;}const_iterator begin()const{return _head->_next;}const_iterator end()const{return _head;}//构造函数void empty_Init(){_head = new Node;_head->_next = _head;_head->_prev = _head;}List(){empty_Init();}~List(){iterator it =begin();while(it != end()){it=erase(it);}}//List(const List<T>& lt){empty_Init();for (auto e : lt){push_back(e);}}void swap( List<T>tmp){std::swap(_head, tmp._head);}List<T>operator=( List<T> tmp){swap(tmp);return *this;}void push_back(const T &x){insert(end(), x);}void push_front(const T& x){insert(begin(), x);}void pop_back(){erase(end());}void pop_front(){erase(begin());}iterator insert(iterator pos, const T& x){Node* cur = pos._node;Node*prve = cur->_prev;Node* newnode = new Node(x);prve->_next = newnode;newnode->_prev = prve;newnode->_next = cur;cur->_prev = newnode;return newnode;}iterator erase(iterator pos){Node* cur = pos._node;Node* next = cur->_next;Node* prev = cur->_prev;delete cur;prev->_next = next;next->_prev = prev;return next;}private:Node* _head;
};//打印
template<typename Container>
void print_container(const Container& con)
{typename Container::const_iterator it = con.begin();while (it != con.end()){cout << *it << " ";++it;}cout << endl;
}

六  细节处理

6.1迭代器的拷贝构造和析构函数

在上面代码中我们没有写迭代器的析构函数和拷贝构造,因为我们不需要析构函数,我们不能说遍历一遍结点就把结点给释放了,再者就是拷贝构造

    List<int>::iterator it = lt.begin();这断代码表示的是lt.begin()返回一个迭代器类型,然后拷贝构造给it,那存不存在析构两次的问题呢,答案是不存在

我们需要的就是浅拷贝,我们要的就是it指向这个结点的位置,同时也只有它指向,所以在函数结束的时候,这个it会被销毁。

所以我们不需要写拷贝构造和析构函数,拷贝构造直接用编译器的浅拷贝就行。对于析构函数来说,因为这里的迭代器只起到遍历的作用,所以不需要去释放任何资源。而且如果写了析构还会出现各种析构两次的问题

6.2  自定义类型
class AA
{
public:AA(int a1=0,int a2=0):_a1(a1),_a2(a2){}int _a1;int _a2;
};
void test1()
{List<AA>lt;lt.push_back(AA(1, 1));lt.push_back(AA(2, 2));lt.push_back(AA(2, 2));List<AA>::iterator it = lt.begin();while (it != lt.end()){cout << *it << " ";it++;}
}

对于这个来说这样写是错误的,因为AA类里面没有重载<<这个运算符

所以我们得换一直方式

void test1()
{List<AA>lt;lt.push_back(AA(1, 1));lt.push_back(AA(2, 2));lt.push_back(AA(2, 2));List<AA>::iterator it = lt.begin();while (it != lt.end()){cout << (*it)._a1 << " " << (*it)._a2 << endl;it++;}while (it != lt.end()){cout << it->_a1 << " " << it->_a2 << endl;it++;}
}

这就是为什么要重载->运算符的原因,这个是给自定义类型用的,对于知内置类型直接解引用就行。

6.3  打印函数

在上面的完整代码中有一个打印函数,它也是用模板实现的,主要是为了打印各种容器

//打印
template<typename Container>
void print_container(const Container& con)
{typename Container::const_iterator it = con.begin();while (it != con.end()){cout << *it << " ";++it;}cout << endl;
}

 这里的不同是我们之前都是用class的,这里用的是typename,原因在于编译器在编译的时候,会去Container里面去找const_iterator这个类型,但是Container是一个模板,并没有实例化,所以编译器也不知道怎么定义,所以就在编译的时候就过不了,但是我们在前面加一个typename就会告诉编译器,先过,后面再来实例化,这样就可以解决问题了

总结

以上就是list模拟实现的全部内容了,希望对你有所帮助 

 

 

这篇关于【C++】——list模拟实现(包懂的,细节满满)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1030337

相关文章

Java实现MD5加密的四种方式

《Java实现MD5加密的四种方式》MD5是一种广泛使用的哈希算法,其输出结果是一个128位的二进制数,通常以32位十六进制数的形式表示,MD5的底层实现涉及多个复杂的步骤和算法,本文给大家介绍了Ja... 目录MD5介绍Java 中实现 MD5 加密方式方法一:使用 MessageDigest方法二:使用

mysql删除无用用户的方法实现

《mysql删除无用用户的方法实现》本文主要介绍了mysql删除无用用户的方法实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 1、删除不用的账户(1) 查看当前已存在账户mysql> select user,host,pa

Nginx配置location+rewrite实现隐性域名配置

《Nginx配置location+rewrite实现隐性域名配置》本文主要介绍了Nginx配置location+rewrite实现隐性域名配置,包括基于根目录、条件和反向代理+rewrite配置的隐性... 目录1、配置基于根目录的隐性域名(就是nginx反向代理)2、配置基于条件的隐性域名2.1、基于条件

Linux配置IP地址的三种实现方式

《Linux配置IP地址的三种实现方式》:本文主要介绍Linux配置IP地址的三种实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录环境RedHat9第一种安装 直接配置网卡文件第二种方式 nmcli(Networkmanager command-line

Java实现将Markdown转换为纯文本

《Java实现将Markdown转换为纯文本》这篇文章主要为大家详细介绍了两种在Java中实现Markdown转纯文本的主流方法,文中的示例代码讲解详细,大家可以根据需求选择适合的方案... 目录方法一:使用正则表达式(轻量级方案)方法二:使用 Flexmark-Java 库(专业方案)1. 添加依赖(Ma

C++快速排序超详细讲解

《C++快速排序超详细讲解》快速排序是一种高效的排序算法,通过分治法将数组划分为两部分,递归排序,直到整个数组有序,通过代码解析和示例,详细解释了快速排序的工作原理和实现过程,需要的朋友可以参考下... 目录一、快速排序原理二、快速排序标准代码三、代码解析四、使用while循环的快速排序1.代码代码1.由快

使用EasyExcel实现简单的Excel表格解析操作

《使用EasyExcel实现简单的Excel表格解析操作》:本文主要介绍如何使用EasyExcel完成简单的表格解析操作,同时实现了大量数据情况下数据的分次批量入库,并记录每条数据入库的状态,感兴... 目录前言固定模板及表数据格式的解析实现Excel模板内容对应的实体类实现AnalysisEventLis

Mybatis从3.4.0版本到3.5.7版本的迭代方法实现

《Mybatis从3.4.0版本到3.5.7版本的迭代方法实现》本文主要介绍了Mybatis从3.4.0版本到3.5.7版本的迭代方法实现,包括主要的功能增强、不兼容的更改和修复的错误,具有一定的参考... 目录一、3.4.01、主要的功能增强2、selectCursor example3、不兼容的更改二、

VSCode中C/C++编码乱码问题的两种解决方法

《VSCode中C/C++编码乱码问题的两种解决方法》在中国地区,Windows系统中的cmd和PowerShell默认编码是GBK,但VSCode默认使用UTF-8编码,这种编码不一致会导致在VSC... 目录问题方法一:通过 Code Runner 插件调整编码配置步骤方法二:在 PowerShell

如何使用C#串口通讯实现数据的发送和接收

《如何使用C#串口通讯实现数据的发送和接收》本文详细介绍了如何使用C#实现基于串口通讯的数据发送和接收,通过SerialPort类,我们可以轻松实现串口通讯,并结合事件机制实现数据的传递和处理,感兴趣... 目录1. 概述2. 关键技术点2.1 SerialPort类2.2 异步接收数据2.3 数据解析2.