C++STL list simulation implementation

Preface

To implement STL's list, first we have to look at the source code of the list.
Insert image description here
We see such a thing, we know that C++ is compatible with C, and you can use struct to create a class. But we are used to using class.

So when would you use struct?
All members of this class want to be exposed, such as node pointers, which are generally exposed. So we use struct.

Continue to look at the more important things in the source code, the structure of member variables.
Insert image description here

What is this thing?
Insert image description here
Insert image description here
That makes it very clear.

Knowing that it is a pointer to a node, what should we look at next?
When members looked at it, they looked at the interface.
The first step in looking at the interface is to look at the constructor. If you look at the constructor, you will know how it is initialized, and you will know what its initial structure is.
Once the initial structure is clear, its general shape will be clear.

Next, let’s look at its core methods. Of course, we have a certain understanding of lists.
Head plug deletion and tail plug deletion are the core methods.

Let’s take a look at its constructor
Insert image description here
Insert image description here
Insert image description here
, so we won’t read on.

list implementation

Write out the most basic things first.

namespace but
{
    
    
	template<class T>
	struct list_node
	{
    
    
		list_node<T>* _next;
		list_node<T>* _prev;
		T _data;
	};

	template<class T>
	class list
	{
    
    
		list()
		{
    
    
			_head = new list_node;
			_head->_next = _head;
			_head->_prev = _head;
		}
	private:
		list_node* _head;
	};
}

push_back

Insert image description here
Insert image description here

Why is the error reported?
Insert image description here
We said before that like a constructor, template parameters do not need to be added to the parameters, but the declared type still has to be added.

list_node is the class name, list_node is the type.

Update the previous code.

namespace but
{
    
    
	template<class T>
	struct list_node
	{
    
    
		list_node<T>* _next;
		list_node<T>* _prev;
		T _data;
	};

	template<class T>
	class list
	{
    
    
		typedef list_node<T> node;
		list()
		{
    
    
			_head = newnode;
			_head->_next = _head;
			_head->_prev = _head;
		}
	private:
		node _head;
	};
}

What to do with push_back?
Find the tail, then create a new node, and finally link.
Insert image description here

void push_back(const T& x)
{
    
    
	node* tail = _head->_prev;
	node* new_node = new node(x);

	tail->_next = new_node;
	new_node->_prev = tail;
	new_node->_next = _head;
	_head->_prev = new_node;
}

Write a constructor for list_node.

list_node(const T& x )
	:_next(nullptr)
	, _prev(nullptr)
	, _data(x)
{
    
    }

Then an error is reported.
Insert image description here
What to do if there is no default constructor?
It's better to provide a fully default constructor.

//list_node(const T& x =0)不能给0
list_node(const T& x =T())
	:_next(nullptr)
	, _prev(nullptr)
	, _data(x)

Iterator (emphasis)

Ordinary iterator

First of all, we will definitely encounter a problem. The previous vector data is stored continuously, but each node of the linked list is discontinuous.
++ cannot point to the next node.
Insert image description here
how to solve this problem?
Is it possible to provide an overload for node? No, because it is node* instead of node;

We can take a look at the source code of STL.
Insert image description here
++ can also dereference
Insert image description here

Now we write a simple iterator based on our own understanding and let it run.
Insert image description here

Then we write begin() and end() in the list object and it can be accessed normally.
Insert image description here

Finally, test it.
Insert image description here
Insert image description here
If you look carefully, the structures of arrays and linked lists are very different, but they are so similar in use.
This comes from encapsulation, which blocks out details that we cannot see.

The most important thing today is not the implementation of linked lists, the implementation of iterators is the most important.

To sum up, node* does not support dereference and ++, but I can encapsulate it with a custom type and then overload the operator. I can control the dereference behavior I want and the + I want. + behavior, this is the meaning achieved by the custom type.

Insert image description here
Note that there is a hidden point here. A copy construct has occurred. We have not written the copy construct ourselves. Will there be problems with the one automatically generated by the compiler?
Insert image description here
The program runs without error, what's the reason? There is no destructor written here, and there is no need to release the node.

Why don't we need to release the node?
Although there is a node pointer, the node pointer does not belong to the iterator.
The node pointer is given to the iterator, just for traversing the linked list, ++, dereferencing, and modifying the linked list.
Release is a matter of the linked list. The destructor of the linked list will release it and you don't need to release it.
This node is not produced by the iterator new. You only have the right to use it, but not the right to own it.

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* n)
		:_node(n)
	{
    
    }

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

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

	self& operator++()
	{
    
    
		_node = _node->_next;

		return *this;
	}

	self operator++(int)
	{
    
    
		self tmp(*this);
		_node = _node->_next;

		return tmp;
	}

	self& operator--()
	{
    
    
		_node = _node->_prev;

		return *this;
	}

	self operator--(int)
	{
    
    
		self tmp(*this);
		_node = _node->_prev;

		return tmp;
	}

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

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

const iterator

Suppose we pass a const linked list and the compilation fails.
Insert image description here
Why does the compilation fail?
It’s still the permission amplification we talked about many times before.
We just need to provide an iterator that supports const objects.

But look here, why can const objects still call constructor iterators?
Insert image description here
First of all, *this modified by const is specifically _head; so _head cannot be changed, not that the content pointed to by _head cannot be changed.
The node pointer itself cannot be changed, but it can be copied to others.

But writing it this way does not meet our expectations and can be modified. Why can it be modified? It is because it constructs an ordinary iterator. But ordinary iterators are not writable.

We are going to write a const iterator

First, let's think about the difference between ordinary iterators and const iterators?

Let’s look at a question first. Can we define a const iterator like this?
Insert image description here
Absolutely not.
First of all, the iterator targets a pointer.
Insert image description here

Written as above, it protects the iterator itself from being modified, and what we want is that the content pointed to by the iterator cannot be modified, that is, const T*;

So how to implement it? The content we want to implement cannot be modified.
We can write another const iterator object just like we implemented a normal iterator before, just change the name, and then it cannot be modified when dereferencing.
Insert image description here

Insert image description here

The two objects are the same except for the return value. How can we simplify it?
It's OK if the control return value is different. Add a template parameter.
You can still play like this.
Insert image description here
Insert image description here

// 1、迭代器要么就是原生指针
// 2、迭代器要么就是自定义类型对原生指针的封装,模拟指针的行为
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* n)
		:_node(n)
	{
    
    }

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

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

	self& operator++()
	{
    
    
		_node = _node->_next;

		return *this;
	}

	self operator++(int)
	{
    
    
		self tmp(*this);
		_node = _node->_next;

		return tmp;
	}

	self& operator--()
	{
    
    
		_node = _node->_prev;

		return *this;
	}

	self operator--(int)
	{
    
    
		self tmp(*this);
		_node = _node->_prev;

		return tmp;
	}

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

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

Let’s take a look at the template parameters in the library
Insert image description here

Why is there still a Ptr?
It also provides an overloaded operator->;
Insert image description here
When will it be used->?
Please note that the above iterator simulates int*;
do you need to use a custom type->
Insert image description here
Insert image description here
You will see an error. What's wrong with the report?
It returns AA, and AA does not return stream insertion.

The first way can be to use overloading to insert a stream. Here, because the members in AA are not private, we can do this.
Insert image description here
It’s awkward to write like this but we can do it this way.
Insert image description here
We can overload one in the iterator ->
Insert image description here

It always feels a little weird, but it's actually like this.
Insert image description here
Insert image description here

Okay, then the -> overload of the const iterator needs to return const T*, so here is another template parameter.

insert

In fact, the linked list has almost been implemented. Now we can improve the function ourselves. In fact, we don't need to implement head plug deletion and tail plug deletion. We only need to implement insert. and erase. After insert and erase are implemented, everything else can be implemented.
Insert image description here

void insert(iterator pos, const T& x)
{
    
    
		node* cur = pos._node;
		node* prev = cur->_prev;

		node* new_node = new node(x);

		prev->_next = new_node;
		new_node->_prev = prev;
		new_node->_next = cur;
		cur->_prev = new_node;
}

Will inserting into a linked list cause the iterator to expire?
Not
because pos always points to this node, and this position relationship will not change.

Then we actually don’t need to write push_back and push_front ourselves.

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

erase

Insert image description here

void erase(iterator pos)
{
    
    
//哨兵卫头节点不能删除
		assert(pos != end());

		node* prev = pos._node->_prev;
		node* next = pos._node->_next;

		prev->_next = next;
		next->_prev = prev;
		delete pos._node;
}

Will the erase of a linked list cause the iterator to expire?
Tietie's failure, the pointers to the nodes pointed to by the iterator were all deleted.

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

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

Insert image description here
Can you see the difference between the two lines of code below?
There is essentially no difference. The difference between them is that pnode is a built-in type and it is a custom type.

From a physical space perspective, their codes are exactly the same, both are 4 bytes, and both have the same address.
Insert image description here
But the behavior of these two is very different

Insert image description here
This is the difference between C language and C++.

destructor

clear can help us clear the data, but it does not clear the head node.

void clear()
{
    
    
	iterator it = begin();
	while (it != end())
	{
    
    
		it = erase(it);//防止迭代器失效
		erase(it++);
	}
}

Insert image description here
Is it okay to write like this?
Can. Isn't it invalid? Why can it++ still work? We have said before that a phenomenon of it failure is wild pointers, so why is nothing happening here?
This is the value of postfix ++, it will return the value before ++.
Insert image description here
In other words, what is erased is not it, but the returned iterator.

The difference between destruction and clear is whether the head node needs to be cleared, while destruction is completely unnecessary.

~list()
{
    
    
	clear();
	delete _head;
	_head = nullptr;
}

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

Constructor

Let's provide the construction of iterator range again.
Insert image description here
Is it possible to write it this way? No, if you want to push_back, you must have a head node of the Sentinel Guard.

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

template <class Iterator>
list(Iterator first, Iterator last)
{
    
    
	empty_init();

	while (first != last)
	{
    
    
		push_back(*first);
		++first;
	}
}

Can const objects call constructors? Can.
Insert image description here

copy construction

traditional writing
Insert image description here

modern writing

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

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

	list<T> tmp(lt.begin(), lt.end());
	swap(tmp);
}

Insert image description here
This is exchanged with tmp, but this is a random value and an error will be reported, so it needs to be initialized.

Assignment

Insert image description here
Why not pass parameters by reference?
Using quotes can have very bad consequences.
You see, if you pass a reference, lt is lt3, and the exchange becomes the exchange of lt1 and lt3.


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

The difference between vector and list

In fact, it is the difference between a sequence list and a linked list.

Guess you like

Origin blog.csdn.net/weixin_68359117/article/details/134944007