STL container - the simulation implementation of list (with detailed notes)

1. What is the list container?

C++ STL (Standard Template Library, Standard Template Library) provides a set of general-purpose template classes and functions for implementing commonly used data structures and algorithms. One of them is std::list, which implements a doubly linked list.

std::list is a container for storing a sequence of values. Unlike continuous storage containers such as arrays and vectors, std::list uses linked lists as the underlying data structure. Each node contains a value, and there are pointers to the previous node and the next node, and the nodes are connected through these pointers.

Due to the use of linked lists as the underlying structure, std::list has the following characteristics:

1. High insertion and deletion efficiency: the operation of inserting and deleting nodes in the linked list is very efficient, and the time complexity is O(1). Because it does not involve the movement of elements and reallocation of memory.

2. Random access is not supported: Since the elements in the linked list are not stored continuously, subscripts cannot be used to access elements randomly like arrays or vectors. It is necessary to move along the pointer from the start point or end point of the linked list to access the elements, which leads to low access efficiency.

3. Does not occupy continuous memory: Compared with continuous storage containers, linked lists do not require a continuous memory space, which makes it flexible to handle memory allocation and release.

std::list provides a series of member functions for inserting, deleting and accessing elements in the linked list, such as push_back(), push_front(), pop_back(), pop_front(), insert(), erase(), etc. Additionally, it supports iterators for traversing linked lists and accessing elements.

Second, the simulation implementation of list

2.1 NodeListNode

The list stores not only the value itself, but a node that uses a class to encapsulate the value. Because this is a two-way headed circular linked list, there must be next and prev pointers in the node, which is convenient for finding the next and previous of the node. node. The type of the node's value is indeterminate, so this is a class template.

	//类模板的模板参数
	template <class T>
	struct ListNode
	{
    
    
		ListNode<T>* _prev;
		ListNode<T>* _next;
		T _val;

		//需要提供构造函数,在新增节点时可以直接使用new,得到新结点
		//这里的缺省值不能简单地写成0,因为T是不确定的类型,0只能代表
		//整形,假如T是其它类型的话就不能是0了,所以我们要写成T类型的
		//匿名对象,这样编译器就会自动地根据T的类型构造匿名对象作为
		//缺省值了,如果T是自定义类型,编译器会自动调用默认构造函数
		//构造匿名对象的,内置类型也有构造函数,整形默认构造成0
		ListNode(const T& x = T())
			:_prev(nullptr)
			, _next(nullptr)
			, _val(x)
		{
    
    }

	};

2.2 Member variables

For the list, the most important member variable is the head node _head that needs a sentinel bit. This _head node does not contain valid data and is only used as the head of the linked list to facilitate subsequent operations such as insertion and deletion. Secondly, you can add a _size member to record the length of a linked list.

	private:
		Node* _head;
		size_t _size;

2.3 Four default member functions

2.3.1 Constructor

The purpose of the constructor is to complete the initialization of member variables. Obviously, the constructor here is to create a head node of a sentinel position.

		void emptyInit()
		{
    
    
			_head = new Node;

			//双向带头循环链表,没有元素的时候哨兵位的头节点
			//的_next和_prev都是指向自己本身的
			_head->_prev = _head;
			_head->_next = _head;
			_size = 0;
		}

		//构造函数
		list()
		{
    
    
		    //初始化
			emptyInit();
		}

2.3.2 Copy constructor

There is still the problem of deep and shallow copying. In order to avoid the nodes in the linked list being released twice, deep copying is still required, that is, create a new head node of the sentinel position, and then new each node of the copied linked list, and connect to this The head node of the new sentinel position goes up.

		//拷贝构造
		list(const list<T>& lt)
		{
    
    
			emptyInit();

			//const_iterator it = lt.begin();
			//while (it != lt.end())
			//{
    
    
			//	push_back(*it);
			//	++it;
			//}

			for (const auto& e : lt)
			{
    
    
				//把lt链表的所有节点都尾插到新的链表中即可
				push_back(e);
			}
		}

2.3.3 Assignment overloaded functions

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

		//赋值重载(现代写法)

		//利用传参的特性,传参时会调用拷贝构造函数构造一个临时的list,而这个
		//list的内容就是新的链表所需要的,所以直接交换两者的成员函数即可,即
		//交换哨兵位的头节点_head和_size即可
		list<T>& operator=(list<T> lt)
		{
    
    
			Swap(lt);

			return *this;
		}

2.3.4 Destructors

The destructor is to clean up the nodes in the linked list. We need to release the nodes of the linked list one by one, and finally release the head node _head of the sentinel position.

		void clear()
		{
    
    
			//需要一个一个地删除释放链表中的节点
			iterator it = begin();
			while (it != end())
			{
    
    
			    //erase之后迭代器会失效,必须接受返回值更新it才能继续删
				it = erase(it);
			}
			_size = 0;
		}

		~list()
		{
    
    
			//先清理释放链表中的有效节点
			clear();
			//再释放哨兵位的头节点_head
			delete _head;
			_head = nullptr;
		}

2.4 Iterator (key content)


The iterator of the list container is different from the previous string and vector, why? Through the properties of string and vector, we can find that the bottom layer of string and vector is implemented by array, and the array has continuous space, but the list is different. The bottom layer of list is connected by nodes one by one, not continuous Space, the requirement of the iterator is that the iterator ++ can point to the next element, and the dereference of the iterator "*" can find the object pointed to by the iterator.

For string and vector, the native pointer can make ++ point to the next element, and dereferencing can get the pointed object, but for list, ++ cannot make the iterator point to the next element, because the space is not continuous , Dereferencing can't get the value we want, but the node object. So for list we cannot directly use native pointers as iterators.

So how do we design this iterator? The reason why the list cannot use native pointers directly as iterators is because the pointer ++ of the linked list cannot find the address of the next node, and the dereference cannot get the value, so how do we find the next node for the linked list? Isn't that _node=_node->_next? That is to say, we want to make the behavior of iterator ++ no longer a simple pointer ++, but make the _node pointer point to the next one, but the behavior of the built-in type ++ is fixed, that is, for the built-in type + We cannot change the behavior of the + operation. At this time, the meaning of operator overloading in C++ is highlighted. We cannot modify the behavior of operators of built-in types, but we can customize the behavior of operators of user-defined types. , you can design whatever you want.

It can be seen from this that if we want to make the ++ or dereferencing operations of the iterator of the list the same as sting and vector, we need to use a custom class to encapsulate the pointer, and use operator overloading to control the ++ and dereferencing of the iterator. Behavior, design the same iterator as string and vector. So the iterators that seem to be the same on the upper layer are really different things at the bottom layer, and it is the complex encapsulation of the bottom layer that makes the upper layer apply the same rules, so you can’t just look at things on the surface! The following is the encapsulation of iterators.

	//T是T   Ref是T&/const T&   Ptr是T*/const T*
	//为什么要传三个模板参数,主要是为了同一份代码实现iterator和const_iterator
	//const_iterator是迭代器指向的内容不能修改,所以返回值需要是const版本的,
	//当Ref是const T&时,*返回的是const T&,是不能修改的,当Ptr是const T*时,
	//->返回值是const T*,指向的内容也是不能被修改的;符合const版本的迭代器。
	//如果Ref是T&,Ptr是T*,就是普通版本的迭代器,传三个参数的目的是让编译器
	//根据模板参数的类型实例化出普通迭代器和const迭代器
	template <class T,class Ref, class Ptr>
	struct __list_iterator
	{
    
    
		typedef ListNode<T> Node;
		typedef __list_iterator<T,Ref,Ptr> self;
		//list的迭代器只是对Node*指针进行了封装,形成了一个类,这样就可以
		//通过运算符重载来控制迭代器++,解引用等行为
		Node* _node;

	public:
		//迭代器的构造函数的参数只需要传一个指针即可
		__list_iterator(Node* ptr)
			:_node(ptr)
		{
    
    }

		//重载前置++,按照++的要求,迭代器++就指向下一个节点,所以
		//_node=_node->_next就指向了下一个节点
		self& operator++()
		{
    
    
			_node = _node->_next;
			return *this;
		}

		//重载后置++,为了与前置++区分,需要加一个参数做占位符
		//跟前置++一样的道理,只不过后置++的返回值是++前的结果
		//所以需要先保存当前迭代器,再让迭代器往后走
		self operator++(int)
		{
    
    
			self tmp(*this);
			_node = _node->_next;
			return tmp;
		}

		//重置前置--,--是找到前一个迭代器,所以_node=_node->_prev
		self& operator--()
		{
    
    
			_node = _node->_prev;
			return *this;
		}

		//与后置++原理类似,需要占位符做区分,并且需要先保存当前位置的迭代器
		self operator--(int)
		{
    
    
			self tmp(*this);
			_node = _node->_prev;
			return tmp;
		}

		//*解引用目的是拿到节点中的value值,所以以引用的方式返回_node->_val即可
		//对比原来的原生指针解引用拿到_node结构体,这里重载了*就能直接拿到value值
		//达到了和string和vector一样的效果
		Ref operator*()
		{
    
    
			return _node->_val;
		}

		//重载->,因为value也可能是一个自定义类型,箭头返回的是节点中value的地址,
		//可以通过->访问这个自定义类型的里的成员
		Ptr operator->()
		{
    
    
			return &_node->_val;
		}

		//必须带上const,因为调用operator!=的迭代器对象的参数是lt.end(),lt.end()
		//本身是通过传值返回的,是iterator的一份临时拷贝,具有常性,所以必须用const接收
		//比较两个迭代器是否相等,本质是比较迭代器里的_node指针是否相等
		bool operator!=(const self& it)
		{
    
    
			return _node != it._node;
		}

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

	};

So it becomes very simple to implement iterators in the list class.

		//双向带头循环链表的begin是哨兵位的头节点的_next
		iterator begin()
		{
    
    
			//return iterator(_head->_next);

			//单参数构造函数的类支持隐式类型的转换
			//可以直接这样写,编译器会自动调用iterator
			//的构造函数构造匿名对象返回
			return _head->_next;
		}

		//因为这是双向带头循环链表,而end又是指最后一个元素的下一个位置,
		//所以就是_head的位置
		iterator end()
		{
    
    
			return _head;
		}

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

		const_iterator end() const
		{
    
    
			return _head;
		}

2.5 insert function

		iterator insert(iterator pos, const T& x)
		{
    
    
			//先创建一个新结点
			Node* newNode = new Node(x);
			//先把迭代器转换成节点的指针Node*,方便操作
			Node* cur = pos._node;
			//记录前一个节点
			Node* prev = cur->_prev;

			//更改连接关系,这个就easy了
			prev->_next = newNode;
			newNode->_prev = prev;
			newNode->_next = cur;
			cur->_prev = newNode;

			//最后记得更新_size
			++_size;

			//同样存在隐式类型的转换
			return newNode;
		}

2.6 erase function

		iterator erase(iterator pos)
		{
    
    
			//断言,不能删除哨兵位的头节点
			assert(pos != end());

			//先把迭代器转换成Node*的指针,方便更改连接关系
			Node* cur = pos._node;

			//保存下一个节点
			Node* next = cur->_next;

			//保存前一个节点
			Node* prev = cur->_prev;

			//前一个节点与后一个节点连接起来即可
			prev->_next = next;
			next->_prev = prev;

			//释放要删除的节点
			delete cur;
			cur = nullptr;

			//最后记得--_size
			--_size;

			//同样存在隐式类型的转换
			return next;
		}

2.7 Tail plug tail delete, head plug delete function

		void push_back(const T& x)
		{
    
    
			//Node* newNode = new Node(x);
			//if (newNode)
			//{
    
    
			//	Node* tail = _head->_prev;
			//	tail->_next = newNode;
			//	newNode->_prev = tail;
			//	newNode->_next = _head;
			//	_head->_prev = newNode;
			//}

			//复用insert即可
			insert(end(), x);
			
		}

		void pop_back()
		{
    
    
			//assert(_head->_prev != _head);

			//Node* tail = _head->_prev;
			//Node* tailPrev = tail->_prev;
			//tailPrev->_next = _head;
			//_head->_prev = tailPrev;
			//delete tail;
			//tail = nullptr;

			//复用erase即可
			erase(--end());
		}

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

		void pop_front()
		{
    
    
			//复用
			erase(begin());
		}

3. List simulation implementation code summary

#pragma once

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

namespace kb
{
    
    
	//类模板的模板参数
	template <class T>
	struct ListNode
	{
    
    
		ListNode<T>* _prev;
		ListNode<T>* _next;
		T _val;

		//需要提供构造函数,在新增节点时可以直接使用new,得到新结点
		//这里的缺省值不能简单地写成0,因为T是不确定的类型,0只能代表
		//整形,假如T是其它类型的话就不能是0了,所以我们要写成T类型的
		//匿名对象,这样编译器就会自动地根据T的类型构造匿名对象作为
		//缺省值了,如果T是自定义类型,编译器会自动调用默认构造函数
		//构造匿名对象的,内置类型也有构造函数,整形默认构造成0
		ListNode(const T& x = T())
			:_prev(nullptr)
			, _next(nullptr)
			, _val(x)
		{
    
    }

	};


	//T是T   Ref是T&/const T&   Ptr是T*/const T*
	//为什么要传三个模板参数,主要是为了同一份代码实现iterator和const_iterator
	//const_iterator是迭代器指向的内容不能修改,所以返回值需要是const版本的,
	//当Ref是const T&时,*返回的是const T&,是不能修改的,当Ptr是const T*时,
	//->返回值是const T*,指向的内容也是不能被修改的;符合const版本的迭代器。
	//如果Ref是T&,Ptr是T*,就是普通版本的迭代器,传三个参数的目的是让编译器
	//根据模板参数的类型实例化出普通迭代器和const迭代器
	template <class T,class Ref, class Ptr>
	struct __list_iterator
	{
    
    
		typedef ListNode<T> Node;
		typedef __list_iterator<T,Ref,Ptr> self;
		//list的迭代器只是对Node*指针进行了封装,形成了一个类,这样就可以
		//通过运算符重载来控制迭代器++,解引用等行为
		Node* _node;

	public:
		//迭代器的构造函数的参数只需要传一个指针即可
		__list_iterator(Node* ptr)
			:_node(ptr)
		{
    
    }

		//重载前置++,按照++的要求,迭代器++就指向下一个节点,所以
		//_node=_node->_next就指向了下一个节点
		self& operator++()
		{
    
    
			_node = _node->_next;
			return *this;
		}

		//重载后置++,为了与前置++区分,需要加一个参数做占位符
		//跟前置++一样的道理,只不过后置++的返回值是++前的结果
		//所以需要先保存当前迭代器,再让迭代器往后走
		self operator++(int)
		{
    
    
			self tmp(*this);
			_node = _node->_next;
			return tmp;
		}

		//重置前置--,--是找到前一个迭代器,所以_node=_node->_prev
		self& operator--()
		{
    
    
			_node = _node->_prev;
			return *this;
		}

		//与后置++原理类似,需要占位符做区分,并且需要先保存当前位置的迭代器
		self operator--(int)
		{
    
    
			self tmp(*this);
			_node = _node->_prev;
			return tmp;
		}

		//*解引用目的是拿到节点中的value值,所以以引用的方式返回_node->_val即可
		//对比原来的原生指针解引用拿到_node结构体,这里重载了*就能直接拿到value值
		//达到了和string和vector一样的效果
		Ref operator*()
		{
    
    
			return _node->_val;
		}

		//重载->,因为value也可能是一个自定义类型,箭头返回的是节点中value的地址,
		//可以通过->访问这个自定义类型的里的成员
		Ptr operator->()
		{
    
    
			return &_node->_val;
		}

		//必须带上const,因为调用operator!=的迭代器对象的参数是lt.end(),lt.end()
		//本身是通过传值返回的,是iterator的一份临时拷贝,具有常性,所以必须用const接收
		//比较两个迭代器是否相等,本质是比较迭代器里的_node指针是否相等
		bool operator!=(const self& it)
		{
    
    
			return _node != it._node;
		}

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

	};

	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;

		void emptyInit()
		{
    
    
			_head = new Node;

			//双向带头循环链表,没有元素的时候哨兵位的头节点
			//的_next和_prev都是指向自己本身的
			_head->_prev = _head;
			_head->_next = _head;
			_size = 0;
		}

		//构造函数
		list()
		{
    
    
			//初始化
			emptyInit();
		}

		//拷贝构造
		list(const list<T>& lt)
		{
    
    
			emptyInit();

			//const_iterator it = lt.begin();
			//while (it != lt.end())
			//{
    
    
			//	push_back(*it);
			//	++it;
			//}

			for (const auto& e : lt)
			{
    
    
				//把lt链表的所有节点都尾插到新的链表中即可
				push_back(e);
			}
		}

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

		//赋值重载(现代写法)

		//利用传参的特性,传参时会调用拷贝构造函数构造一个临时的list,而这个
		//list的内容就是新的链表所需要的,所以直接交换两者的成员函数即可,即
		//交换哨兵位的头节点_head和_size即可
		list<T>& operator=(list<T> lt)
		{
    
    
			Swap(lt);

			return *this;
		}

		void clear()
		{
    
    
			//需要一个一个地删除释放链表中的节点
			iterator it = begin();
			while (it != end())
			{
    
    
				//erase之后迭代器会失效,必须接受返回值更新it才能继续删
				it = erase(it);
			}
			_size = 0;
		}

		~list()
		{
    
    
			//先清理释放链表中的有效节点
			clear();
			//再释放哨兵位的头节点_head
			delete _head;
			_head = nullptr;
		}

		//双向带头循环链表的begin是哨兵位的头节点的_next
		iterator begin()
		{
    
    
			//return iterator(_head->_next);

			//单参数构造函数的类支持隐式类型的转换
			//可以直接这样写,编译器会自动调用iterator
			//的构造函数构造匿名对象返回
			return _head->_next;
		}

		//因为这是双向带头循环链表,而end又是指最后一个元素的下一个位置,
		//所以就是_head的位置
		iterator end()
		{
    
    
			return _head;
		}

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

		const_iterator end() const
		{
    
    
			return _head;
		}

		size_t size() const
		{
    
    
			return _size;
		}

		void push_back(const T& x)
		{
    
    
			//Node* newNode = new Node(x);
			//if (newNode)
			//{
    
    
			//	Node* tail = _head->_prev;
			//	tail->_next = newNode;
			//	newNode->_prev = tail;
			//	newNode->_next = _head;
			//	_head->_prev = newNode;
			//}

			//复用insert即可
			insert(end(), x);
			
		}

		void pop_back()
		{
    
    
			//assert(_head->_prev != _head);

			//Node* tail = _head->_prev;
			//Node* tailPrev = tail->_prev;
			//tailPrev->_next = _head;
			//_head->_prev = tailPrev;
			//delete tail;
			//tail = nullptr;

			//复用erase即可
			erase(--end());
		}

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

		void pop_front()
		{
    
    
			//复用
			erase(begin());
		}

		iterator insert(iterator pos, const T& x)
		{
    
    
			//先创建一个新结点
			Node* newNode = new Node(x);
			//先把迭代器转换成节点的指针Node*,方便操作
			Node* cur = pos._node;
			//记录前一个节点
			Node* prev = cur->_prev;

			//更改连接关系,这个就easy了
			prev->_next = newNode;
			newNode->_prev = prev;
			newNode->_next = cur;
			cur->_prev = newNode;

			//最后记得更新_size
			++_size;

			//同样存在隐式类型的转换
			return newNode;
		}

		iterator erase(iterator pos)
		{
    
    
			//断言,不能删除哨兵位的头节点
			assert(pos != end());

			//先把迭代器转换成Node*的指针,方便更改连接关系
			Node* cur = pos._node;

			//保存下一个节点
			Node* next = cur->_next;

			//保存前一个节点
			Node* prev = cur->_prev;

			//前一个节点与后一个节点连接起来即可
			prev->_next = next;
			next->_prev = prev;

			//释放要删除的节点
			delete cur;
			cur = nullptr;

			//最后记得--_size
			--_size;

			//同样存在隐式类型的转换
			return next;
		}
	private:
		Node* _head;
		size_t _size;
	};

	void test1(void)
	{
    
    
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);
		lt.push_back(5);

		lt.insert(lt.begin(), 100);
		lt.erase(lt.begin());
		lt.erase(lt.begin());


		//lt.pop_back();
		//lt.pop_back();
		//lt.pop_back();
		//lt.pop_back();
		//lt.pop_back();


		
		list<int>::iterator it = lt.begin();
		while (it != lt.end())
		{
    
    
			cout << *it << " ";
			++it;
		}
		cout << endl;

	}

	struct A
	{
    
    
		A(int x1=0,int x2=0)
			:_a1(x1)
			,_a2(x2)
		{
    
    }

		int _a1;
		int _a2;
	};

	void test2()
	{
    
    
		list<A> lt;
		lt.push_back(A(1, 1));
		lt.push_back(A(2, 2));
		lt.push_back(A(3, 3));
		lt.push_back(A(4, 4));

		list<A>::iterator it = lt.begin();
		while (it != lt.end())
		{
    
    
			//cout << (*it)._a1 << " " << (*it)._a2 << endl;
			cout << it->_a1 << " " << it->_a2 << endl;

			++it;
		}
		cout << endl;
	}

	void test3(void)
	{
    
    
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);
		lt.push_back(5);

		list<int> lt1(lt);
		for (const auto& e : lt1)
		{
    
    
			cout << e << " ";
		}
		cout << endl;
	}

	void test4(void)
	{
    
    
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);
		lt.push_back(5);

		list<int> lt1;
		lt1.push_back(1);

		lt1 = lt;

		lt1.pop_back();
		lt1.pop_back();
		lt1.pop_front();

		for (const auto& e : lt1)
		{
    
    
			cout << e << " ";
		}
		cout << endl;

		cout << lt.size() << endl;
		lt.clear();
		cout << lt.size() << endl;
	}
}

The above is the entire content of the simulation implementation of the commonly used interfaces of the STL container list. In fact, there are many interfaces in the list, but these interfaces are commonly used, and other less commonly used interfaces will not be implemented. The above is what I want to share with you today, have you learned it? If this article is helpful to you, please be careful and pay attention to it. We will continue to update the relevant knowledge of C++ in the future. See you in the next issue! ! ! ! !

Guess you like

Origin blog.csdn.net/weixin_70056514/article/details/131768188