[c++] list simulation implementation

Table of contents

Foreword:

Ways to learn classes:

1 class member variable

1.1 list member variable

 1.2 Node structure variables

1.3 Iterator member variables

2 Default function - construction

2.1 Node structure constructor

2.2 list constructor

2.3 Iterator constructor

3 Iterator Implementation

3.1 list section

3.2 Iterator structure part

3.2.1 operator*

3.2.2 operator++/operator--

3.2.3 operator->

3.2.4 operator==/!=

3.2.5 Ref and Ptr

code:

4 Interval construction, copying, assignment, and destruction

4.1 Iterator Interval Construction

4.2 Copy construction

4.3 Assignment overloading

4.4 Destructors

5 Insert and delete functions


Foreword:

        This article simulates the implementation of the list container in STL. The difficulty lies not in its data structure - linked list, but in the connection and mutual calling between classes. The relationship is very clever, and there is also a great template class. I believe that everyone will gain something after reading carefully.

Ways to learn classes:

        If you have learned other classes such as string or vector, you will definitely develop a good habit- to understand a class, you must first understand its member variables, then its default functions, then iterators, and finally its other functions function function .

1 class member variable

1.1 list member variable

        First look at the member variables of the list class, yes, you read it right, there is only one _head, but its type is Node*, the type obtained through our typedef, see this, do you think of where you have seen it before? ? The string class in Linux uses this method to encapsulate a structure type with a pointer, instead of splitting a structure into individual variables as the list class as I used in the vector in the previous article. Although there is actually no difference in the bottom layer, the uniqueness of the code becomes unsound, and there are still many functions that are not easy to implement, which I will explain later on iterators .

 template<class T>
class list

{

public:

        typedef struct node<T> Node;
        typedef struct __list_iterator<T, T&, T*> iterator;
        typedef struct __list_iterator<T,const T&, const T*> const_iterator;

private:

        Node* _head;

}        

 1.2 Node structure variables

        If you have the basis of linked list, I think you should be able to reflect that the form of this linked list is a two-way, because it retains the pointers of the front and back nodes, so it can move forward or backward, but because we want a very efficient Simple two-way linked list is not enough, we also need to add a circular attribute to become the most powerful bidirectional circular linked list in the linked list .

        The T type is a template type, which does not simply refer to a type, that is, we can construct a linked list that can store any type, including custom types.

template<class T>
struct node
{

        struct node<T>* next;
        struct node<T>* prev;
        T data;

};

1.3 Iterator member variables

        If you want to say which design is the most powerful in the STL container, I will only give one answer, which is the construction of the iterator idea. Why? It can only be said that by encapsulating the data structure into an iterator, the external interface can be accessed through the same interface without knowing what the structure is, which greatly saves the energy of beginners.

        To give an example: (see the code below)

void test1()
{
	std::vector<int> vv = { 1, 2, 3, 4, 5 };
	std::list<int> ll = { 1, 2, 3, 4, 5 };
	std::vector<int>::iterator itv = vv.begin();
	while (itv != vv.end())
	{
		std::cout << (*itv *= 2) << " ";
		itv++;
	}
	std::cout << std::endl;

	std::list<int>::iterator itl = ll.begin();
	while (itl != ll.end())
	{
		std::cout << (*itl *= 3) << " ";
        itl++;
	}
    std::cout << std::endl;
}

         I believe that everyone who has studied data structures must understand that the underlying implementations of vector and list are completely different, but have you found out that these two data structures with completely different underlying structures are organized by iterators, and the two data structures The use of becomes a unified structure . Does it feel awesome? It can only be said that the world of the patriarch and the bosses is too big. If this blogger can come up with this kind of structure, it is estimated that this life will be enough to brag about. Ha ha. The results of the visit are as follows.

         Seeing the iterator structure below, there is only one variable in it, and it is exactly the same as the variable type of our list class, but you should not be surprised, because the appearance of the iterator class itself is to assist the list to access the node class Members, so type equality is inevitable, and I tell you in advance that they are not only equal in type, but also the space pointed to by the variable is the same . So won't this cause the space to be freed repeatedly?

        The answer is, definitely not, unless our brains are suddenly gnawed by dogs, and we need to write a destructor in the iterator structure and release this space. So this also means that the iterator does not need copy construction and assignment overloading, what we want is a shallow copy .

        The iterator said: I just touch, look, and move, but I will never give you a smooth ride, so don't worry.

template<class T, class Ref, class Ptr>
struct __list_iterator
{         

     typedef struct node<T> Node;
     typedef struct __list_iterator<T, Ref, Ptr> self;

     Node* Pos;
};

        As for the Ref and Ptr in the iterator template, you don't need to worry about it now, and I will explain it to you later.

2 Default function - construction

2.1 Node structure constructor

        In the construction, I used T() as the default parameter. After all, we still have to consider the case where the type is a custom type, or there are handsome guys who are too lazy to pass parameters, such as the blogger himself.

        Because this node is just a node, it has not yet established a connection with the linked list, so it does not point to any other nodes, it points to nothing, and the data can be assigned directly.

node(const T& val = T())
       :next(nullptr)
       , prev(nullptr)
       , data(val)
{}

         I suddenly thought, will everyone open a space in it when constructing a node node? Probably not, after all, as long as we call this function, the space must already exist, whether it is opened in the heap or on the stack, we just assign the variables in this space, so there is no need to open the space.

2.2 list constructor

        Because the linked list we wrote takes the lead, even if it is a no-argument structure, it is not enough to directly set a nullptr for _head, but also needs to open up a node space, and the front and rear pointers of the node point to itself .

        And did you see where I opened the space? It is because _head needs to point to such a space, and it cannot be invalidated because the function exits, so it is necessary to open up space on the heap, here the constructor of Node will be called, and then there will be space for Node variables. Naturally, there is no need to go to Node to open up space.

void Init_head()
{
    _head = new Node;
    _head->next = _head;
    _head->prev = _head;
}

//无参
list()
{
    Init_head();
}

2.3 Iterator constructor

        The constructor of the iterator is simple. You only need to pass in a pointer of type Node*, and then make a shallow copy and assign it to Pos. I believe you will not be stuck here.

struct __list_iterator(Node* node)
     :Pos(node)
{}

3 Iterator Implementation

3.1 list section

            For iterators, the most basic thing is to provide begin and end functions, so that our scope for can be realized by the editor.

        And have you noticed? In the past, when we wrote iterators, we directly returned a pointer, but is this still the case for lists? Obviously not, we get the pointers of the head node and the tail node, and then use this pointer to call the constructor of the iterator, and then return the structure to get it .

iterator begin()
{
	//进入迭代器类当中去,然后传入起始值的数据初始化
	return iterator(_head->next);
}
iterator end()
{
	//_head相当于循环里的迭代器最后一个数据的下一个
	//所以初始化就用_head去初始化
	return iterator(_head);
}
const_iterator begin() const
{
	return const_iterator(_head->next);
}
const_iterator end() const
{
	return const_iterator(_head);
}

3.2 Iterator structure part

3.2.1 operator*

        What do we usually do when we use iterators to get data? Is it possible to get it directly by *iterator? But what about our list iterator? Can the data be obtained directly by *iterator? Obviously not, and our iterator is not a pointer, but a complete structure, so it has no meaning by itself. So because of this, we need to overload an * operator on the iterator, and when we use *iterator, we can get the data in the nodes inside it .

        So at this time I ask a question, if the list class does not encapsulate the iterator, can it still overload * like this?

        The answer is no, because if we do not encapsulate the iterator, then this iterator must only be a pointer, but is this pointer the pointer to the data in our node node? no! It is the pointer of this node. After dereferencing with *, only the structure of the node node can be obtained, not the data in the structure.

        Then someone has to ask again, can I get the node and I can't access the data yet?

        Yes, but I ask, is it still the result we want if we get the node through the iterator, and then access the data through this node? In other words, the behavior of iterators is no longer uniform, what use do I want him to be? as follows:

// desired result

yf::list<int>::iterator it = head.begin();

*it = data

// undesired result

yf::list<int>::iterator it = head.begin();

*it = node

node.data = data

        From this point of view, the encapsulation of list iterators is not just to achieve functional separation, but to unify the behavior and form a whole with other container iterators. 

3.2.2 operator++/operator--

        We learned what the iterator ++/-- means, the iterator ++/-- means that we want to move the iterator node to the next node position or the previous node position instead of going to the previous node position The next position in the continuum . If the iterator in the form of vector itself is a continuous space, then there is no problem for it to go to the next position, but the key is that the list is not, its list is encapsulated, then it means that the iterator can only repeat Load ++/--. According to the different requirements of the front and back, return nodes at different positions.

3.2.3 operator->

        When you use a structure pointer, will you dereference it first, and then use this structure to access the data inside? In this way, you may not understand it very well, so please see the following code:

struct AA

{

        int a;

        int b;
}

struct AA val;

struct AA* ptr = &val;

// use 1

(*ptr).a = 2;

// use 2

ptr->a = 2;

        I don't need a brain to know that everyone must use the second method when using structure pointers. Doesn't the first method just disgust yourself, so this thing also appears in the iterator->. Of course, what this -> arrow operator overloads is not our Node node, but the variable data in the Node node. If its type is a class or a structure, it can be used.

list<struct AA> ll;

list<struct AA>::iterator it = ll.begin();

it->a = 2;

        In other words, if the T type itself does not support the use of the -> operator, then overloading -> will have no effect

list<int> ll;

list<struct AA>::iterator it = ll.begin();

it-> //error, I don't even know what you mean

//If you want to refer to the data directly *it is fine

        And do you feel that this overload is missing something?

        The witty guy probably has already seen it. Yes, there is a -> symbol missing, which was originally:

list<struct AA> ll;

list<struct AA>::iterator it = ll.begin();

it->->a = 2;

         This is not intuitive, but you can know it when you expand it:

it.operator->()->a = 2;

        Did you see it? There should have been an overloaded operator, but the access above is gone, why?

        The editor omitted it for code readability. The bosses must know what we want to do here, so they have dealt with this redefinition, only one -> will do. 

3.2.4 operator==/!=

        These two overloads are easy to understand. Don't you want to compare whether the two nodes are the same? I happen to know the addresses of these two nodes, so as long as the addresses are different, they are different, otherwise they are the same.

3.2.5 Ref and Ptr

        Seeing this, everyone should also know why there are two template parameters, Ref and Ptr. Ref is to distinguish const T& from T&, and Ptr is to distinguish const T* from T*.

        Why make a distinction? Because if we don’t distinguish, we have to write two templates with basically the same content and functions and write them twice. Then this template is rubbish, and Ref appears. In the template, we must also consider the return value of const T* and T*. Type, but also write the function on both sides, isn't this pure and disgusting, there is Ptr.

code:

//*迭代器返回结点内部的数据
Ref operator*()
{
	return Pos->data;
}

//到下一个结点,而不是结点指针的下一个位置
self& operator++()
{
	Pos = Pos->next;
	return *this;
}

self operator++(int)
{
	self temp = *this;
	Pos = Pos->next;
	return temp;
}

self& operator--()
{
	Pos = Pos->prev;
	return *this;
}

self operator--(int)
{
	self temp = *this;
	Pos = Pos->prev;
	return temp;
}

//返回节点数据的地址
Ptr operator->()
{
	//Pos已经是结点了,但是如果需要访问的数据也是一个结构体
	//那么获取到了它的地址,就能用->去访问了
	return &Pos->data;
}

bool operator==(const self& val)
{
	return Pos == val.Pos;
}

bool operator!=(const self& val)
{
	return Pos != val.Pos;
}

4 Interval construction, copying, assignment, and destruction

4.1 Iterator Interval Construction

        The iterator range is a good thing. It not only realizes the construction of our same type of iterators, but also directly copies other types of iterators, but its own logic is very simple, as follows.

//迭代器区间构造
template<class InputIterator>
list(InputIterator first, InputIterator last)
{
	Init_head();
	while (first != last)
	{
		push_back(*first);
		++first;
	}
}

4.2 Copy construction

        The copy I wrote is a modern way of writing. It is not traditional that every data must be copied directly and then connected. That is too troublesome and does not conform to my habit of being lazy, so the code is as follows:

//拷贝
list(const list<T>& val)
{
	Init_head();
	list<T> temp(val.begin(), val.end());
	std::swap(_head, temp._head);
}

        You may be confused just by looking at the code, so I will explain it to you. First, I saw Init_head(), which needs to be written here. Otherwise, when the function is exchanged later, _head will have no space and a wild pointer will be used. Our temp(val.begin(), val.end()) sentence directly copies a backup from the original data val, and does not point to the same space.

        It also means that we now have three heads, _head, temp._head, val._head. At this time, we only need to exchange _head and temp._head, then our class will have data, and temp will be gone. Why can you do this? Because these spaces are on the heap, there is no need to worry about disappearing except for the scope of the function. What disappears is only the variable on the stack called temp . What is this called, stealing the labor law, it is very powerful, I suggest everyone learn it, haha.

4.3 Assignment overloading

        Assignment overloading and copy construction use the same logic, both are stealing, but the way of stealing is different, because the function parameters of assignment overloading do not use references, then passing parameters back and forth to call copy construction means that there are already A new space has been opened, so we can just go and steal it.

//赋值重载
list<T>& operator=(list<T> val) 
{
	std::swap(_head, val._head);
	return *this;
}

4.4 Destructors

        The destructor is to clean up all the nodes. The clear function is to clean up all the nodes in the object except the head node. The cleaning method is very simple. Just delete the node. My clear reuses my erase function, wait a minute For everyone to share.

//清空除头结点之外的所有结点
void clear()
{
	iterator it = begin();
	while (it != end())
	{
		erase(it++);
	}
}

//析构
~list()
{
	clear();
	delete _head;
}

5 Insert and delete functions

        The code is exactly the same as the implementation logic of the data structure linked list, and it is too simple, so the blogger does not want to explain it.

//插入
void insert(iterator pos, const T& val)
{
	Node* cur = pos.Pos;

	Node* NewNode = new Node(val);

	cur->prev->next = NewNode;
	NewNode->prev = cur->prev;
	NewNode->next = cur;
	cur->prev = NewNode;
}

//尾插
void push_back(const T& val)
{
	iterator it = end();
	insert(it,val);
}

//头插
void push_front(const T& val)
{
	iterator it = begin();
	insert(it,val);
}	

//删除
void erase(iterator pos)
{
	assert(pos != end());

	Node* cur = pos.Pos;

	cur->prev->next = cur->next;
	cur->next->prev = cur->prev;

	delete cur;
}

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

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

//判空
bool empty()
{
	return _head->next == _head;
}

Guess you like

Origin blog.csdn.net/weixin_52345097/article/details/129464627