C++——STL容器——List

1. 前言 

        List也是STL容器的一种,是C++提供的链表结构的容器。C++中所提供的list是双向带头循环链表,我们这篇文章通过自己模拟实现来学习list的使用。

        为了避免和库中的命名冲突,也为了封装的考虑,我们将我们的list放入一个命名空间之中以和外界隔离。

namespace m_list	//为自己实现的list定义命名空间
{//代码
}

2. 链表的结点

        因为链表的各个元素是由一个个结点串起来的,所以我们首先需要定义出结点。

	//结点template <class T>struct ListNode	//使用类模板来定义结点的信息{//双向链表,节点内存前驱指针和后继指针,以及值ListNode<T>* _prev;ListNode<T>* _next;T _val;//构造函数,使用初始化列表初始化ListNode(const T& val = T())	//参数注意使用const引用,因为实参可能是常性的:_prev(nullptr),_next(nullptr),_val(val){}};

        对于链表节点的定义,有几点需要解释:

        ①我们采用泛型编程的思想,使用类模板来定义结点的结构体以保证满足各种数据与类型的结点。
        ②由于我们的链表是双向带头循环链表,所以结点的成员变量应该包括前驱指针、后继指针和结点的值。

        ③构造函数缺省值为我们已经使用了多次的T(),且为了保证变量和常量值均可被传递,需要使用const引用修饰参数。

3. 链表

3.1 成员变量

        同样是为了满足泛型编程的需求采取类模板定义链表类。链表类管理着链表,所以其成员变量中存储着链表的头结点的指针,类型就是结点类模板实例化的指针。另外一个参数则是表示链表的size。

	//链表template <class T>class list{private:typedef ListNode<T> Node;	//使用结点时需要对结构体模板参数实例化,因此使用typedef简化代码Node* _head;	//链表的头size_t _size;	//链表的规模};

3.2 构造函数

3.2.1 无参构造

        无参构造为我们给出了最基本的构造要求,由于是带头链表,所以应该在构造函数中将链表初始化完成,也就是创建好头结点并且指针指向对应的位置。

		//无参构造//双向带头链表,所以需要先new一个头结点,并且前后指针都指向自己list():_head(new Node), _size(0){_head->_prev = _head;_head->_next = _head;}

3.2.2 初始化列表构造

        初始化列表在vector的文章中接触过,这里不再解释。初始化列表构造也需要完成无参构造中的操作,创建出头结点,然后再进行元素的逐个插入。所以此处会用到尾插函数以及迭代器,这部分会在后续介绍。

		list(std::initializer_list<T> il):_head(new Node), _size(0){_head->_prev = _head;_head->_next = _head;for (auto& e : il){push_back(e);}}

3.2.3 拷贝构造

        拷贝构造也需要通过同样的方式创建出头结点。然后将元素进行尾插,此处则是需要尾插函数,由于传参考虑到const和非const的链表对象,因此使用const修饰的链表作为参数,因此遍历这个链表取出数据就需要const迭代器。

		//拷贝构造//拷贝构造初始化出头结点,然后将结点逐个插入list(const list<T>& lt):_head(new Node),_size(0){_head->_prev = _head;_head->_next = _head;for (auto& e : lt){push_back(e);}}

3.3 析构函数(以及clear函数)

        clear函数通过迭代器的遍历将链表结点进行意义释放删除。析构函数z则是复用clear函数,将链表结点全部释放后,再释放头结点,完成析构操作。

		//析构函数//析构需要释放所有的结点,可以通过逐个结点删除的方式来复用函数,完成释放~list(){clear();delete _head;_head = nullptr;}void clear(){iterator it = begin();//使用迭代器对链表结点进行删除时,注意无需it++while (it != end()){it = erase(it);}}

3.4 赋值运算符重载(以及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);return *this;}

3.5 链表属性函数

        主要使用到size和empty函数,实现十分简单。

		size_t size(){return _size;}bool empty(){return _size == 0;}

3.6 插入删除函数

3.6.1 插入函数

3.6.1.1 随机位置插入insert

        我们主要使用下图中的第一种重载形式,insert函数在pos位置前插入val的值。

         插入数据的逻辑就是通过迭代器变量pos找到对应的位置,然后创建新的结点,然后进行链接就好了。

		//插入//对于list而言,不存在扩容的行为,所以list的插入不会导致迭代器失效//insert在pos位置前插入val,为满足临时变量的插入需要使用const引用void insert(iterator pos, const T& val){Node* next = pos._node;Node* prev = next->_prev;Node* newnode = new Node(val);prev->_next = newnode;newnode->_prev = prev;newnode->_next = next;next->_prev = newnode;++_size;}
3.6.1.2 尾部插入

        复用insert函数。

void push_back(const T& val)
{insert(end(), val);
}
3.6.1.3 头部插入

        复用insert函数。

void push_back(const T& val)
{insert(end(), val);
}
3.6.1.4 迭代器失效(否)

        和vector不同,由于链表的特殊结构,所以在插入值的过程中并不会发生让结点地址产生改变的情况,所以list的插入不会让迭代器失效。

3.6.2 删除数据

3.6.2.1 随机位置删除

        erase删除pos位置处的值,并且返回新的迭代器变量,因为迭代器存在失效的可能。

        删除pos位置的值,首先是通过pos找到对应的结点,然后将链表链接起来后释放pos位置的结点。在这里细致梳理一下(没什么强调的,就是梳理一下流程),释放对应结点时,由于结点是一个自定义的结构体类型,所以此时会调用结点结构体的析构函数,由于我们没有写,所以编译器默认生成,对于内置类型(如_prev、_next和内置类型实例化的value)不做处理,而对于自定义类型(自定义类型实例化的val)则会调用其析构函数。

        在最后返回更新后迭代器的值,由于迭代器类的构造函数是单参数,所以支持隐式类型转换。 

		//删除//erase由于释放结点,所以有迭代器失效的可能性,所以删除操作需要返回迭代器提供更新iterator erase(iterator pos){Node* cur = pos._node;Node* prev = cur->_prev;Node* next = cur->_next;prev->_next = next;next->_prev = prev;delete cur;//cur是ListNode<T>类型,delete时会调用对应的析构函数//由于ListNode<T>的成员变量都是内置类型,所以无需手动释放资源,使用编译器生成的析构函数即可--_size;//return next;	//也可以,单参数构造函数支持隐式类型转换return iterator(next);}
3.6.2.2 尾部删除

        复用erase函数。

		iterator pop_back(){erase(--end());}
3.6.2.3 头部删除

        复用erase函数。

		iterator pop_front(){erase(begin());}
3.6.2.4 迭代器失效

        因为删除节点会使得原空间被释放,所以存在迭代器失效的可能,迭代器失效的原因和vector一样,由于存在删除,后续的元素会向前补齐,迭代器指向的内容发生变化,所以在删除造作后,记得使用返回值更新迭代器。

4. 迭代器

        迭代器可谓是list中最值得学习的部分了,我们将通过list的迭代器实现明白如何包装一个迭代器让其符合使用逻辑,以及如何通过类模板的方式提高代码复用率。

        迭代器最重要的一点就是不论内部实现和底层如何花里胡哨,最后使用的方式一定是一致的。

4.1 迭代器类

        对于链表而言,由于其物理存储空间不连续,所以迭代器不可以使用原生的指针,因为自增等行为有很大不同。因此需要我们自己根据需要实现迭代器,因此封装一个迭代器类,来支持链表的特性。

        那么我们首先要明确这个迭代器需要哪些参数。

        首先,因为实例化的参数不同,所以迭代器也必然不同,因此第一个模板参数应该可以表示迭代器之下的数据的数据类型。

        其次,对于迭代器,为了支持const和非const的迭代器,同时为了代码复用,所以在迭代器模板中增加模板参数,来选择const和非const,因此我们就需要考虑清楚const和非const的迭代器之间存在哪些不同。是否是const的区别只在于能否对值进行修改,所以返回值是与存储的元素直接关联的函数,即两个解引用操作。因此考虑他们的返回值不同,对模板参数增加两个,分别代表*解引用和->解引用的返回值。

4.1.1 迭代器模板与成员变量

        因为*解引用操作返回的是元素的引用,->解引用操作返回的是元素的指针,所以使用T表示真正索要存储数据的数据类型,增加Ref表示其const或非const的引用,Ptr表示其const或非const的指针。

        为了可以很方便地进行链表遍历,所以使用结点指针的方式作为迭代器,这样方便进行遍历,同时是一个指针所以节省空间。

//迭代器
//对于链表而言,由于其物理存储空间不连续,所以迭代器不可以使用原生的指针,因为自增等行为有很大不同
//因此需要我们自己根据需要实现迭代器,因此封装一个迭代器类,来支持链表的特性
//对于迭代器,为了支持const和非const的迭代器,同时为了代码复用,所以在迭代器模板中增加模板参数,来选择const和非const
//由于const版本迭代器只在解引用的时候有不同,所以增加两个模板参数对解引用的返回值进行控制
template <class T, class Ref, class Ptr>
struct ListIterator
{//类比学习,迭代器的作用相当于元素的替身。//在string中,string是由一个个字符组成的,迭代器变量表示字符串中的字符,通过解引用操作即可访问其值//在vector中,vector是由一个个元素组成的,迭代器变量表示数组中的元素,通过解引用操作可以访问该元素//同理,对于list是由一个个结点组成的,迭代器应该表示链表的结点,因此采用结点指针的方式定义迭代器typedef ListNode<T> Node;Node* _node;};

4.1.2 构造函数

        对于iterator迭代器,其存在一定是指向某个结点的,所以使用带一个参数的构造函数为结点指针成员赋值,即迭代器此时就是这个结点的高层次的代替。

		//构造函数ListIterator(Node* node):_node(node){}

4.1.3 解引用

4.1.3.1 *操作符重载

        再次提及这句话:迭代器最重要的一点就是不论内部实现和底层如何花里胡哨,最后使用的方式一定是一致的。所以回忆曾经的迭代器使用,当对一个迭代器遍历it进行*it操作后,表达式结果就是迭代器指向元素的值的引用,可以进行访问修改。

        对于链表也不例外,我们索要返回的正是这样一个迭代器所指向元素的引用。对于this指针,其成员变量_node就是对应元素所在结点的指针,所以返回_node->_val,就相当于返回了引用,达到目的。

        至于返回值,由Ref模板参数决定,当为T类型的const时,Ref就会是const T,否则就是T。所以返回值类型为Ref&即可。

		//解引用//*it//迭代器追求的是不考虑组织形式,采取一致的格式进行遍历//因此,在list中,要保证*it可以像其他容器一样拿到对应的数据,所以*it应当返回T//对于*it,非const对象拿到的是非const返回值,const对象拿到的是const返回值Ref& operator*(){return _node->_val;}
4.1.3.2 ->操作符重载

        为什么需要->操作符呢,在我们的印象中似乎*解引用已经足够了。那么这时候就需要考虑当T被实例化为一个结构体,我们此时拿到了迭代器变量it,我们知道*it是取得其元素,那么对于结构体我们想一步到位访问结构体对象中的成员,就会用到it->a类似的情景。

	void Test2(){A a{ 'a',{1,1} };list<A> lt1;lt1.push_back(a);lt1.push_back({ 'b', { 2,2 } });list<A>::iterator it1 = lt1.begin();while (it1 != lt1.end()){std::cout << it1->c1 << ' ';++it1;}std::cout << std::endl;it1 = lt1.begin();std::cout << it1->a[0] << ' ' << it1->a[1] << std::endl;++it1;std::cout << it1->a[0] << ' ' << it1->a[1] << std::endl;}

        为了应付这种情况所以出现了->重载。->重载函数的返回值是this指针的_node->_val的指针,返回地址后再->即可找到需要的成员。仔细思考,发现似乎少了一个->操作符,因为一个->是迭代器操作符重载,返回了自定义类型(实例化T)的指针,应该再来一个->指向T的成员。但是这样写会让语法变得奇怪,不易读,所以规定省略一个->来保证可读性。所以实质上,原本的it->->a,可以写作it->a,实际上是it.operator->()->a。

        返回类型Ptr和Ref一样,根据需求传递const T*或者T*即可。

4.1.4 自增自减与判断

        需要强调的是为了区分前置++/--和后置++/--,会给后置操作符一个int参数。

		//对于list,迭代器自增自减的行为即是找到next和prev结点//前置++ListIterator<T, Ref, Ptr>& operator++(){_node = _node->_next;return *this;}//后置++ListIterator<T, Ref, Ptr> operator++(int){ListIterator<T, Ref, Ptr> tmp(_node);_node = _node->_next;return tmp;}//前置--ListIterator<T, Ref, Ptr>& operator--(){_node = _node->_prev;return *this;}//后置--ListIterator<T, Ref, Ptr> operator--(int){ListIterator<T> tmp(_node);_node = _node->_prev;return tmp;}//==和!=bool operator==(const ListIterator<T, Ref, Ptr>& it){return _node == it._node;}bool operator!=(const ListIterator<T, Ref, Ptr>& it){return _node != it._node;}

4.2 迭代器在list类中

        在解决了迭代器形式和操作的问题后就好办了,在list类中实现const和非const的begin和end函数即可。

        模板参数已经做过介绍,iterator的构造函数是单参数的结点指针,所以支持单参数隐式类型转换。

		//迭代器//对于*it,非const对象拿到的是非const返回值,const对象拿到的是const返回值//对于it->,非const对象拿到的是非const指针,const对象拿到的是const指针typedef ListIterator<T, T&, T*> iterator;typedef ListIterator<T, const T&, const T*> const_iterator;//begin和end返回值都是迭代器类的对象,单参数构造函数所以支持隐式类型转换//非constiterator begin(){return _head->_next;}iterator end(){return _head;}//constconst_iterator begin() const{return _head->_next;}const_iterator end() const{return _head;}

4.3 迭代器总结

        为了实现迭代器正常功能所以将迭代器用类模板进行了封装,又为了const考虑增加两个模板参数来控制。因为模板和结构体相互交错,所以此处对于->和*给出两个分析示例,帮助理清逻辑:

	void Test3(){list<A> lt1;lt1.push_back({ 'a',{1,1} });list<A>::iterator it1 = lt1.begin();it1->c1;//it1实际的类型是ListIterator<A,A&,A*>,成员是ListNode<A>的结构体指针,这个结构体是链表的结点,包含成员前序、后继指针和值//it1的构造函数将ListNode<A>*类型的参数赋值给it1下的成员(链表结点指针),使用的是lt1调用的begin,返回值是ListIterator<A,A&,A*>类型,其中的结点是头结点的下一个结点//ListIterator<A,A&,A*>类型的参数通过隐式类型转换传参给构造函数,赋值给it1对象//it1->c1调用了Ptr operator->()函数,Ptr被实例化为了A*,返回了_node->_val的地址,也就是返回了lt1链表中的值的地址,即结构体{ 'a',{1,1} }的地址//省略了一个->操作符,找到了c1成员,实际上是it1.operator->()->c1list<int> lt2;lt2.push_back(1);list<int>::iterator it2 = lt2.begin();*it2;//it2实际的类型是ListIterator<int,int&,int*>,成员是ListNode<int>的结构体指针,这个结构体是链表的结点,包含成员前序、后继指针和值//it2的构造函数将ListNode<int>*类型的参数赋值给it1下的成员(链表结点指针),使用的是lt1调用的begin,返回值是ListIterator<int,int&,int*>类型,其中的结点是头结点的下一个结点//ListIterator<int,int&,int*>类型的参数通过隐式类型转换传参给构造函数,赋值给it2对象//*it2调用了Ref operator*()函数,Ref被实例化为了int&,返回了_node->_val的引用,也就是返回了lt2链表中的值的引用,即结点内容 1 的引用}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/13260.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

微服架构基础设施环境平台搭建 -(七)Kubesphere pod内安装vimping命令

微服架构基础设施环境平台搭建 -&#xff08;七&#xff09;Kubesphere pod安装vim&ping命令 在K8s集群运维过程&#xff0c;需要进入pod容器内通过ping来测试网络是否通畅&#xff0c;或使用vim进行编辑文件&#xff0c;但是pod容器默认情况下是不支持ping、vim命令的&…

前端面试题日常练-day05 【面试题】

题目 希望这些选择题能够帮助您进行前端面试的准备&#xff0c;答案在文末。 1. 下列哪个 CSS 属性用于设置元素的背景颜色&#xff1f; A) color B) font-size C) background-color D) text-align2. 在 JavaScript 中&#xff0c;以下哪个函数可以用于将字符串转换为小写&a…

2024.05.15学习记录

1、完成Ts重构Axios项目中更多功能的开发 2、刷题&#xff1a;二叉树&#xff08;代码回忆录&#xff09; 3、复习diff算法源码解读

关于RK3588平台使用配置Qt、QtCreator、Gstreamer环境的一点记录

最近在做Qt工程代码平台适配的过程中&#xff0c;遇到了一些问题&#xff0c;记录一下。 问题一、主窗体无法透明 首先发现自己的Qt工程的主窗体的透明度无法控制的问题&#xff0c;要么全透明&#xff0c;要么不透明&#xff0c;后来查阅了一些关于linux和linux图形界面的相…

C语言 | Leetcode C语言题解之第91题解码方法

题目&#xff1a; 题解&#xff1a; int numDecodings(char* s) {int n strlen(s);// a f[i-2], b f[i-1], c f[i]int a 0, b 1, c;for (int i 1; i < n; i) {c 0;if (s[i - 1] ! 0) {c b;}if (i > 1 && s[i - 2] ! 0 && ((s[i - 2] - 0) * 10…

Flutter 中的 MaterialApp 小部件:全面指南

Flutter 中的 MaterialApp 小部件&#xff1a;全面指南 MaterialApp 是 Flutter 中用于创建整个 Material Design 风格的应用程序的小部件。它提供了一套丰富的组件和默认设置&#xff0c;以确保应用遵循 Material Design 的指南。本文将详细介绍 MaterialApp 的用途、属性、使…

C++|树形关联式容器(set、map、multiset、multimap)介绍使用

目录 一、关联式容器介绍 1.1概念 1.2键值对 1.3树形结构的关联式容器 1.3.1pair模板介绍 1.3.2make_pair的介绍 二、set的介绍和使用 2.1set介绍 2.2set使用 2.2.1构造 2.2.2容量 2.2.3修改 三、map的介绍和使用 3.1map介绍 3.2map使用 3.2.1构造 3.2.2容量 …

MyBatis-Plus 2万字面试题详解

目录 什么是MyBatis-Plus? MyBatis-Plus与MyBatis有什么区别? MyBatis-Plus的主要优点有哪些?

ubuntu下pycharm闪退

pycharm依赖于jdk环境&#xff0c;要把java的jdk环境配置好&#xff0c;可参考以下链接&#xff1a; ubuntu2024.04下配置jdk&#xff08;安装java环境&#xff09;

力扣刷题总结--链表部分

链表部分 通用操作 链表的两种操作方式 不用虚拟头结点用虚拟头节点 为什么要定义cur指针 因为操作完链表以后&#xff0c;是要返回头节点的。如果你上来就操作头节点&#xff0c;那么头节点的值都改了&#xff0c;返回head就不是我们最开始的链表了。 写while循环的时候&am…

相机模型的内参、外参

相机模型的内参、外参 文章目录 相机模型的内参、外参1. 针孔模型、畸变模型&#xff08;内参&#xff09;2. 手眼标定&#xff08;外参&#xff09; Reference 这篇笔记主要参考&#xff1a;slam十四讲第二版&#xff08;高翔&#xff09; 相机将三维世界中的坐标点&#xff…

JVM线程和内存溢出问题排查思路

一、工具 Arthas&#xff1a;Arthas 是一款能在线查看应用 load、内存、gc、线程 等状态信息&#xff0c;并对业务问题进行诊断的工具&#xff0c;支持 JDK 6 和 Linux/Mac/Windows 系统。 jstack&#xff1a;jstack是JVM自带的Java堆栈跟踪工具&#xff0c;它用于打印出给定…

PDK安装及简介

目录 PDK简介 pdk安装 Standard Cell Library简介 IO Library简介 PDK简介 PDK&#xff1a;全称Process Design Kit&#xff0c;是工艺设计工具包的缩写&#xff0c;是制造和设计之间的沟通桥梁&#xff0c;是模拟电路设计的起始点。 具体来说&#xff0c;PDK是代工厂(FAB…

使用Docker进行Jmeter分布式搭建

大家好&#xff0c;随着技术的不断发展&#xff0c;对性能测试的要求也日益提高。在这样的背景下&#xff0c;如何利用 Docker 来巧妙地搭建 Jmeter 分布式成为了关键所在。现在&#xff0c;就让我们开启这场探索之旅&#xff0c;揭开其神秘的面纱。前段时间给大家分享了关于 L…

通信指挥类装备(多链路聚合设备)-应急通信指挥解决方案

现场通信指挥系统是一种功能全面的便携式音视频融合指挥通信平台&#xff0c;可实现现场应急救援指挥、多种通信手段融合、现场通信组网等功能&#xff0c;是现场指挥系统的延伸。 多链路聚合设备&#xff0c;是一款通信指挥类装备&#xff0c;具有 4G/5G&#xff0c;专网&…

Golang开发:切片的两种创建方式及区别

在Go语言中&#xff0c;var和make都可以用来初始化切片&#xff08;slice&#xff09;&#xff0c;但它们之间有一些重要的区别。 初始化方式&#xff1a; 使用var关键字声明的切片会被初始化为nil&#xff0c;即没有底层数组&#xff0c;长度为0&#xff0c;容量为0。使用make…

Free RTOS中Semaphore(二值信号量)的使用介绍

目录 概述 1 使用STM32Cube 配置信号量 1.1 引子 1.2 STM32Cube中配置FreeRTOS 1.3 STM32Cube生成Project 2 cmsis_os中信号量接口函数 2.1 函数&#xff1a;osSemaphoreNew 2.2 函数&#xff1a;osSemaphoreGetName 2.3 函数&#xff1a;osSemaphoreAcquire 2.4 函数…

hashmap数据结构为什么是链表

HashMap 数据结构中&#xff0c;链表通常用于解决哈希冲突。当不同的键映射到相同的哈希桶时&#xff0c;就会发生哈希冲突。链表是一种简单而有效的解决方法。 在 JDK 8 之前的 HashMap 实现中&#xff0c;当发生哈希冲突时&#xff0c;冲突的元素会被存储在同一个哈希桶中&a…

设计模式:外观模式(Facade)

设计模式&#xff1a;外观模式&#xff08;Facade&#xff09; 设计模式&#xff1a;外观模式&#xff08;Facade&#xff09;模式动机模式定义模式结构时序图模式实现在单线程环境下的测试在多线程环境下的测试模式分析优缺点适用场景应用场景模式扩展参考 设计模式&#xff1…

C++STL(queue和list)

3.6 queue 容器 3.6.1 queue 基本概念 概念&#xff1a;Queue是一种先进先出(First In First Out,FIFO)的数据结构&#xff0c;它有两个出口 队列容器允许从一端新增元素&#xff0c;从另一端移除元素队列中只有队头和队尾才可以被外界使用&#xff0c;因此队列不允许有遍历…