C++中List的实现

前言

数据结构中,我们了解到了链表,但是我们使用时需要自己去实现链表才能用,但是C++出现了list将这一切皆变为现。list可以看作是一个带头双向循环的链表结构,并且可以在任意的正确范围内进行增删查改数据的容器。list容器一样也是不支持下标访问的,主要也是物理空间并不连续。

list的部分重要接口实现

一样的,在实现之前,我们要对list的结构有一个大概的框架

在此基础就很容易理解list的内部是一个个节点,该节点的类型也都是自定义类型,而模版参数是节点中val的类型。代码框架如下:

namespace cr
{
	template<class T>
	struct ListNode
	{
		ListNode(const T& x = T())//T()匿名对象
			:val(x)
			,pre(nullptr)
			,next(nullptr)
		{}
		T val;
		struct ListNode<T>* pre;
		struct ListNode<T>* next;
	};

	template<class T>
	class list
	{
		typedef ListNode<T> Node;//对类进行重命名

	public:
		
	private:
		Node* _head;//自定义类型成员的指针

	};

}

1.迭代器实现

cr::list<int>::iterator it = lt.begin();
while (it != lt.end())//lt.end()临时对象
{
	cout << *it << endl;
	it++;
}

先看这段代码,该代码也就是我们想要最终实现的结果,所以我们想知道对迭代器进行哪些操作,可以先从需要实现的功能进行分析。可以初步知道迭代器"八成"是节点的指针(节点是自定义类型),所以*it和it++这两个操作铁定是需要进行运算符重载的,*it实际就是得到节点类的成员变量val的值,而it++就是通过指针的偏移,指向下一个节点。而最重要的是应该知道*it和it++操作对象是迭代器而不是list实例化出来的对象(lt),像begin和end函数面向的就是list实例化的对象,所以可以直接在list类中实现。如果*it和it++也在在list类中实现的话,默认传参传的就是list实例化对象的地址,最后默认由this指针接受。而我们需要的是对迭代器进行操作,固然需要this指向迭代器的指针。所以应该将迭代器封装起来到一个类里里更理想的状态。


所以迭代器封装成类的话,那么肯定需要的成员对象就是节点指针,而成员函数就一定有operator*和operator++。其实operator!=是最容易忽略的,仔细想想迭代器类也是一个自定义类型,这么可以直接去判断呢,所以operator!=也是要实现的,所以下边整理了需要实现的运算符重载函数以及其他的一些。

代码如下:

template<class T>
struct _List_iterator
{
	typedef ListNode<T> Node;

	_List_iterator(Node* node)
		:_pnode(node)
	{}

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

	_List_iterator<T> operator++()//前置++
	{
		_pnode = _pnode->next;
		return *this;
	}

	_List_iterator<T> operator++(int)//后置++
	{
		_List_iterator tmp = *this;
		_pnode = _pnode->next;
		return tmp;
	}

	_List_iterator<T> operator--()
	{
		_pnode = _pnode->pre;
		return *this;
	}

	_List_iterator<T> operator--(int)
	{
		_List_iterator tmp = *this;
		_pnode = _pnode->pre;
		return tmp;
	}

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

    T* operator->()
	{
		return &_pnode->val;//一般用于存自定义类型对象时,会隐藏一个箭头
	}

	Node* _pnode;
};


这里operator->的实现你可能会有疑问,举例下列代码:

class A
{
public:
	A(int a=0,int b=0)
		:_a(a)
		,_b(b)
	{}
	int _a;
	int _b;
};
int main()
{
	cr::list<A> a;
	a.push_back(A(1, 1));//创建匿名对象初始化
	a.push_back(A(2, 2));
	a.push_back(A(3, 3));

	return 0;
}

面对这种情况,list实例化的是一个类的对象的话,而且想要打印出该类中成员变量的话,就是下面的方法:

cr::list<A>::iterator it = a.begin();
while (it != a.end())
{
	cout << (*it)._a << (*it)._b << endl;
	it++;
}

这里的(*it)就是list中的pnode->val也就是这个A类,但是并没有重载流运算符,所以就只能去访问里面的数据(公有),所以就是通过(.)运算符,既然可以用(.)那么肯定也可以用(->)所以就重载了(->)运算符,而(->)有点不一样:

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

返回值的类型的是A*一个类型,也就是返回一个指针,所以在调用(->)运算赋不得加上两个(->)了吗,一个是调用运算符重载函数返回A*,另一个是指针访问符,所以为了增强可读性就省略一个(->)

cr::list<A>::iterator it = a.begin();
while (it != a.end())
{
	cout << it->_a << it->_b << endl;
	it++;
}

以上两种写法实现结果都是一样的:



而对于begin和end函数面向的就是list实例化的对象,那么说就是在list类中实现就行

iterator begin()
{	
	//return iterator(_head->next);//匿名对象
	return _head->next;
}
iterator end()
{
	//return iterator(_head);//匿名对象
	return _head;
}

因为迭代器封装的类是单参数,所以可以直接返回节点的指针(会自动调用迭代器的构造函数)

2.const迭代的实现

首先我们要了解list中const迭代器与非const迭代器的区别,const迭代器中的成员变量指向的数据是一个节点的指针,并不是说const修饰了以后这个节点不能改变,实际上是节点中的val成员变量不能改变。假如说是节点不能改变的话,那么就连pre和next这两个成员变量也不可以改变,如果连这两个都不能改变的话,那么还怎么实现operator++,还怎么遍历链表中的数据呢。

所以凭这一点,const迭代器就不可能是直接在iterator前面加上const限定符修饰那么容易。

而想要val的值不能改变的话,实际上就是operator*和operator->这两个函数的返回值要加const引用来修饰


**方法一:**再写一个_List_const_iterator的类

对于_List_terator类而言_List_const_iterator这个类只需要改一下类型而已,像operator*和operator->的返回值加上const修饰而其他需要返回迭代器的地方改成_List_const_iterator<T>就行,最后在list类中typedef _List_const_iterator<T> const_iterator就完成了


**方法二:**通过模版实现

方法一是需要再实现一个_List_const_iterator的类,但是本质上其实这个类相较于_List_iterator没什么区别,也就是返回值的类型有区别而已,所以此时对迭代器模版的引入就在合适不过了。而此时的模版参数肯定不能只有一个参数,而应该有三个参数:

template<class T,class ref,class ptr>

模版中第二个参数的实参其实就是const T&用于operator*运算符函数的返回值,而第三个参数的实参其实就是const T* 用于operator->函数的返回值。此时你可能会问为什么是需要这两个参数呢?其实判断方法很简单,就是通过实质上的比较const迭代器和非const迭代器实现时各函数返回值有哪些发生了改变。表面上你是只写了一个迭代器的类加一个模版就实现了两个迭代器干的活,实际上在编译的时候编译器通过模版参数自动生成了两个类,在使用时会优先找最匹配的调用。

所以这里提一点:模版中类名相同模版参数不同并不代表是同一个类型,类型=类名+模版参数

迭代器实现代码

template<class T,class ref,class ptr>
	struct _List_iterator
	{
		typedef ListNode<T> Node;

		_List_iterator(Node* node)
			:_pnode(node)
		{}

		ref operator*()
		{
			return _pnode->val;
		}
		ptr operator->()
		{
			return &_pnode->val;
		}

		_List_iterator<T,ref,ptr> operator++()
		{
			_pnode = _pnode->next;
			return *this;
		}
		_List_iterator<T,ref,ptr> operator++(int)
		{
			_List_iterator tmp = *this;
			_pnode = _pnode->next;
			return tmp;
		}
		_List_iterator<T,ref,ptr> operator--()
		{
			_pnode = _pnode->pre;
			return *this;
		}
		_List_iterator<T,ref,ptr> operator--(int)
		{
			_List_iterator tmp = *this;
			_pnode = _pnode->pre;
			return tmp;
		}
		bool operator!=(const _List_iterator<T,ref,ptr>& cmpnode)const
		{
			return _pnode != cmpnode._pnode;
		}

		Node* _pnode;
	};

其实上面的代码本质上就是将operator*和operator->的返回值虚拟化,然后在调用迭代器时才进行实例化的。

	template<class T>
	class list
	{
		typedef ListNode<T> Node;//对类进行重命名

	public:
		typedef _List_iterator<T,T&,T*> iterator;//实例化
		typedef _List_iterator<T, const T&, const T*> const_iterator;

		const_iterator begin()const
		{  
			//return iterator(_head->next);//匿名对象
			return _head->next;
		}
		const_iterator end()const
		{
			//return iterator(_head);//匿名对象
			return _head;
		}

        ......
    }

在list类中分别将两种类型迭代器重命名,所以在外面调用哪种迭代器时就会在内部模版实例化成具体的迭代器

3.list的插入与删除

        iterator insert(iterator pos,const T& x)
		{
			Node* l1 = pos._pnode->pre;
			Node* l2 = pos._pnode;
			Node* tmp = new Node(x);

			l1->next = tmp;
			tmp->pre = l1;
			tmp->next = l2;
			l2->pre = tmp;

			return tmp;
		}
		iterator erase(iterator pos)//调用之后pos节点失效
		{
			Node* l1 = pos._pnode->pre;
			Node* l2 = pos._pnode->next;

			l1->next = l2;
			l2->pre = l1;
			delete pos._pnode;

			return l2;
		}

        void push_back(const T& x)
		{
			insert(this->end(), x);
			//Node* tmp = new Node(x);
			//Node* tail = _head->pre;//找尾

			//tail->next = tmp;
			//tmp->pre = tail;
			//tmp->next = _head;
			//_head->pre = tmp;
		}

这里的插入删除同链表一样,没什么好说的,唯一要注意的就是迭代器失效的问题,list的insert不存在扩容换址,所以不会造成迭代器失效,但是list的erase就不同了,earase会释放当前的节点,所以调用之后就会失效。所以给erase一个返回值,返回需要删除的节点的下一个节点。

4.list的构造加赋值

        list()
			:_head(nullptr)
		{
			_head = new Node;//该节点也是一个类,创建时调用构造函数
			_head->next = _head;
			_head->pre = _head;
		}
		list(const list<T>& copy)
			:_head(nullptr)
		{
			_head = new Node;//该节点也是一个类,创建时调用构造函数
			_head->next = _head;
			_head->pre = _head;

			for (auto it : copy)//内部实际是用迭代器
			{
				push_back(it);
			}
		}
        list<T>& operator=(list<T> copy)//调用拷贝构造
		{
			std::swap(_head, copy._head);
			
			return *this;
		}

因为list容器类似带头双向循环链表,所以构造时要new一个带头节点,其次要注意的是new的这个节点的类型是我们自定义的类型,所以在new时就会自动调用该节点的构造函数将其空间内容初始化。赋值函数就实现的比较巧妙,通过传参调用拷贝构造函数的深拷贝,然后再换值就行。

5.空间释放析构函数

        void clear()
		{
			list::iterator del = this->begin();
			while (del != this->end())
			{
				del = erase(del);
			}
			_size = 0;
		}
		~list()
		{
			clear();
			delete _head;
			_head = nullptr;
		}

若有不恰当的描述欢迎留言指正!

相关推荐
杜杜的man1 分钟前
【go从零单排】迭代器(Iterators)
开发语言·算法·golang
亦世凡华、1 分钟前
【启程Golang之旅】从零开始构建可扩展的微服务架构
开发语言·经验分享·后端·golang
神仙别闹8 分钟前
基于MFC实现的赛车游戏
c++·游戏·mfc
小c君tt16 分钟前
MFC中 error C2440错误分析及解决方法
c++·mfc
测试界的酸菜鱼16 分钟前
C# NUnit 框架:高效使用指南
开发语言·c#·log4j
GDAL16 分钟前
lua入门教程 :模块和包
开发语言·junit·lua
李老头探索17 分钟前
Java面试之Java中实现多线程有几种方法
java·开发语言·面试
CSXB9919 分钟前
三十四、Python基础语法(文件操作-上)
开发语言·python·功能测试·测试工具
web Rookie38 分钟前
JS类型检测大全:从零基础到高级应用
开发语言·前端·javascript
很楠不爱1 小时前
Qt——窗口
开发语言·qt