C++8:模拟实现list

本文详细介绍了如何从零开始实现一个简单的双向链表容器mylist,包括节点结构、构造函数、push_back、迭代器、增删查改功能,以及const迭代器和赋值操作符重载的优化。通过这个实现,读者可以更好地理解STL中的list容器的工作原理。
摘要由CSDN通过智能技术生成

目录

最基础的链表结构以及迭代器实现

链表节点结构

构造函数

push_back

list的迭代器

增删查改功能实现

insert

erase

pop_front        

pop_back

push_front

clear

默认成员函数

析构函数

拷贝构造函数

赋值操作符重载

 list的完善

 const迭代器

赋值操作符重载优化

swap

size

类型名问题

->重载问题


list容器的本身其实是一个双向带头循环链表,具体的结构其实并不复杂本人曾在学习数据结构时有所记述,有需要的读者可以跳转至这篇文章数据结构4:双向链表+OJ题_lanload的博客-CSDN博客_双向链表题

那么既然是一个链表,那就少不了节点指针以及节点结构,模拟实现list也需要套入模板,那么我们先实现一个最基本具有数据存放功能的链表来试试看。

最基础的链表结构以及迭代器实现

链表节点结构

     一个链表结构,需要下一个节点的指针,存放当前数据的变量,和上一个节点的指针
    那么作为一个容器,使用模板必不可少。

	template<class T>
	struct List_node
	{
		T _data;
		List_node* _prev;
		List_node* _next;
        
        //节点结构的构造函数,处理数据的传入
		List_node(const T& x)
			:_data(x)
			, _next(nullptr)
			, _prev(nullptr)
		{}

	};

构造函数

  无参构造函数
  由于需要设定节点结构内部的变量,不走初始化列表

namespace mylist
{
    template<class T>
    class mylist
    {
    public:
		typedef List_node<T> Node;
		mylist()
		{
			_head = new Node(T());

			//_head->_data = T();匿名构造防止自定义类型偷家.不过这一步NEW已经做过了
			_head->_prev = _head;
			_head->_next = _head;
		}


    };
}

Node* == List_node<T>
list的基础结构是一个带头双向循环链表,那么有一个哨兵位非常合理

private:
    Node* _head;

push_back

  • 双向带头循环链表不需要考虑边边角角,直接尾插
  • 需要一个尾部节点,尾节点的next指向新节点,哨兵位头节点的prev指向新节点,新节点的next指向头节点

		void push_back(const T& val)
		{
			Node* newnode = new Node(val);

			Node* tail = _head->_prev;

			tail->_next = newnode;
			newnode->_next = _head;
			newnode->_prev = tail;
			_head->_prev = newnode;

		}

list的迭代器

 上文我们已经实现了一个具有最基础插入数据功能的链表,但是还差一个访问方式,那么我们就创建一个迭代器。

但是list的迭代器不能同vector不包装直接实现:

  1. 由于不同于连续容器支持连续的指针访问,链表的迭代器不能使用原生指针
  2.  这也就导致了我们实现迭代器的时候需要重新封装一个类,这个类用于获取mylist的指针节点
  3.  所以需要重写++以及*运算符,使得这个类的运作方式形同原生指针

迭代器的结构类
这个类的作用其实很像一个打包袋,既然我们没法使用原生指针来实现++和解引用,那么在原生指针上面套上一个类,就可以间接的实现这些功能了


	template <class T>
	struct _list_iterator
	{    

        //节点类型重定义
		typedef List_node<T> Node;

		//把原生指针装进打包袋之前,需要一个空袋子
		Node* _pnode;


	};

为了很方便的直接把节点指针放进这个类里面,我们直接使用构造函数,然后把指针装进去,非常简单,用构造函数走初始化列表,不走也成能装进去就行

 _list_iterator( Node* p)
      :_pnode(p)
       {}

那么一个迭代器的标准访问还需要解引用以及++的功能才能迭代访问,这些也实现一下,实现的逻辑已用注释给出

	template <class T>
	struct _list_iterator
	{
		typedef List_node<T> Node;

//把原生指针装进打包袋之前,需要一个空袋子
		Node* _pnode;

//然后把指针装进去,非常简单,用构造函数走初始化列表,不走也成能装进去就行
		_list_iterator( Node* p)
			:_pnode(p)
			{}

//运算符重载,*解引用,传进来一个节点,返回节点里面的值
		T& operator*()
		{
			return(_pnode->_data);
		}


//运算符重载,++指向下一个节点,返回下一个节点
//这里为什么返回_list_iterator<T>?因为我们包装了这个原生指针,但是不需要得到里面的东西
//只需要让它往下走一个节点然后返回就行,并且能实现链式访问
		_list_iterator<T>& operator ++()
		{
			_pnode = _pnode->_next;
			return *this;

		}


	};

 重写完了这个类还没有结束,我们还需要给出begin的接口以及end的接口。在这里我们重定义迭代器类的名称以方便使用

		typedef _list_iterator<T> iterator;

		iterator begin()
		{
			return iterator(_head->_next);
		}

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

试试效果,使用迭代器和范围for进行访问

 没有问题。

增删查改功能实现

  • 链表的增删查改,insert,earse,pop_back,pop_front,push_front,clear,析构,拷贝构造,赋值操作符重载,其中的尾插尾删头插头删都可以借由insert以及erase便捷实现,其余的实现也不算困难

insert

		void insert(iterator pos, const T& val)
		{
			//先创建一个节点
			Node* newnode = new Node(val);
//这里需要节点的指针,但是iterator并不能解引用,理解稍微有点不到位,被存放的指针拿去初始化这个类了
			Node* cur = pos._pnode;
			
			Node* prev = cur->_prev;

			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;

		}

erase

 删除,然后链接被删除的节点,但是需要注意的是别把哨兵位节点给干掉了

        iterator erase(iterator pos)
		{
			assert(pos != end());

			Node* cur = pos._pnode;
			Node* prev = cur->_prev;
			Node* next = cur->_next;


			prev->_next = next;
			next->_prev = prev;

			delete pos._pnode;

			return iterator(next);
		}

pop_front        

		//头删
		void pop_front()
		{
			erase(begin());//等价于this->erase( this-> begin())
		}

pop_back

		//尾删
		void pop_back()
		{
			erase(--end());
		}

push_front

    //头删
		void pop_front()
		{
			erase(begin());//等价于this->erase( this-> begin())
		}

clear

		void clear()
		{

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

默认成员函数

析构函数

        ~mylist()
		{
			clear();

			delete _head;
			_head = nullptr;
		}

拷贝构造函数

 拷贝构造的传参我们暂时不使用const修饰,因为还没有实现const迭代器,我们后文实现

		mylist(mylist<T>& l2)
		{
			//我们先置空

			_head = new Node(T());

			_head->_next = _head;
			_head->_prev = _head;

			iterator il2 = l2.begin();
			while (il2 != l2.end())
			{
				push_back(*il2);
				++il2;
			}
		}

赋值操作符重载

		//赋值操作符重载
		mylist<T>& operator = ( mylist<T>& l2)
		{
			if (this != &l2)
			{
				clear();

				for (auto& e : l2)
				{
					push_back(e);
				}
			}
			return *this;
		}

 list的完善

 const迭代器

  •  我们前文提到过,之所以不使用const对象做拷贝构造函数的参数是因为没有实现conts对象的迭代器,那么为什么没实现就不能用呢?

在这里,先再次回顾一次const迭代器的作用,当一个类生成一个对象的时候,可以分为普通对象以及conts对象,普通对象可读可写,const对象只能读不能写,那么针对const对象的这个特性,一部分成员函数以及迭代器都需要额外的实现const版本,不然const对象无法调用对应的成员函数。

回到话题上来,在尝试给拷贝构造的参数加上const之后,程序就报错了

 那么根据以上的报错,我们很可能会借助前车之鉴也就是vector的const迭代器来尝试改写成如下形式以修复bug

 但其实这样子与const迭代器的使用目的不相同,const修饰一个变量的时候有两种形式

const T* p1;//1
T* const p2;//2
  • 代码1的const保护的是p1指针所指向的对象,而p1本身则可以被修改
  • 代码2的const保护的则是p2这个指针本身,其对象依旧可以被修改。
  • 我们希望const迭代器遵从的修饰规则是代码1,也就是保护所指向的对象,但是我们的迭代器是一个被封装好的类,我们对其加上了const只能让迭代器类本身不能被修改。
  • const迭代器和普通迭代器都可以解引用以及执行++操作区别则在于const迭代器并不能对解引用出来的值进行修改

归根结底:使用const做传递参数,需要额外实现针对const对象而编写的const迭代器以支持访问,但其实同普通的迭代器所实现的成员函数相比,const迭代器的不同则体现在解引用时的返回值的不同。普通迭代器返回T&     而const迭代器需要返回const T&

 那么很简单我们直接上手,对症下药不就好了吗

		T& operator*()
		{
			return(_pnode->_data);
		}

		const T& operator*() const
		{
			return(_pnode->_data);
		}

诶,这样不就解决了吗?const对象调用下面的const成员函数,普通对象调用上面的!完美!

但很显然我们忽略了++这个问题,但你可能会想:“那有什么难的?跟上面一样再重载一个不就成了吗?”

但++的逻辑我们回顾一下

 这个_pnode可是不能动的!没法重写。


所以综上所述:以上的实现方法,可以解引用,但是不能++,这还是有悖于我们的const迭代器没有实现对应功能的问题。

那么我们其实可以尝试多实现一个类,称之为_mylist_const_itreator,其中与当前的迭代器中唯一的区别就是解引用的返回值不同即可,其他的除去类名都不需要变换,当我们需要使用const迭代器的时候使用_mylist_const_itreator这个类名替代即可,不仅能++还能解引用。

	template<class T>
	struct _list_const_iterator
	{
		typedef list_node<T> node;
		node* _pnode;

		_list_const_iterator(node* p)
			:_pnode(p)
		{}

		const T& operator*()
		{
			return _pnode->_data;
		}

		_list_const_iterator<T>& operator++()
		{
			_pnode = _pnode->_next;
			return *this;
		}

		_list_const_iterator<T>& operator--()
		{
			_pnode = _pnode->_prev;
			return *this;
		}

		bool operator!=(const _list_const_iterator<T>& it)
		{
			return _pnode != it._pnode;
		}
	};

但是这样冗杂的实现方法虽然说可以成立,但是对于STL的实现大佬来说这肯定是不合格的,那么库里是如何实现的?

我们先回顾一个概念:不同的模板参数,会生成不同的类

举例,对于vector而言:

vector<int>
vector<string>
vector<vector<int>>

如上的三种不同的模板参数,生成了3个不同的类。

那么大佬则是利用了这个特点,不同的模板参数,同一个类型,生成不同的对象

	template <class T,class Ref>
	struct _list_iterator

先额外加一个模板参数,Ref是reference(引用)的英文。你可能会觉得奇怪,多加一个模板参数可以改变现状吗?答案是完全可以,而且非常巧妙,只需要加上以下的语句就可以了。

typedef _list_iterator<T,T&> iterator;
typedef _list_iterator<T, const T&> const_iterator;
  • 我们分析这段语句就可以发现其巧妙之处,借助不同的模板参数生成不同的类这个特质,相当于以一个类特化生成了两个不同的类,当我们使用const迭代器的时候,const T&会直接被模板参数套用生成一个const版本的迭代器,我们只需要再完善迭代器类内部的名称就可以实现这种特化了。

在额外的添加了一个模板参数之后,_list_iterator<T>&这个语句段的模板参数里面也需要额外加上一个Ref,因为之后还需要添加一个模板参数,我们直接typedef一下迭代器来方便更改。

	template <class T,class Ref>
	struct _list_iterator
	{
		typedef List_node<T> Node;
		typedef _list_iterator<T,Ref> Self;


		//把原生指针装进打包袋之前,需要一个空袋子
		Node* _pnode;

		//然后把指针装进去,非常简单,用构造函数走初始化列表,不走也成能装进去就行
		_list_iterator( Node* p)
			:_pnode(p)
			{}


		Ref operator*()
		{
			return(_pnode->_data);
		}

		Self& operator ++()
		{
			_pnode = _pnode->_next;
			return *this;

		}

		Self& operator --()
		{
			_pnode = _pnode->_prev;
			return *this;

		}

		bool operator != (const Self& it)
		{
			return _pnode != it._pnode;
		}


	};

赋值操作符重载优化

 前文所记述的赋值操作符重载是“传统写法”

		//赋值操作符重载
		mylist<T>& operator = ( mylist<T>& l2)
		{
			if (this != &l2)
			{
				clear();

				for (auto& e : l2)
				{
					push_back(e);
				}
			}
			return *this;
		}

 为了实现“现代写法”也就是“摇人打工法”我们需要自己先实现list自己的成员函数swap

swap

swap的逻辑非常简单,不必一个个的交换节点,既然我们有哨兵位头节点,我们直接交换他俩即可完成交换

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

为什么不直接使用算法库内部的swap来直接交换两个链表?

因为算法库的算法消耗还是比较大的,毕竟为了适配所有类型的交换,消耗远大于我们自己单纯的交换两个指针。

那么我们的赋值操作符重载就非常简单了,注意需要使用传值传参,触发拷贝构造


		mylist<T>& operator = ( mylist<T> tmp)
		{
			swap(tmp);
			return *this;
		}

那为什么拷贝构造不使用“现代写法”?

我们在实现vector的现代拷贝构造的时候,为了防止当前的this指针交换过去的时候析构一个没有初始化过的指针,我们会给予一个空指针来防止此事的发生,那么套用到list这上面反而就不行了,因为我们在迭代器里面还是需要访问到当前头节点的,也就是_head不可以为空。还是需要初始化,那么跟我们的传统写法差比不大,实现也可没有也罢。

size

size的实现非常简单,我们直接复用就好了,在mylist的成员变量之中加入一个size,由于我们复用了insert和erase来构筑了mylist的增删查改,我们只需要在触发了insert和erase的时候对size++或者--就可以了。

		size_t size()
		{
			return _size;
		}

不过由于复用了push_back一类函数在拷贝构造以及赋值操作符重载内部,其中size的变化还需要额外处理,也就是赋值时也需要更新被赋值的变量的_size。这一部分不做记述,逻辑简单也不复杂。

类型名问题

我们查阅官方文档的时候,可能会对赋值操作符的重载产生一定的疑惑

 啥玩意?list后面的模板参数怎么没了?

在解释这个问题之前我们再次回顾一边类和对象中类型的问题。

  • 普通类的类名 == 类型
  • 类模板的类名 !=  类型   而类模板<模板参数> == 类型

那么根据我们上面的回顾,这里应该是list<T>才对,怎么官方文档是这样子的呢?那么换成我们自己实现的试一试

  •  也是没有问题的,这里其实算是C++语言设计的一个陷阱,对于赋值操作符重载来说,类模板的名称也被归为了类型,这其实并不符合我们的使用习惯,但是从语法角度来讲是合理的。

不过平常我们能不用就不用,毕竟还是容易造成歧义。


->重载问题

我们创建一个自定义类型来尝试我们的list能否存储和读取

	struct Location
	{
		Location(int x = 0,int y = 0)
			:_x(x),_y(y)
		{}


		int _x;
		int _y;
	};

当我们想要遍历访问这个类的时候,由于我们没有重载这个类的流插入运算符,将会报错

 但是重载一个流插入未免有些麻烦,Location的成员变量是内置类型,也是一个结构体,那么我们简单一点直接取出来访问就好,那么一个结构体访问其中的成员变量非常简单,使用->是我们常用的手段。

  • 但是->一般是用于结构体指针的,我们还没有重载,但是p->data等价于(*p).data,所以使用(*it)._x这个语句是可行的。

 不过这样子的可读性还是比较差的,所以我们还是需要重载->操作符以方便我们访问结构体。

  • 既然->作用于一个结构体指针,那么我们就直接把存放于当前节点的数据指针捞出来即可,那么我们在迭代器类里面重载一下,把它的地址捞出来放到指针里头去
		T* operator->()
		{
			return &_pnode->_data;
		}

那么根据我们的理解,整个过程应该是这样的:

it->返回的是当前节点里面的值的地址也就是T*,根据我们当前的程序,返回的应该是Location*,那么我们想要取得里面的数据就应该再加上一个->,因为原生的结构体指针已经提取出来了,使用->是可以的

为了验证我们的猜想,我们先上一个->试一试,此时应该提取出的是当前值的指针

 怎么回事?不应该是指针吗,怎么直接提取出了其中的值?

原因则是:在这段过程中,如果按照我们理论上的语法来实现,应该写成it->->_x 才可以,但是这样子的写法可读性太低了,所以编译器自己优化了这个过程,实际上确实是使用了两次->操作符,但是我们写一次即可

那么这样就万事大吉了?我们不能忘记const对象的问题。

		T* operator->()
		{
			return &_pnode->_data;
		}

我们的确对普通对象的->进行了重载,但是当我们调用const迭代器的时候,这段代码可不会返回const T* 而是T* ,const对象随便更改这种情况绝对不是我们想看到的。

那么前文我们对模板的灵活运用在这里就可以再次发光发热了,我们多加上一个模板参数称之为Ptr


	template <class T,class Ref,class Ptr>
	struct _list_iterator
	{
		typedef List_node<T> Node;
		typedef _list_iterator<T,Ref,Ptr> Self;
    }
		typedef List_node<T> Node;
		typedef _list_iterator<T,T&,T*> iterator;
		typedef _list_iterator<T, const T&,const T*> const_iterator;
		Ptr operator->()
		{
			return &_pnode->_data;
		}

 这样const就成功的生效了。


到这,一个仿照STL具有基础功能的list就实现完毕了,希望对你有点帮助!

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值