Article directory
Preface
To implement STL's list, first we have to look at the source code of the list.
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.
What is this thing?
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
, 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
Why is the error reported?
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.
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.
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.
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.
++ can also dereference
Now we write a simple iterator based on our own understanding and let it run.
Then we write begin() and end() in the list object and it can be accessed normally.
Finally, test it.
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.
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?
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.
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?
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?
Absolutely not.
First of all, the iterator targets a pointer.
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.
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.
// 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
Why is there still a Ptr?
It also provides an overloaded operator->;
When will it be used->?
Please note that the above iterator simulates int*;
do you need to use a custom type->
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.
It’s awkward to write like this but we can do it this way.
We can overload one in the iterator ->
It always feels a little weird, but it's actually like this.
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.
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
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());
}
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.
But the behavior of these two is very different
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++);
}
}
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 ++.
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.
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.
copy construction
traditional writing
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);
}
This is exchanged with tmp, but this is a random value and an error will be reported, so it needs to be initialized.
Assignment
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.