C++stl_list 链表

链表早在我们学习数据结构的时候就已经学习过了,我之前在学习数据结构的时候和我上一篇vector一样是用c语言实现的,那个时候写的代码又臭又长,要满足许多接口;

future/my_road - 码云 - 开源中国 (gitee.com)

打开链接就可以找到我的stl模拟实现代码实现 

因为我们上一篇就讲了为什么要有stl容器的原因,这次我们就直接进入正题,我们直接来看我们的stl_list;

打个预防针:我们list中迭代器的运用非常巧妙要做好心里准备;

stl_list文档: 

cplusplus.com/reference/list/list/

 使用文档辅助学习效率更高哦!

我们的stl_list的结构是双向带头循环链表,这个链表的好处有很多,既方便我们遍历,也方便我们删除与插入的查找,更重要的是我们的尾插的效率大大提高了;

我们还是老样子先看它的成员变量与析构函数和无参的构造函数:

成员变量和无参的构造函数与析构函数

	template<class T>//节点的类
	struct list_node
	{
		T _data;
		struct list_node* _next;
		struct list_node* _prev;
		list_node(const T& data = T())
			:_next(nullptr)
			, _prev(nullptr)
		{
			_data = data;
		}
	};

template<class T>
class list
{
public:
	typedef list_node<T> node;
	typedef list_iterator<T,T&,T*> iterator;
	typedef list_iterator<T,const T&,const T*> const_iterator;
	void list_emptyinit()
	{
		_head = new node;
		_head->_next = _head;
		_head->_prev = _head;
	}
	list()
	{
		list_emptyinit();
	}

	~list()//考虑一下const对象的析构怎么办
	{
		clear();
		delete _head;
		_head = nullptr;
	}


private:
	node* _head=nullptr;
};

我们的list的成员变量就只有_head指针;它的类型是我们节点的指针类型,我们通过new节点类的方式调用了我们节点类(list_node)的构造函数,并在堆上开辟出来了节点这么大的空间,使得我们可以向其中存放数据;这样我们就有了我们的头节点;有了头节点之后我们要开始存放数据了;

list的插入删除函数

iterator erase(iterator pos)//在pos处删除
{
	assert(pos != end());
	node* next = pos._node->_next;
	node* prev = pos._node->_prev;
	next->_prev = prev;
	prev->_next = next;
	delete pos._node;
	return iterator(next);
}

void insert(iterator pos, const T& data)//在pos处插入
{
	node* newnode = new node(data);
	node* prev = pos._node->_prev;
	prev->_next = newnode;
	pos._node->_prev = newnode;
	newnode->_next = pos._node;
	newnode->_prev = prev;
}

void push_back(const T& data)//尾插
{
	insert(end(),data);
}

void push_front(const T& data)//头插
{
	insert(begin(),data);
}

void pop_back()//尾删
{
	erase(iterator(_head->_prev));
}

void pop_front()//头删
{
	erase(begin());
}

我们先看我们的插入函数,我们在pos位置插入数据时,我们需要先得到pos位置的迭代器,这里list的迭代器是什么我们待会再说,我们先把它理解成像指针一样的东西可以帮助我们找到pos位置;我们会在pos位置插入我们新new的节点;erase函数也是删除pos位置的节点(需要注意的是我们删除节点一定会导致迭代器失效,我们需要返回我们新的迭代器),这些都很好理解,参照我的模拟实现,应该能够很好的理解;现在剩下的疑问就是怎么传这个迭代器给我们的pos函数?迭代器到底是什么?

迭代器:

迭代器顾名思义就是用来迭代遍历的东西,它是怎么做到的呢?我们先回想一下我们之前的vector在gcc中它是通过原生指针写的迭代器,vs中通过封装类写迭代器;vector由于它的数据是在连续的空间中的,所以它可以使用原生指针的++直接遍历我们的vector,但是我们的list是不是无法使用我们的指针的++来直接遍历,list不是连续空间,如果我们list的迭代器是原生指针的话,我们++遍历是不是会让指针指向物理空间上我们节点后面一个位置,可是我们想要的是++后指针从我们这个节点指向我们的next指向的下一个节点,所以为了满足这个效果,我们需要封装我们的迭代器,实现++运算符重载,使得我们可以遍历list;

typedef list_iterator<T> iterator;
typedef const_list_iterator<T> const_iterator;


iterator begin()
{
	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);
}
	template<class T>
	struct list_iterator
	{
		typedef list_node<T> node;
		typedef list_iterator<T> iterator;
		node* _node;
		list_iterator(node* p)
			:_node(p)
		{}
		T& operator *()
		{
			return _node->_data;
		}

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

		iterator& operator ++()
		{
			return _node->_next;
		}

		iterator operator ++(int)
		{
			node* tmp = _node;
			_node= _node->_next;
			return tmp;
		}

		iterator& operator --()
		{
			return _node->_prev;
		}

		iterator operator --(int)
		{
			node* tmp = _node;
			_node = _node->_prev;
			return tmp;
		}

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

		bool operator==(const iterator& it)
		{
			return it._node == _node;
		}
	};
	template<class T>
	struct list_const_iterator
	{
		typedef list_node<T> node;
		typedef list_const_iterator<T> iterator;
		node* _node;
		list_const_iterator(node* p)
			:_node(p)
		{}
		const T& operator *()
		{
			return _node->_data;
		}

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

		iterator& operator ++()
		{
			return _node->_next;
		}

		iterator operator ++(int)
		{
			node* tmp = _node;
			_node = _node->_next;
			return tmp;
		}

		iterator& operator --()
		{
			return _node->_prev;
		}

		iterator operator --(int)
		{
			node* tmp = _node;
			_node = _node->_prev;
			return tmp;
		}

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

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

 我们先看类里面的实现,在我们的迭代器这个类中,我们的成员变量只有_node这一个成员变量,它的类型依然是节点的指针类型,我们可以通过它指向我们的list上的每一个节点;我们在迭代器的类中,我们重载了*和++等运算符,这些运算符使得我们的迭代器类拥有了我们自己定义的功能;这样我们就实现了一个非const类型的迭代器的类模板,它可以接受不同T类型的数据;

但是如果我们传递的是cosnt类型的成员变量的话会怎么样?const list<int> l;我们这样定义了一个l的list,我们使用迭代器来遍历它;我们的l是const对象;所以它只能使用const成员函数;我们就又写了一个list_const_iterator的迭代器模板,通过这个模板我们就实现了我们的const变量的迭代器;

可是我们这么写代码是不是非常的冗余呀?我们要写这么多代码,明明我们的const和非const迭代器类他们的差别就只有一个const T&和const T*的差别呀,所以我们的大佬非常牛逼!他们用一种巧妙的方法解决了这个问题;

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

iterator begin()
{
	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);
}
	template<class T,class Ref,class Ptr>
	struct list_iterator
	{
		typedef list_node<T> node;
		typedef list_iterator<T, Ref,Ptr> iterator;
		node* _node;
		list_iterator(node* p)
			:_node(p)
		{}
		Ref operator *()
		{
			return _node->_data;
		}

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

		iterator& operator ++()
		{
			return _node->_next;
		}

		iterator operator ++(int)
		{
			node* tmp = _node;
			_node= _node->_next;
			return tmp;
		}

		iterator& operator --()
		{
			return _node->_prev;
		}

		iterator operator --(int)
		{
			node* tmp = _node;
			_node = _node->_prev;
			return tmp;
		}

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

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

看到上面的代码,是不是非常惊诧,咦!为啥这个迭代器有三个模板参数呀,这是什么情况?不要着急,我们再回过头来看看我们传递给我们这个list_iterator的三个模板参数吧:

我们可以看到我们list中传输的是T&,T*和constT&,const T*为什么这么传呢?这是因为要满足我们的const变量的迭代器;我们重载的*和->运算符是不是分别需要返回T&和T*呢?const变量则需要的是const T&和const T*所以我们传了三个模板参数将我们的迭代器类const类型和非const类型写在了一起,这样精简了我们的代码;因为我们的写的是迭代器模板,我们只需要改变我们传递的参数就能使的我们的迭代器是实例化成我们需要的迭代器类;

而我们如果使用的list是const类型的话,它使用迭代器一定是需要调用我们的begin()和end()函数两个接口的,因为我们的list的成员变量_head是私有的我们一定得通过上面两个接口来获得我们的迭代器,而我们的begin和end又是两个函数,而函数的访问是会更具你list的类型来访问的,所以我们写的begin和end是有const类型和非const类型的;

这就是面向对象语言的魅力,我们的成员是被封装的我们是无法在类外面访问的!迭代器只能通过接口获取节点指针,而我们在类外面根本不需要懂底层,直接传递接口即可!太牛啦!

我的思考(这是在学习完map与set后写出的结论)这是给我自己看的思考

为什么不能直接使用一个变量T来接收const数据和非const数据呢?

这个问题我之前在实现的时候困扰了我非常非常久,最后我才想明白,原来是我们如果改变传递给iterator的T的话,那么在iterator中的typedef的node节点接收的也会是改变后的const T,这样在上层的list中我们的node节点还是T类型的,而在iterator中的node节点却是const T类型的这样我们通过模板实例化后获得的node节点类型是不同的,所以iterator的构造函数无法接收这样的参数也就无法构造出我们的iterator;在我们后面的map和set的iterator中实现了cosnt迭代器接收非cosnt迭代器参数的构造函数,从而使其可以形成iterator;但我们这里由于更改了传递给iterator的T为const T,所以我们没办法写出来一个非const 类型的T了,所以也无法像set中RBTree那样增加构造函数来修改了;

构造函数

void list_emptyinit()
{
	_head = new node;
	_head->_next = _head;
	_head->_prev = _head;
}
list()
{
	list_emptyinit();
}

list(size_t n, const T& data = T())
{
	list_emptyinit();
	for (int i = 0; i < n; i++)
	{
		push_back(data);
	}
}

list(iterator first, iterator last)
{
	list_emptyinit();
	while (first != last)
	{
		push_back(*first);
		first++;
	}
}

list(const_iterator first, const_iterator last)
{
	list_emptyinit();
	while (first != last)
	{
		push_back(*first);
		first++;
	}
}

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

list(const list<T>& lt)
{
	list_emptyinit();
	list<T> tmp(lt.begin(), lt.end());
	swap(tmp);
}

list &operator=(const list<T>&lt)
{
	list<T> tmp(lt.begin(), lt.end());
	swap(tmp);
	return *this;
}

过了迭代器那关之后我们看这样的构造函数就非常简单啦,上面就是list传递不同参数的几种构造函数有通过迭代器构造的有通过n和data来构造n个相同的data数据的,这些构造函数的理解应该阅读权威一些的文档看文档中的描述更有利于学习:

list::list - C++ Reference (cplusplus.com)

上面就是权威文档的解释;

以上就是对stl_list重要部分的讲解;

2023.11.10

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值