Simulation implementation of STL-list

Table of contents

basic concept

Simulation implementation

1. Implementation of list_node

2. The basic framework of the list

<1> member variable

<2> No parameter constructor

3.__list_iterator (list iterator implementation) The most important content of this article

<1>Constructor and copy constructor of __list_iterator

<2>* overload

<3> Overloading of ++ and --

<4> Overloading of != and ==

4. Continue to implement the list

<1> push_back interface

<2>insert

<3> list's iterator interface begin and end key content

<*>empty

<4>erase

<5> apply erase and insert to implement push_back, push_front and pop_back, pop_front functions

<6>. Iterator constructor  

<*>swap

<7> copy constructor and assignment copy in modern writing

<*>clear

 <8> Destructor

<*>Expand important content

Summarize


basic concept

In C++, the implementation of list is implemented in the form of linked list. The characteristic of linked list is that it is not stored in continuous space in memory, but continuously opens up new random memory to store data. The biggest advantage of this storage method is to insert Data will not need to move data like string and vector, resulting in a lot of time cost.

But its disadvantages are also obvious

1. A large amount of discontinuous space storage may cause a lot of memory fragments in the middle but cannot be effectively utilized.

2. Unable to support random access, like sting and vactor can use [num] for random access.

3. Every time you insert data, you need to apply for new space, and the cache utilization rate is not high.

But even so, the linked list is still an important part of STL, and its shortcomings do not overshadow its advantages. Let's simply simulate and implement the commonly used interface functions in STL.

Simulation implementation

1. Implementation of list_node

From the above figure, we can know that the linked list is stored by one node after another. The node has its stored data and pointers to the previous node and the next node inside, so we can implement list_node through the structure.

template<typename T>
	struct list_node {
		typedef list_node<T> node;

		T _data;
		node* _prev;
		node* _next;

		list_node(const T& val = T()) //缺省值为T的默认构造
			:_data(val)
			,_prev(nullptr)
			,_next(nullptr)
		{}
	};

For the constructor of list_node, we only need to pass in a value, and set _prev and _next to nullptr (null pointer)

Question 1: Why use struct instead of class?

Class is relatively tight for encapsulation. If we use class, it is not conducive to list to access its nodes, so use struct, and you can clearly feel it later.

Question 2: Do I need to write its destructor here?

No, if your class and object science are solid, these three member variables are all built-in types and will call their own destructors.

2. The basic framework of the list

<1> member variable

class list { 
public:
	typedef list_node<T> node;
private:
	node* _head;
};

There is nothing to say about this, the list only points to the _head sentinel node.

<2> No parameter constructor

void ini_empty()
{
	//初始化哨兵位节点
	_head = new node;
	_head->_next = _head;
	_head->_prev = _head;
}
//无参构造函数
list()
{
	ini_empty();
}

We only need to open up space for the sentinel bit, and when the linked list does not store any data, both _next and _prev of the _head point to itself.

3.__list_iterator (list iterator implementation) the most important content of this article

We have learned string and vector words before, we can clearly know that their iterators are native pointers, why?

First of all, we need to understand that iterators exist just like pointers. For string and vector, because they are a continuous storage memory space, their iterators can directly use native pointers .

So for the linked list, how is its iterator implemented? How can its iterators traverse our storage space as easily as string and vector iterators?

template<class T, class Ref, class Ptr>
struct __list_iterator{
	typedef list_node<T> node;

    node* _node;
};

You may have a lot of questions, the iterator of list is actually a structure, its member variables are also node*, and there are three types of template parameters in this structure.

We will slowly answer these questions

First of all, why is the iterator of list a structure?

Because if he wants to be able to traverse the stored data like a raw pointer, it is impossible to directly use string and vector as a raw pointer because it is a discontinuous storage space. We can only access the next node through the _next of a node, and the most important reason is that struct, like class, supports operator overloading! 

<1>Constructor and copy constructor of __list_iterator

__list_iterator(node* Node)
	:_node(Node){}

__list_iterator(const iterator& lt)
	:_node(lt._node){}

I believe these are not difficult for everyone to understand, and there is nothing new here.

<2>* overload

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

After reloading *, we can directly access the data of the node through (*iterator).

<3> Overloading of ++ and --

iterator& operator++()   //前置++
{
	_node = _node->_next;
	return *this;
}
iterator operator++(int)  //后置++
{
	iterator tmp(*this);
	_node = _node->_next;
	return tmp;
}
iterator& operator--()   //前置--
{
	_node = _node->_prev;
	return *this;
}
iterator operator--(int)  //后置--
{
	iterator tmp(*this);
	_node = _node->_prev;
	return tmp;
}

Seeing this, is it clear that after overloading ++ and --, we can access the data at the next (previous) position like ++(--) like a vector iterator.

It should be noted that the post ++ and -- need to return the original iterator, and the return value cannot be returned by reference because tmp is a temporary variable.

<4> Overloading of != and ==

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

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

4. Continue to implement the list

<1> push_back interface

void push_back(const T& val)      //尾插
{
	node* newnode = new node(val);//创建新节点

	node* tail = _head->_prev;    //提前记录尾部节点
    //节点的指向重新设定
	tail->_next = newnode;
	newnode->_prev = tail;
	_head->_prev = newnode;
	newnode->_next = _head;
}

Very simple implementation of push_back, no difficulty

<2>insert

iterator insert(iterator pos, const T& val)  //在迭代器pos位置插入一个节点
{  
	node* newnode = new node(val);    //创建新节点
	node* cur = pos._node;            //提前记录pos所指结点
	node* prev = cur->_prev;  
	prev->_next = newnode;
	newnode->_prev = prev;
	newnode->_next = cur;
	cur->_prev = newnode;
	return pos;
}

Here return returns the iterator position of pos.

<3> Key content of list iterator interface begin and end   

We had such a question in __list_iterator before, why do we have three template parameters here?

Ref refers to reference (reference), Ptr refers to pointer

Here you need to match whether the list is const

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;

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);
}

};

Based on our past experience in writing strings and vectors, the most important interfaces between their iterators and them are the begin and end functions, and the list we are writing now is no exception.

If we define a const list, then he should construct a const iterator, and how we can identify whether it is const, we can judge by adding more Ref and Ptr template parameters, if it is const, then return a < T, const T&, const T*> is the iterator of the parameter template, Ref is const T&, Ptr is const T*, if it is recognized that the parameter template is <T, const T&, const T*>, it is calling *When overloaded, it will return const T&, otherwise, it will return T&

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

--------------------------------------------------------------------------------------------------------------------------------

Because the range of begin and end is left-closed and right-open, begin points to _next of _head (that is, the first valid storage node)

end points to _head (the sentinel itself).

<*>empty

Function: Determine whether the linked list is empty

bool empty()
{
	return begin()._node == _head;  //begin是否是指向哨兵位
}

If begin points to a sentinel bit, it means that this node has no data, that is, an empty linked list.

<4>erase

bool empty()
{
	return begin()._node == _head;  //begin是否是指向哨兵位
}

iterator erase(iterator pos)
{
	assert(pos._node != end()._node);    //不能把哨兵位给删了
	assert(!empty());                    //不能是空链表
	node* cur = pos._node;
	node* prev = cur->_prev;
	node* next = cur->_next;
	prev->_next = next;
	next->_prev = prev;
	delete cur;
	return iterator(next);
}

Note that the node to be deleted is released to delete, and the next node of the deleted node is returned.

<5> apply erase and insert to implement push_back, push_front and pop_back, pop_front functions

void push_back(const T& val)     //尾插
{
	/*node* newnode = new node(val);
	node* tail = _head->_prev;
	tail->_next = newnode;
	newnode->_prev = tail;
	_head->_prev = newnode;
	newnode->_next = _head;*/

	//套用insert
	insert(end(), val);
}

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


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

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

<6>. Iterator constructor  

//迭代器构造
template<typename InputIterator>
list(InputIterator first, InputIterator last)
{
	ini_empty();    //初始化哨兵位
	while (first != last)
	{
		push_back(*first);
		++first;
	}
}

Why does its iterator constructor add template parameters?    

For example, if you want to insert a series of data in a vector, you can insert the data into the list by passing a range of the iterator of the vector, isn't it very convenient!

<*>swap

Function: exchange the data of two lists

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

If we want to exchange the data of two linked lists, we don't need to move them one by one, we just need to exchange their sentinel bits.

<7> copy constructor and assignment copy in modern writing

list(const list& lt)      //在类里可以不加模版参数(但是推荐加上)
{
	ini_empty();
	list tmp(lt.begin(), lt.end());
	swap(*this, tmp);
}

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

We advocate the modern way of writing copy construction and assignment copy, because it is really concise! ! !

<*>clear

Function: Clean up all valid storage nodes in the linked list (excluding sentinel bits)

void clear()
{
	auto it = begin();
	while (it != end())
	{
		it = erase(it);
	}
}

 <8> Destructor

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

Unlike clear, the destructor needs to release the sentinel bit, otherwise there will be a memory leak!

<*>Expand  important content

If there is such a case, what is stored in the list is a structure or class, and if we want to access the member variables and functions in these structures, we can overload its -> operator in __list_iterator, so that this Simplified access to data.

Take the following code as an example

struct Pos
{
    int _a1;
    int _a2;

    Pos(int a1 = 0, int a2 = 0)
	    :_a1(a1)
	    , _a2(a2)
        {}
};
void list_test3()
{
	list<Pos> lt;
	lt.push_back(Pos(10, 20));
	lt.push_back(Pos(10, 21));

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

		++it;
	}
	cout << endl;
}
Ref operator*()
{
	return _node->_data;
}

Ptr operator->()        //注意,这里竟然没有参数!!
{
	return &(operator*());         //这种写法是什么意思呢?
}

Take a closer look at the -> overloading method here, what does &(operator*()); mean?

Just take the address of _node->_data.

Let's go back to the above example, it->_a1, because our overloaded -> has no parameters , then we will find a very strange phenomenon, is there a missing ->, shouldn't it be it->- >_date?

In fact, there is nothing wrong, there should be two ->, but because the compiler simplifies the ->-> here to -> for code readability.

And if we write ->-> here, it will report an error instead!

The return value here is somewhat similar to the overload of *, if it is a const list, it will return const T*.

Summarize

The biggest difference between list and string and vector is the difference between iterators, so we learn list mainly to learn its iterator implementation.

STL is similar. After learning one, in fact, from the perspective of use, the others will be used, even if their data structures are completely different.

Guess you like

Origin blog.csdn.net/fengjunziya/article/details/130574156