【C++】模拟实现list

news2025/1/13 6:36:50

🦄个人主页:修修修也

🎏所属专栏:数据结构

⚙️操作环境:Visual Studio 2022


目录

一.了解项目及其功能

📌了解list官方标准

了解模拟实现list

📌了解更底层的list实现

二.list迭代器和vector迭代器的异同

📌迭代器的分类

🎏单向迭代器(Forward Iterator)

🎏双向迭代器(Bidirectional Iterator)

🎏随机迭代器(Random Access Iterator)

📌list和vector的底层实现差异对迭代器的影响

三.逐步实现项目功能模块及其逻辑详解

📌分析list的组成结构

📌实现list结点类模板

🎏构造list结点类成员变量

🎏实现list结点类构造函数

📌实现list迭代器类模板

🎏构造list迭代器类成员变量

🎏实现list迭代器类构造函数

🎏实现operator*重载函数

🎏实现前置operator++重载函数

🎏实现后置operator++重载函数

🎏实现前置operator--重载函数

🎏实现后置operator--重载函数

🎏实现operator!=重载函数

🎏实现operator==重载函数

🎏实现operator->重载函数

🎏实现const修饰的list迭代器类模板

📌实现list类模板

🎏构造list类成员变量

🎏实现list类empty_init()函数

🎏实现list类无参构造函数

🎏实现list类拷贝构造函数

🎏实现list类swap()函数

🎏实现list类operator=()函数

🎏实现list类clear()函数

🎏实现list类析构函数

🎏实现list类begin()函数

🌳实现普通list类的begin()函数

🌳实现const修饰list类的begin()函数

🎏实现list类end()函数

🌳实现普通list类的end()函数

🌳实现const修饰list类的end()函数

🎏实现list类insert()函数

🎏实现list类erase()函数

🎏实现list类push_front()函数

🎏实现list类pop_front()函数

🎏实现list类push_back()函数

🎏实现list类pop_back()函数

🎏实现list类size()函数

四.项目完整代码

test.cpp文件

  list.h 文件

结语


一.了解项目及其功能

📌了解list官方标准

        在本次项目中我们的目标是模拟实现一个list,先一起看一下C++标准文档中list的定义:cplusplus : C++ list标准文档icon-default.png?t=N7T8https://legacy.cplusplus.com/reference/list/list/?kw=list

        总结一下:

  1. list是可以在O(1)范围在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代
  2. list的底层是带头双向循环链表结构,带头双向循环链表中每个数据元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素
  3. list与forward_list(单链表)非常相似:最主要的不同在于forward_list是单链表只能朝前迭代,让其更简单高效。
  4. 与其他的序列式容器相比(array,vector,deque),list通常在任意位置进行插入、移除元素的执行效率更好
  5. 与其他序列式容器相比,list和forward_list最大的缺陷是不支持任意位置的随机访问,比如:要访问list的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;list还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的list来说这可能是一个重要的因素)

 声明:

        该模拟实现仅适用于STL初学小白了解list的简单实现,会结合一些STL源码作为参照,但是源码中涉及的空间配置器部分我们暂不做涉及!

        在本篇博文中,将使用new/delete等方式来完成C++的动态内存的申请与释放.该方式相比于空间配置器效率有所降低,但是初学者较为友好的一种初步了解STL的一种方式.


了解模拟实现list

        该list类模板使用动态内存分配空间,可以用来存储任意数量的同类型数据.

带头双向循环链表结点(Node)需要包含三个成员:前指针域prev,数据域val,后指针域next.

结点(Node)逻辑结构图示如下:

  list提供的功能有:

  1. list的默认成员函数
  2. list的swap()函数
  3. list的clear()函数
  4. list的begin()函数
  5. list的end()函数
  6. list的insert()函数
  7. list的erase()函数
  8. list的push_front()函数
  9. list的pop_front()函数
  10. list的push_back()函数
  11. list的pop_back()函数
  12. list的size()函数
  13. list的operator=运算符重载函数

        注意,因为我们要实现的list类并不只满足于只能存储一种固定类型的元素,我们在一个项目中,可能会创建几个存储不同类型元素的list,如:

list<int> lage;      //存放学生年龄
list<string> lname;  //存放学生姓名
list<double> lhigh;  //存放学生身高
list<Student> lstu;  //存放自定义学生类

        因此,我们需要将list实现为一个类模板,这样才可以满足上面的需求,有关C++泛型编程模板相关知识还不是很了解的朋友可以先移步:

【C++】初阶模板icon-default.png?t=N7T8https://blog.csdn.net/weixin_72357342/article/details/137910913?spm=1001.2014.3001.5502


📌了解更底层的list实现

        由于在之前C语言模拟实现双向带头循环链表的时候我们已经对链表的结构和操作进行的详细的解析,所以本篇博客的侧重点将会是C++的语法特性,而不会很细致的深入探究链表在操作上的结构特性,如果有对链表操作的底层原理和逻辑感兴趣的朋友可以先移步更偏底层逻辑实现的C语言实现双向循环链表的文章:

【数据结构】C语言实现带头双向循环链表万字详解(附完整运行代码)icon-default.png?t=N7T8https://blog.csdn.net/weixin_72357342/article/details/134513792?spm=1001.2014.3001.5502

该图为文章部分节选

二.list迭代器和vector迭代器的异同

📌迭代器的分类

        迭代器是一种对象允许顺序访问其元素而无需暴露底层表示。根据其访问方式和特性,迭代器可以被分为不同类型。这里我们介绍三种主要的迭代器:单向迭代器、双向迭代器随机迭代器

🎏单向迭代器(Forward Iterator)

        单向迭代器只允许顺序访问元素只能从头到尾进行迭代。它不能返回到先前的元素。C++的forward_list、unordered_map、unordered_set等数据结构的迭代器都可以被视作单向迭代器。

特点:

  • 只能向前移动,即只能进行++操作。
  • 没有回退功能。

🎏双向迭代器(Bidirectional Iterator)

        双向迭代器允许在迭代时向前和向后移动可以在遍历过程中回退。这种迭代器常用于需要访问前一个元素的场景。C++的list、map、set等数据结构的迭代器都可以被视作双向迭代器。

特点:

  • 支持向前和向后移动,即支持 ++ / -- 操作.
  • 能够在迭代过程中返回到先前的元素。

🎏随机迭代器(Random Access Iterator)

        随机迭代器允许在任意顺序中访问元素可以直接通过索引访问这种迭代器在数组等数据结构中常见,因为它们支持快速随机访问。C++的vector、string、deque等数据结构的迭代器都可以被视作随机迭代器。

特点:

  • 可以跳转到任何元素,即可以进行 ++ / -- / + / - 操作。
  • 对每个元素的访问时间复杂度通常是 O(1)。

📌list和vector的底层实现差异对迭代器的影响

        我们知道,vector的底层实现是传统顺序表的形式,这种形式下对象的指针天然的就是迭代器,并且是随机迭代器,它可以通过物理的++ / -- / + / - 来精确定位要找到的元素位置.

        vector底层示意图:

        但是由于list在底层实现是带头双向循环链表的形式,这种形式下对象的指针就无法承担起迭代器的功能,因为它在物理内存中 ++ / -- 的结果是未知的内存块,我们想要的是对迭代器 ++ / -- 后它就能自动的指向链表的下一个/前一个结点,因此想要达到迭代器的效果,就只能自己将结点指针封装成一个类,然后重载结点指针的 ++ / -- / != /的执行逻辑,使其可以正常完成迭代器的功能,后面我们会着重分析并搭建迭代器的结构.

        list底层示意图:


三.逐步实现项目功能模块及其逻辑详解

        通过第一部分对项目功能的介绍,我们已经对list的功能有了大致的了解,虽然看似需要实现的功能很多,貌似一时间不知该如何下手,但我们可以分步分模块来分析这个项目的流程,最后再将各部分进行整合,所以大家不用担心,跟着我一步一步分析吧!


!!!注意,该部分的代码只是为了详细介绍某一部分的项目实现逻辑,故可能会删减一些与该部分不相关的代码以便大家理解,需要查看或拷贝完整详细代码的朋友可以移步本文第四部分。


📌分析list的组成结构

        我们在之前C语言阶段就已经一起模拟实现过带头双向循环链表,可以知道C语言中带头双向循环链表的结构是由两部分组成的,一部分是链表结点,一部分是链表本身.因此我们至少要封装两个类模板才能够组成带头双向循环链表.

        但是在C++中,由于STL引入了迭代器,并且因为list的结点指针在空间上不像vector那样连续,不能承担起迭代器的功能,所以我们必须将结点指针封装起来,设计一个迭代器类模板来完成迭代器相关操作的重载以便完成迭代器的功能.

        综上所述,我们在本篇中要实现的结构如下图所示:

      根据前面的分析,我们可以先搭建一个代码框架(该部分删除了大量的注释,只是为了把框架先拎出来给大家参考一下,后续会在具体实现部分有详解): 

namespace mfc 
{
	template<class T>//链表结点类模板

    //struct定义结点就直接是公有的,如果用class就要将其设为public
	struct list_node 
	{
        //成员变量
		list_node<T>* _next;
		list_node<T>* _prev;
		T _val;

        //构造函数
		list_node(const T& val = T()){}
	};


	template<class T,class Ref,class Ptr>//链表迭代器类模板
	struct __list_iterator
	{
        //成员变量
		typedef list_node<T> Node;
		typedef __list_iterator<T, Ref, Ptr> self;
		Node* _node;

        //构造函数
		__list_iterator(Node* node){}

		//操作符重载函数
		Ref operator*(){}
		Ptr operator->(){}
		
		self& operator++(){}
		self operator++(int){}
		self& operator--(){}
		self operator--(int){}

		bool operator!=(const self& it) const{}
		bool operator==(const self& it) const{}
	};


	template<class T>//链表类模板
	class list
	{
		typedef list_node<T> Node;

        //成员变量
    private:
		Node* _head;

        //成员函数
	public:
		typedef __list_iterator<T,T&,T*> iterator;
		typedef __list_iterator<T,const T&,const T*> const_iterator;
		
        //迭代器相关函数
		iterator begin(){}
		iterator end(){}
		const_iterator begin() const{}
		const_iterator end() const{}

        //默认成员函数
		void empty_init(){}
		list(){}
		list(const list<T>& lt){}
		void swap(list<T>& lt){}
		list<T>& operator=(list<T> lt){}
		~list(){}

        //成员函数
		void clear(){}
		void push_front(const T& x){}
		void pop_front(){}
		void push_back(const T& x){}
		void pop_back(){}
		iterator insert(iterator pos, const T& x){}
		iterator erase(iterator pos){}
		size_t size(){}
	};
}

📌实现list结点类模板

🎏构造list结点类成员变量

        list结点的成员比较简单,我们在第一部分也已经大致介绍过了,即:

        带头双向循环链表结点(Node)需要包含三个成员:前指针域prev,数据域val,后指针域next.结点(Node)逻辑结构图示如下:

        这里还有一个小的点需要提一下,我们在这里实现的list_node类,后续是要给list类使用的,
考虑到后续我们在链表的操作函数中会有直接操作结点成员变量的情况,如:

prev->_next = newnode; //相当于在list_node外部直接通过类指针访问了类成员变量_next

        基于class的封装特性,class的成员变量一般都是默认为私有的,如果我们要允许其他类直接访问class的成员变量和函数,就要将其都设置为public,或者通过友元/内部类来解决成员访问问题.

        既然如此,我们不妨直接使用struct定义结点成员变量和函数,因为struct定义的类的成员变量和函数默认就是公有,完全可以满足我们的需求.

        综上所述,该部分代码如下:

template<class T>//链表结点类模板

struct list_node 
{
	list_node<T>* _next;
	list_node<T>* _prev;
	T _val;
};

🎏实现list结点类构造函数

        list结点的构造函数我们实现两个即可,一个是有参构造,一个是无参构造,而无参构造又可以通过给缺省值的方式和有参构造合二为一,所以我们用初始化列表来实现一下list_node的构造函数:

//缺省值的作用是在无参调用时直接去调用模板实例化的类的无参构造函数
//这里一定不能将缺省值给0/nullptr!因为你不知道模板实例化的类具体到底是内置类型还是自定义类型
list_node(const T& val = T())
	:_next(nullptr)
	,_prev(nullptr)
	,_val(val)
{}

📌实现list迭代器类模板

🎏构造list迭代器类成员变量

        list迭代器类的目的是改造并封装结点指针,使其行为符合我们期望的迭代器行为,所以list迭代器的成员变量就是list结点的指针:

struct __list_iterator
{
    typedef list_node<T> Node;
    //这里typedef是为了防止我们把成员类型写成list_node*
    //对于类模板来说,类名不是类型,类名实例化出的类才是类型
    //即 list_node 不是类的类型 而 list_node<int> 才是类的类型

    Node* _node;
};

🎏实现list迭代器类构造函数

        list的迭代器构造的情况都是将一个已存在的结点指针传给iterator,使之成为list的迭代器变量,不存在无参构造的情况,因此迭代器的构造都是接收一个Node*的指针参数,然后将它赋值给_node成员变量即可,代码如下:

__list_iterator(Node* node)
	:_node(node)
{}

🎏实现operator*重载函数

        对迭代器解引用的操作是为了获取list结点里面的数据,所以我们重载的operator*函数要完成的就是返回给调用函数这个迭代器指向结点的数据_val,代码如下:

//const迭代器的设计让这里的返回值又套了一个模板参数Ref
//在这里不理解没关系,我们最后实现const迭代器时会详细讲
//在这里可以先认为根据模板的实例化不同 Ref = T& / const T&

Ref operator*()
{
	return _node->_val;
}

🎏实现前置operator++重载函数

        迭代器前置++的功能是使迭代器指向下一个链表结点,并返回下一个链表的结点,要指向下一个结点,其实就是迭代器的结点指针改为_node->_next,代码如下:

//因为我们把__list_iterator也写成类模板了,而返回值必须写的是类模板实例化的类型
//即__list_iterator<T, Ref, Ptr>
//为了方便,我们不妨typedef一下这个为self 方便后续用


//++前置
self& operator++()
{
	//更新一下*this的_node指针,然后再返回*this
	_node = _node->_next;
	return *this;
}

🎏实现后置operator++重载函数

        迭代器后置++的功能是使迭代器指向下一个链表结点,并返回链表没有++前的结点,要指向下一个结点,其实就是迭代器的结点指针改为_node->_next,代码如下:

//后置++
self operator++(int)
{
	self tmp(*this);

	//更新一下*this的_node指针,然后再返回*this当前的值
	_node = _node->_next;

	return tmp;
}

🎏实现前置operator--重载函数

        迭代器前置--的功能是使迭代器指向上一个链表结点,并返回上一个链表的结点,要指向上一个结点,其实就是迭代器的结点指针改为_node->_prev,代码如下:

//--前置
self& operator--()
{
	//更新一下*this的_node指针,然后再返回*this
	_node = _node->_prev;

	return *this;
}

🎏实现后置operator--重载函数

        迭代器后置--的功能是使迭代器指向上一个链表结点,并返回没有--前链表的结点,要指向上一个结点,其实就是迭代器的结点指针改为_node->_prev,代码如下:

//后置--
self operator--(int)
{
	self tmp(*this);

	//更新一下*this的_node指针,然后再返回*this
	_node = _node->_prev;

	return tmp;
}

🎏实现operator!=重载函数

        判断两个迭代器是否不相等,就是判断它们是否不指向同一个结点,即地址是否不相同,代码如下:

bool operator!=(const self& it) const
{
	return _node != it._node;
}

🎏实现operator==重载函数

        判断两个迭代器是否相等,就是判断它们是否指向同一个结点,即地址是否相同,代码如下:

bool operator==(const self& it) const
{
	return _node == it._node;
}

🎏实现operator->重载函数

        我们先来看一下下面代码里库里的list的使用上有没有什么特别的地方:

        可以看到,对迭代器it而言,下面两行代码的结果是一样的,都是取出成员变量x:

it->x ;
(*it).x ;

        但是如果把这段代码用我们实现的list来完成,就会出错:

        再看一下 . 操作符 ->操作符的定义:

📖 . 操作符

  • 用法:用于直接访问对象的成员(属性或方法)。
  • 适用场景:当你有一个对象的实例时,使用 . 操作符来访问其成员。

📖 -> 操作符

  • 用法:用于通过指向对象的指针来访问对象的成员。
  • 适用场景:当你有一个对象的指针时,使用 -> 操作符来访问其成员。

         综上所述,我们还需要实现 operator-> ()函数,以便可以通过迭代器直接访问Node类型的成员_val,对迭代器而言, operator->()函数的作用就是通过迭代器指针直接返回结点Node的数据_val的地址.以便->操作符能够通过operator->()函数的返回值访问_val的成员,代码如下:

//const迭代器的设计让这里的返回值又套了一个模板参数Ptr
//在这里不理解没关系,我们最后实现const迭代器时会详细讲
//在这里可以先认为根据模板的实例化不同 Ptr = T* / const T*

Ptr operator->()
{
	return &_node->_val;
}

        可能有朋友不理解为什么上面的函数里的 it->x 有些奇怪,分析一下好像写成it->->x才对,我画图帮大家分析并解释一下原因:


🎏实现const修饰的list迭代器类模板

        我们在实现list迭代器时有一个无法忽视的问题,就是要实现正常对象迭代器和const修饰对象迭代器,先来看一下我们以往在实现vector的时候是怎么实现const迭代器的:

        先画图演示一下两种const修饰指针的区别,可见上图中vector的const迭代器显然是第一张图示的情况:

        我们创建const迭代器的目的就是保证const修饰的对象在使用迭代器时不能通过迭代器*或者->的访问修改其指向的数据值.那么我们要实现的list的迭代器当然是第一种const修饰的指针,那么我们list就直接套用vector的const迭代器的写法可以吗?像下面这样?

        答案是绝对不可以的,简单画图分析一下大家就明白了:

        基于这个原因,我们没法通过直接使用const来修饰list迭代器的方式来获得const_list迭代器,只能寻找其他的方法来实现迭代器.

        通过前面的分析,我们知道,我们要实现的const迭代器就是要实现迭代器指向的_val是不能改变的.那我们为什么不在解引用重载函数operator*()部分直接让它的返回值是const修饰的类型引用呢?就像下面这样:

const T& operator*()
{
	return _node->_val;
}

        同理,operator->()函数也可以通过限制其返回值来限制_val不能被更改:

const T* operator->()
{
	return &_node->_val;
}

        只要完成这两个限制,我们就可以达到const修饰迭代器的要求,那么再写一个一模一样的类模板,只是operator*()函数和operator->()函数的返回值和普通迭代器不同就可以了.

        但是,既然我们都用类模板了,为什么不顺便把这两个合成一下,写成一个多参数的类模板?当我们使用普通迭代器时,参数就传< T , T& , T* > ; 当我们使用const迭代器时,参数就传

< T , const T& , const T* > . 综上所述,list迭代器部分完整代码如下:

//迭代器的本质是通过自定义类型的封装,改变了类型的行为
//内置类型的行为不符合我们的要求时,C++就可以用一个类来封装这个类型,然后自己定义这个类型的行为
//这里的思路是,我们本身就想用结点的指针来做list的迭代器,但是因为list的结构原因
//它的++ / -- / * / != 等操作得到的结果不是我们想要的
//所以我们就把这个结点指针封装起来,重新定义它的++ / -- / * / != 等操作
//使这些操作得到的结果是我们想要的

template<class T,class Ref,class Ptr>
struct __list_iterator
{
	typedef list_node<T> Node;
	typedef __list_iterator<T, Ref, Ptr> self;

    //成员变量
	Node* _node;

    //成员函数
	__list_iterator(Node* node)
		:_node(node)
	{}

	Ref operator*()
	{
		return _node->_val;
	}

	Ptr operator->()
	{
		return &_node->_val;
	}

	//++前置
	self& operator++()
	{
		//更新一下*this的_node指针,然后再返回*this
		_node = _node->_next;
		return *this;
	}

	//后置++
	self operator++(int)
	{
		self tmp(*this);
		//更新一下*this的_node指针,然后再返回*this
		_node = _node->_next;
		return tmp;
	}

	//--前置
	self& operator--()
	{
		//更新一下*this的_node指针,然后再返回*this
		_node = _node->_prev;
		return *this;
	}

	//后置--
	self operator--(int)
	{
		self tmp(*this);
		//更新一下*this的_node指针,然后再返回*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类模板

🎏构造list类成员变量

        list类成员变量组成比较简单,就是一个哨兵位的头结点指针(如果介意size()函数每次调用O(n)的时间复杂度,那么可以在这里再加一个_size成员,后续size()函数只需要返回这个成员变量就行,但是每个更改链表结点个数的函数都要记得更新_size,这里我们就只是实现一个O(n)的size()函数吧)

        代码如下:

template<class T>
class list
{
	typedef list_node<T> Node;
public:
    /*
	因为iterator和const_iterator只有operator*和operator->的返回值不同
	所以我们不如把operator*和operator->的返回值也设置成模板参数,
	当是iterator的时候,返回值模板就是T&/T*
    当是const_iterator的时候,返回值模板就是const T&/const T*
	本质上iterator和const_iterator还是两个类,但是用模板可以简化一些
    */

	typedef __list_iterator<T,T&,T*> iterator;
	typedef __list_iterator<T,const T&,const T*> const_iterator;

private:
    Node* _head;
};    

🎏实现list类empty_init()函数

        因为后面无参构造函数和拷贝构造函数都需要创建一个哨兵位的头结点,并初始化其成员,我们直接将这个操作封装成一个empty_init()函数,代码如下:

void empty_init()
{
	_head = new Node;
	_head->_prev = _head;
	_head->_next = _head;
}

🎏实现list类无参构造函数

        list类无参构造函数主要功能是创建一个哨兵位的头结点,并初始化其成员.这个代码块我们之前已经封装成了一个函数,直接调用就行,代码如下:

list()
{
	empty_init();
}

🎏实现list类拷贝构造函数

        拷贝构造函数的功能是深拷贝一个一模一样的链表,那我们第一步是创建一个新的链表头结点,再把原链表的结点一个一个拷贝链接在新链表上就可以了,代码如下:

list(const list<T>& lt)
{
	empty_init();

	for (auto& e : lt)
	{
		push_back(e);
	}
}

🎏实现list类swap()函数

        swap()函数的功能是交换两个list类,只需要交换两个类里的成员变量即可,我们前面实现时成员只有_head,那就直接交换_head就行,如果有加了_size成员的朋友记得还要交换_size,代码如下:

void swap(list<T>& lt)
{
	std::swap(_head, lt._head);
}

🎏实现list类operator=()函数

        我们在string和vector的模拟实现中都使用过一种简单的赋值操作符重载方式,思路就是直接把形参lt的值直接拷贝给*this,这样*this里的值就是我们想要赋给的值了,代码如下:

list<T>& operator=(list<T> lt)
{
	swap(lt);
	return *this;
}

🎏实现list类clear()函数

        clear()函数的作用就是清空链表中的所有结点,但不包括头结点.我们按顺序将链表中的结点逐一删除即可,代码如下:

void clear()
{
	iterator it = begin();
	while (it != end())
	{
		it = erase(it);
	}
}

🎏实现list类析构函数

        析构函数的作用就是销毁整个链表,其执行逻辑实际上就是删除所有结点+销毁头结点,我们复用一下clear()再销毁头结点就可以,代码如下:

~list()
{
	clear();

	delete _head;
	_head = nullptr;
}

🎏实现list类begin()函数

🌳实现普通list类的begin()函数

        begin()函数就是要返回链表第一个有效结点的迭代器,实际上就是头结点后一个结点的指针,因为迭代器的底层实现是Node*,所以这里直接返回Node*,不强转成迭代器类型也没问题,因为单参数的构造函数支持隐式类型转换,代码如下:

iterator begin()
{
	//return _head->_next;
	//上下两种方式都可以
	return (iterator)_head->_next;
}

🌳实现const修饰list类的begin()函数

        const修饰list的begin()函数和普通list类的begin()函数的区别就是函数接收的是const修饰的参数,返回的是const修饰的迭代器,代码如下:

const_iterator begin() const
{
	return (const_iterator)_head->_next;
}

🎏实现list类end()函数

🌳实现普通list类的end()函数

       end()函数就是要返回链表最后一个有效结点后一个结点的迭代器,实际上就是头结点的指针,因为迭代器的底层实现是Node*,所以这里直接返回Node*,不强转成迭代器类型也没问题,因为单参数的构造函数支持隐式类型转换,代码如下:

iterator end()
{
	return (iterator)_head;
}

🌳实现const修饰list类的end()函数

        const修饰list的end()函数和普通list类的end()函数的区别就是函数接收的是const修饰的参数,返回的是const修饰的迭代器,代码如下:

const_iterator end() const
{
	return (const_iterator)_head;
}

🎏实现list类insert()函数

        这里插入的逻辑如下图:

        不同之处在于C++插入的位置参数和返回的位置都是迭代器类型,具体代码逻辑见代码注释,代码如下:

//pos位置之前插入
iterator insert(iterator pos, const T& x)
{
	//纪录下pos位置和pos的上一个位置
	Node* cur = pos._node;
	Node* prev = cur->_prev;

	//创建新结点
	Node* newnode = new Node(x);

	//将pos的上一个结点和新结点相互链接
	prev->_next = newnode;
	newnode->_prev = prev;

	//将新结点和pos结点相互链接
	cur->_prev = newnode;
	newnode->_next = cur;

	//返回新插入的结点的迭代器
	return newnode;
}

🎏实现list类erase()函数

        这里删除的逻辑如下图:

        不同之处在于C++删除的位置参数和返回删除后下一个结点的位置都是迭代器类型,具体代码逻辑见代码注释,代码如下:

//删除pos位置的结点
iterator erase(iterator pos)
{
	//不能删哨兵位的头结点
	assert(pos != end());

	//纪录下pos位置, pos的上一个位置和下一个位置
	Node* cur = pos._node;
	Node* prev = cur->_prev;
	Node* next = cur->_next;

	//将pos的上一个位置和下一个位置相互链接
	prev->_next = next;
	next->_prev = prev;

	//删除pos位置结点
	delete cur;

	//返回pos位置的下一个结点的迭代器
	return next;
}

🎏实现list类push_front()函数

        push_front()函数就是在第一个结点之前插入结点,我们直接复用insert()函数即可,代码如下:

void push_front(const T& x)
{
	insert(begin(), x);
}

🎏实现list类pop_front()函数

        pop_front()函数就是删除第一个结点,我们直接复用erase()函数即可,代码如下:

void pop_front()
{
	erase(begin());
}

🎏实现list类push_back()函数

        push_back()函数就是在最后一个结点之后插入结点,即头结点之前插入结点,我们直接复用insert()函数即可,代码如下:

void push_back(const T& x)
{
	insert(end(), x);
}

🎏实现list类pop_back()函数

        pop_back()函数就是删除最后一个结点,即删除头结点之前的那个结点,我们直接复用erase()函数即可,代码如下:

void pop_back()
{
	erase(--end());
}

🎏实现list类size()函数

        size()有两种求法:

  1. 如果list没有成员变量_size,就写一个函数手动遍历一遍计算一下有多少个结点,该方法时间复杂度是O(n).
  2. 如果有成员变量_size,就可以直接返回_size的值.

         我们这里采用第一种方式,代码如下:

        
size_t size()
{
	size_t sz = 0;
    
    //通过迭代器遍历链表
	iterator it = begin();
	while (it != end())
	{
		++sz;
		++it;
	}

	return sz;
}

四.项目完整代码

         因为模板定义和声明不能分离,所以我们将程序运行的代码分别在两个工程文件中编辑,完整代码如下:

test.cpp文件

//仅测试list功能用
#include"STL_List.h"

int main()
{
	mfc::test_list1();
	mfc::test_list2();

	return 0;
}

  list.h 文件

#pragma once
#include<iostream>
#include<assert.h>
using namespace std;

namespace mfc 
{
	template<class T>
	//链表的链和结点分装成两个类
	struct list_node //struct定义结点就直接是公有的,如果用class就要将其设为public
	{
		list_node<T>* _next;
		list_node<T>* _prev;
		T _val;

		list_node(const T& val = T())
			:_next(nullptr)
			,_prev(nullptr)
			,_val(val)
		{}
	};

	template<class T,class Ref,class Ptr>
	//迭代器的本质是通过自定义类型的封装,改变了类型的行为

	//内置类型的行为不符合我们的要求时,C++就可以用一个类来封装这个类型,然后自己定义这个类型的行为
	//这里的思路是,我们本身就想用结点的指针来做list的迭代器,但是因为list的结构原因
	//它的++ / -- / * / != 等操作得到的结果不是我们想要的
	//所以我们就把这个结点指针封装起来,重新定义它的++ / -- / * / != 等操作
	//使这些操作得到的结果是我们想要的
	struct __list_iterator
	{
		typedef list_node<T> Node;
		typedef __list_iterator<T, Ref, Ptr> self;
		Node* _node;

		__list_iterator(Node* node)
			:_node(node)
		{}

		//const迭代器的设计
		Ref operator*()
		{
			return _node->_val;
		}

		Ptr operator->()
		{
			return &_node->_val;
		}


		//++前置
		self& operator++()
		{
			//更新一下*this的_node指针,然后再返回*this
			_node = _node->_next;
			return *this;
		}

		//后置++
		self operator++(int)
		{
			self tmp(*this);
			//更新一下*this的_node指针,然后再返回*this
			_node = _node->_next;
			return tmp;
		}


		//--前置
		self& operator--()
		{
			//更新一下*this的_node指针,然后再返回*this
			_node = _node->_prev;
			return *this;
		}

		//后置--
		self operator--(int)
		{
			self tmp(*this);
			//更新一下*this的_node指针,然后再返回*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:
		//因为iterator和const_iterator只有operator*和operator->的返回值不同
		//所以我们不如把operator*和operator->的返回值也设置成模板参数,
		//当是iterator的时候,返回值模板就是T&/T*
		//当是const_iterator的时候,返回值模板就是const T&/const T*
		//本质上iterator和const_iterator还是两个类,但是用模板可以简化一些
		typedef __list_iterator<T,T&,T*> iterator;
		typedef __list_iterator<T,const T&,const T*> const_iterator;
		
		iterator begin()
		{
			//因为迭代器的底层实现是Node*,所以这里直接返回Node*没问题
			//单参数的构造函数支持隐式类型转换
			//return _head->_next;
			//所以上下两种方式都可以
			return (iterator)_head->_next;
		}

		iterator end()
		{
			return (iterator)_head;
		}

		const_iterator begin() const
		{
			return (const_iterator)_head->_next;
		}

		const_iterator end() const
		{
			return (const_iterator)_head;
		}

		void empty_init()
		{
			_head = new Node;
			_head->_prev = _head;
			_head->_next = _head;
		}


		list()
		{
			empty_init();
		}

		list(const list<T>& lt)
		{
			empty_init();

			for (auto& e : lt)
			{
				push_back(e);
			}
		}

		void swap(list<T>& lt)
		{
			std::swap(_head, lt._head);
		}

		list<T>& operator=(list<T> lt)
		{
			swap(lt);
			return *this;
		}

		~list()
		{
			clear();

			delete _head;
			_head = nullptr;
		}

		void clear()
		{
			iterator it = begin();
			while (it != end())
			{
				it = erase(it);
			}
		}


		void push_front(const T& x)
		{
			insert(begin(), x);
		}
		void pop_front()
		{
			erase(begin());
		}

		void push_back(const T& x)
		{
			/*
			//找尾
			Node* tail = _head->_prev;
			Node* newnode = new Node(x);
			
			//链接
			tail->_next = newnode;
			newnode->_prev = tail;
			newnode->_next = _head;
			_head->_prev = newnode;
			*/
			insert(end(), x);
		}

		void pop_back()
		{
			erase(--end());
		}

		//pos位置之前插入
		iterator insert(iterator pos, const T& x)
		{
			//纪录下pos位置和pos的上一个位置
			Node* cur = pos._node;
			Node* prev = cur->_prev;

			//创建新结点
			Node* newnode = new Node(x);

			//将pos的上一个结点和新结点相互链接
			prev->_next = newnode;
			newnode->_prev = prev;

			//将新结点和pos结点相互链接
			cur->_prev = newnode;
			newnode->_next = cur;

			//返回新插入的结点的迭代器
			return newnode;
		}

		//删除pos位置的结点
		iterator erase(iterator pos)
		{
			//不能删哨兵位的头结点
			assert(pos != end());

			//纪录下pos位置, pos的上一个位置和下一个位置
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* next = cur->_next;

			//将pos的上一个位置和下一个位置相互链接
			prev->_next = next;
			next->_prev = prev;

			//删除pos位置结点
			delete cur;

			//返回pos位置的下一个结点的迭代器
			return next;
		}

		//size求法1:写函数
		//方法2:加一个成员变量_size.然后insert就++,erase就--,初始化记得附0
		size_t size()
		{
			size_t sz = 0;
			iterator it = begin();
			while (it != end())
			{
				++sz;
				++it;
			}
			return sz;
		}


	private:
		//写成list_node* _head;是错误的
		//因为有了类模板后,类名就不是类型了,只有类模板显示实例化出的类才是类型
		Node* _head;
	};

	//迭代器测试打印函数
	void Print(const list<int>& lt)
	{
		list<int>::const_iterator it = lt.begin();

		while (it != lt.end())
		{
			cout << *it << " ";
			++it;
		}
		cout << endl;
	}
	
	//测试函数1
	void test_list1()
	{
		list<int> lt;

		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);

		list<int>::iterator it = lt.begin();//这个地方把begin()赋给it是拷贝构造
		//而iterator没有实现拷贝构造,则默认就是浅拷贝
		//没有出错的原因就是,这里本身就是应该浅拷贝
		//我把begin()赋值给it,本身就是希望it指向这个结点,而不是指向这个结点的拷贝
		//那么,多个指针指向一个结点没有崩溃的原因是,我们没有进行二次释放操作
		//一般来讲,指针的销毁都伴随着空间的销毁,但是迭代器并没有销毁链表结点的权力
		//所以我们不写迭代器的析构函数,迭代器的销毁不导致结点的释放,所以不会有二次释放的问题
		while (it != lt.end())
		{
			cout << *it << " ";
			++it;
		}
		cout << endl;

		for (auto e : lt)
		{
			cout << e << " ";
		}
		cout << endl;

		Print(lt);
	}

	//测试函数2
	void test_list2()
	{
		class Point {
		public:
			int x;
			int y;
			Point(int xCoord = 0, int yCoord = 0)
				: x(xCoord)
				, y(yCoord)
			{}
		};

		list<Point> lt;
		lt.push_back(Point(1, 1));
		lt.push_back(Point(2, 2));
		lt.push_back(Point(3, 3));
		lt.push_back(Point(4, 4));
		list<Point>::iterator it = lt.begin();
		while (it != lt.end())
		{
			cout << it->x << "," << it->y << endl;
			cout << (*it).x << "," << (*it).y << endl;
			cout << endl;
			++it;
		}

	}
}

结语

希望这篇list的实现详解能对大家有所帮助,欢迎大佬们留言或私信与我交流.

学海漫浩浩,我亦苦作舟!关注我,大家一起学习,一起进步!

相关文章推荐

【C++】模拟实现vector

【C++】标准库类型vector

【C++】模拟实现string类

【C++】构建第一个C++类:Date类

【C++】类的六大默认成员函数及其特性(万字详解)

【C++】什么是类与对象?【C++】什么是类与对象?


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

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

相关文章

SSH实现电脑VScode免密登录到虚拟机其原理

在网上想看一下这个原理。发现写的还是比较乱&#xff0c;所以自己总结了一份方便回顾 SSH免密登录的原理主要基于非对称密钥加密技术&#xff0c;比较常用的是RSA算法。 以下是SSH免密登录的详细步骤和原理&#xff1a; 1. 生成密钥对 在客户端上生成一对密钥&#xff0c;…

系统复习Java日志体系

一&#xff0c;我们采用硬编码体验一下几个使用比较多的日志 分别导入几种日志的 jar 包 <?xml version"1.0" encoding"UTF-8"?><project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSch…

【已解决】YOLOv8加载模型报错:super().__init__(torch._C.PyTorchFileReader(name_or_buffer))

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 《------往期经典推…

C#——Json数据存储

本文使用的软件为VS2022&#xff0c;不同的软件使用上有些许差异。 C#数据存储 关于数据存储&#xff0c;一般在退出控制台之后&#xff0c;暂存的数据都会释放掉&#xff0c;有没有什么方法能够在下一次进入的时候还能加载上一次的数据呢&#xff1f;答案是有的&#xff0c;关…

利用Arcgis设置分式标注(分子分母标注)

因工作需要&#xff0c;需要设置分式标注&#xff0c;下面详细介绍下如何利用arcgis 设置分式标注&#xff0c;以下操作以供参考&#xff0c;如有疑义可提出。 一、准备工作 软件&#xff1a;arcmap 示例数据&#xff1a;行政区shp矢量图 二、操作步骤 1.添加数据 将行政区sh…

Golang | Leetcode Golang题解之第307题区域和检索-数组可修改

题目&#xff1a; 题解&#xff1a; type NumArray struct {nums, tree []int }func Constructor(nums []int) NumArray {tree : make([]int, len(nums)1)na : NumArray{nums, tree}for i, num : range nums {na.add(i1, num)}return na }func (na *NumArray) add(index, val …

MinIO安装(思路、方法、步骤(以centos8安装为例))

MinIO安装&#xff08;思路、方法、步骤&#xff08;以centos8安装为例&#xff09;&#xff09; 简介 MinIO 是一个高性能、开源的对象存储系统&#xff0c;旨在提供简单、可扩展的存储解决方案&#xff0c;特别适用于大数据、AI 和 ML 应用。 步骤记录 首先我们进入官网看…

前端使用 Konva 实现可视化设计器(19)- 连接线 - 直线、折线

本章响应小伙伴的反馈&#xff0c;除了算法自动画连接线&#xff08;仍需优化完善&#xff09;&#xff0c;实现了可以手动绘制直线、折线连接线功能。 请大家动动小手&#xff0c;给我一个免费的 Star 吧~ 大家如果发现了 Bug&#xff0c;欢迎来提 Issue 哟~ github源码 gitee…

最全个人笔记【Makefile】

1. 基本概念 1.1 make是什么 当一个项目中要编译的文件很多时&#xff0c;手工使用编译器一个个进行编译&#xff0c;很明显不具有可操作性&#xff0c;此时必须借助某些软件&#xff0c;协助我们有序地、正确地自动编译整个工程的所有该编译的文件。这样的软件被称为 工程管…

Simulink|基于粒子群算法的永磁同步电机多参数辨识

目录 主要内容 模型研究 结果一览 下载链接 主要内容 仿真程序参考文献《改进粒子群算法的永磁同步电机多参数辨识》&#xff0c;采用粒子群算法与simulink模型结合的方式&#xff0c;对永磁同步电机进行多参数辨识。程序以定子绕组电阻、d轴电感、q轴电感和永磁…

ai写作免费版工具上哪找?一文详解5大ai写作神器

面对写作难题&#xff0c;你是否曾感到力不从心&#xff1f;从创意枯竭到语法错误&#xff0c;每个写作环节都可能成为挑战。但如今&#xff0c;有了ai写作工具的帮助&#xff0c;这些难题都能迎刃而解。今天&#xff0c;就让我们一起来看看ai写作免费网页版应该怎么选吧&#…

Win11系统文件资源管理器鼠标右键卡顿解决方法

引用链接&#xff1a; Windows 11文件资源管理器崩溃怎么解决&#xff1f;看看这7个解决办法&#xff01;

订单搜索分页查询业务

文章目录 概要整体架构流程技术细节小结 概要 订单搜索分页查询是电商、物流、零售等众多行业中的常见需求&#xff0c;主要用于管理和分析大量订单数据. 需求分析以及接口设计 技术细节 1.Controller层: 根据接口设计来写 ApiOperation("订单搜索")GetMapping(…

使用plink和git进行数据处理

首先使用git进行plink环境配置&#xff0c;显示环境安装成功&#xff0c;在此环境下可以使用plink 在基因型数据处理过程中&#xff0c;看到vcf文件后首要做的就是将vcf文件转成二进制文件&#xff0c;输入命令 plink --vcf genotype.vcf --allow-extra-chr --recode --out tes…

滑动窗口代码实现

public int minSubArrayLen(int target, int[] nums) {int len nums.length;int res len 1;//最大是len&#xff0c;如果最后res结果还是n1说明没有答案返回0int sum0;int left0;for(int right0; right<len; right){sumnums[right];while(sum>target){//不需要判断rig…

算法 —— 递推

目录 递推 数楼梯 斐波那契数列 一维数组递推 P1002 过河卒 二维数组递推 P1044 栈 卡特兰数 递推 将一个很大的任务分解成规模小一些的子任务&#xff0c;子任务分成更小的子任务&#xff0c;直到遇到初始条件&#xff0c;最后整理归纳解决大任务的思想就是递推与递…

Kafka基本概念,工作流程介绍

1、消息队列与Kafka 1.1、Kafka简介 Kafka使用scala开发&#xff0c;支持多语言客户端&#xff08;c、java、python、go等&#xff09; Kafka最先由LinkedIn公司开发&#xff0c;之后成为Apache的顶级项目。 Kafka是一个分布式的、分区化、可复制提交的日志服务 LinkedIn使…

麒麟系统查看和修改ip

查看ip ifconfig ifconfig enp0s3 192.168.1.110

ROS中自定义头文件和源文件

今天分享一下如何在ROS中进行头文件和源文件的调用。案例是实现输出"Hello World" 本文的头文件(即.h文件)放在include/${project_name}路径下, 源文件和可执行文件则在src文件夹下 第一步&#xff1a;创建工作空间&#xff0c;创建包&#xff0c;使用vscode打开工作…

mysql高级语句的查询语句

一、排序语法&#xff0c;关键字排序 升序和降序 默认的排序方式就是升序 升序&#xff1a;ASC 降序&#xff1a;DESC 配合语法&#xff1a;order by 语法 1、升序 select * from info order by name; 根据名字升序排序&#xff0c;不需要加ASC select * from info order…