The use and simulation of the list class

Table of contents 

1. Introduction to the list class

Second, the use of list

1. Construction, copy constructor and iterator

2. Add, delete, check and modify data

3. Partial interface implementation of list

1. Node definition

2. Organization of the list class

Fourth, the iterator of the list

1. The design idea of ​​the iterator

2. const iterator

3. Overloading of the -> operator

4. Reverse iterator


1. Introduction to the list class

list is the implementation of the linked list in the C++ library, and its bottom layer is a leading two-way circular linked list.

Compared with data structures such as vector, the biggest defect of linked list is that it does not support random access. To access the nth element of the list, you must go forward or backward from a known position (commonly the head or tail) to that position, which requires a certain amount of time overhead. At the same time, the list also needs additional space to save the addresses of the front and back nodes.

Second, the use of list

1. Construction, copy constructor and iterator

explicit list (const allocator_type& alloc = allocator_type());———default structure, default parameter alloc I will not talk about it first

explicit list (size_type n, const value_type& val = value_type(), const allocator_type& alloc = allocator_type());———constructed list contains n elements whose value is val

template

list (InputIterator first, InputIterator last, const allocator_type& alloc = allocator_type());———construct a list with elements in the interval [first, last)

list (const list& x);———copy constructor

#include<iostream>
#include<list>
using namespace std;

void test()
{
    list<int> l1;//构造空list
    list<int> l2(4, 100);//l2中放4个值为100的元素
    list<int> l3(l2.begin(), l2.end());//用l2的[begin(), end())左闭右开区间构造l3
    list<int> l4(l3);//用l3构造l4
    
    //迭代器区间的构造可以是任何一种数据结构的迭代器,甚至是原生指针,下面就是以数组的原生指针为迭代器的构造
    int arr[] = { 16, 2, 77, 29 };
    list<int> l5(arr, arr + sizeof(arr) / sizeof(arr[0]));
    
    //C++11中引入了列表式的初始化
    list<int> l6{ 1, 2, 3, 4, 5 };
    
    //用迭代器可以遍历元素,范围for底层也是使用迭代器
    list<int>::iterator it = l6.begin();
    while (it != l6.end())
    {
        cout << *it << " ";
        ++it;
    }
    cout << endl;
}

int main()
{
    test();
    return 0;
}

2. Add, delete, check and modify data

void push_front (const value_type& val);—————Insert an element whose value is val at the head of the list

void pop_front ();————delete the first element in the list

void push_back (const value_type& val); Insert an element with value val at the end of the list

iterator insert(iterator position, const value_type& val);————insert an element whose value is val at the position position

iterator erase (iterator position);———delete the element at position position

void swap (list& x);———exchange the elements in the two lists

void clear();————Empty the effective elements in the list

void empty();————Check whether the list is empty

size_t size() const;———returns the number of valid nodes in the list

#include<iostream>
#include<list>
using namespace std;

void test()
{
    list<int> l1;
    l1.push_front(2);
    l1.push_front(3);
    l1.push_front(4);
    l1.insert(l.begin(), 1);
    list<int>::iterator it = l1.begin();
    while (it != l1.end())
    {
        cout << *it << " ";
        ++it;
    }
    //1234
    cout << endl;
    l1.pop_front();
    l1.erase(l1.begin());
    vector<int> l2;
    l2.swap(l1);
    it = l2.begin();
    while (it != l2.end())
    {
        cout << *it << " ";
        ++it;
    }
    //34
    cout << l2.empty();
    l2.clear();
    it = l2.begin();
    while (it != l2.end())
    {
        cout << *it << " ";
        ++it;
    }
    //
}

int main()
{
    test();
    return 0;
}

3. Partial interface implementation of list

In the implementation of list, three classes are required, one is node class, one is list class, and the other is iterator class, all of which are stored in the namespace of my_list.

1. Node definition

We use a doubly linked list to implement list, so a node needs pointers to the previous node and the next node as well as the stored data itself

template<class T>
struct list_node
{
    list_node<T>* _next;
    list_node<T>* _prev;
    T _data;
    //构造函数

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

2. Organization of the list class

In the list class we need a member variable to hold the sentinel. Since the linked list does not support random access, you can also add a size_t _size variable to save the number of nodes. If you do not add it, you need an O(N) traversal function, which is much faster than direct access.

template <class T>
class list
{
    typedef list_node<T> node;
    public:
        //成员函数
    private:
        node* _head;//哨兵卫头节点
};

The following are mock implementations of most interfaces:

template <class T>
class list
{
    typedef list_node<T> node;
    public:
    //构建空链表
    void empty_initialize()
    {
        _head = new node(T());//创建一个哨兵卫头
    _head->_next = _head;
    _head->_prev = _head;//prev和next都指向哨兵卫
        _size = 0;
    }
    
    //默认构造          

    list()
    {
        empty_initalize();
    }
    
    //拷贝构造        

    list(const list<T> & lt)
    {
        empty_initalize();//创建空链表          
        for (const auto &e : lt)//将每一个数据按顺序尾插
        {
            push_back(e);
        }
        return *this;
    }
    
    //析构函数
    ~list()
    {
        clear();//清空数据
        //释放哨兵卫并置空
        delete _head;
    _head = nullptr;
    }
    
    //清空数据                        
        
    void clear()
    {
        iterator it = begin();
        while (it != end())
        {
            it = erase(it);//一个一个释放
        }
    }
    
    //赋值运算符重载可能会涉及深拷贝
    list<T>& operator=(const list<T>& lt)
    {
        if (this != &lt)//避免自己给自己赋值
        {
            clear();//清空数据
            for (const auto&e : lt)
            //将每一个数据按顺序尾插,而且用引用还避免了深浅拷贝的问题
            {
                push_back(e);
            }
        }
    }
    
    //任意迭代器位置插入
    iterator insert(iterator pos, const T& x)
    {
        node* newnode = new node(x);
        node* cur = pos._pnode;//迭代器时一个类,类中找这个原生指针
        node* prev = cur->_prev;

    prev->_next = newnode;//链接前面
    newnode->_prev = prev;
    newnode->_next = cur;//链接后面
    cur->_prev = newnode;

        return iterator(newnode);
    }
    
    //头插
    void push_front(const T& x)
    {
        insert(begin(), x);//调用insert
    }
    
    //头删
    void pop_front()
    {
        erase(begin());//调用erase
    }
    
    //尾删
    void pop_back()
    {
        erase(--end());
    }
    
    //尾插
    void push_back(const T& x)
    {

        insert(end(),x)
    }
 
        
 
    iterator erase(iterator pos)
    {
        assert(pos != end());//不能删除头节点
        node* prev = pos._pnode->_prev;//保存pos前的节点
        node* next = pos._pnode->_next;//保存pos后的节点
        prev->_next = next;//将后节点链接前节点
        next->_prev = prev;//将前节点链接后节点
        delete pos._pnode;//删除pos节点--会失效
        return iterator(next);//返回pos节点后的节点
    }
    
     size_t size()
     {
         int i = 0;
         list<int>::iterator it = begin();
         while(it != end())
         {
             it++;
             i++;         
         }
         return i;     
     }
     
    private:
    node* _head;
};

Fourth, the iterator of the list

1. The design idea of ​​the iterator

Iterators have huge advantages in various data structures: iterators can provide a unified iteration method.

There are generally two design ideas for it: one is that continuous memory access can be implemented using the encapsulation of native pointers, and the other is an out-of-the-box data structure such as a linked list. It is achieved by moving the node forward or backward. Dereference is achieved by obtaining the variables inside the iterator. The idea used by list is the latter.

Therefore, although the access methods of various data structures are consistent, the underlying implementations may be completely different. This is also a huge advantage of encapsulation as a basic feature of object-oriented languages.

The iterator will specially design a class, and the member variable saves the node pointer.

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

    node* _pnode;
    __list_iterator(node* p)
        :_pnode(p)
    {}



    
    T operator*()//非const
    {
        return _pnode->_data;
    }
    
    //前置++,迭代器内部指针后挪
    __list_iterator<T>& operator++()
    {
        _pnode = _pnode->_next;
        return *this;
    }


    //后置++,迭代器内部指针后挪
    __list_iterator<T> operator++(int)
    {
        Self tmp(*this);
        _pnode = _pnode->_next;
        return tmp;
    }
    
    //前置--,迭代器内部指针前挪
    __list_iterator<T>& operator--()
    {
        _pnode = _pnode->_prev;
        return *this;
    }
    
    //后置--,迭代器内部指针前挪      

    __list_iterator<T> operator--(int)
    {
        Self tmp(*this);
        _pnode = _pnode->_prev;
        return tmp;
    }
    
    //迭代器的相同和不同不应该是结构体成员的一致,而应该是内部储存的节点的指针一致才可以
    bool operator!=(const __list_iterator<T>& it) const
    {
        return _pnode != it._pnode;
    }

    bool operator==(const __list_iterator<T>& it) const
    {
        return _pnode == it._pnode;
    }
};

2. const iterator

Some people will simply write: const list::iterator lt1 = lt.begin();

Just adding const before the iterator just prevents the iterator itself from being modified, and of course it cannot be + or -.

A const iterator should be such that the elements in the iterator cannot be modified. So we only need an operate* function that uses const to modify the return value, then we can try function overloading, but according to the above error, we can clearly determine that the overload of this function must not use const to modify the this pointer.

So the two functions should look like this:

T& operator*()
{
    return _pnode->_data;
}
 
const T& operator*()
{
    return _pnode->_data;
}

But we need to re-familiarize with the following requirements for function overloading: if the function names and parameters of two functions are the same, the two functions constitute overloading.

They just differ in their return types, which do not constitute overloading, but redefinition. At this point we can only create another class for a function for its proper existence: const_iterator

In this const_iterator, compared with the non-const iterator, the only function that has changed is: the return type of T& operator*() is modified with const.

The result is probably something like this:

struct __list_iterator
{
    //省略重复的代码
    T& operator*()
    {
        return _pnode->_data;
    }
    //省略重复的代码
};
struct const_iterator
{
    //省略重复的代码
    const T& operator*()
    {
        return _pnode->_data;
    }
    //省略重复的代码
};

But don't you feel uncomfortable writing a bunch of repetitive code?

So we introduced the second parameter claas Ref of the iterator class. The type of this Ref is different, and the class it instantiates is also different. So we can let the compiler generate two classes through two display instantiations: __list_iterator and __list_iterator.

In other words, we must define a new class in order to understand the reference overloading of the previous const, but the work of generating this class is handed over to the compiler, not by ourselves.

And name the __list_iterator class Self, which is also convenient for us to modify the code.

template<class T, class Ref>
struct __list_iterator
{
    typedef list_node<T> node;
    typedef __list_iterator<T, Ref> Self;
    node* _pnode;
    __list_iterator(node* p)
        :_pnode(p)
    {}

    
    Ref operator*()
    {
        return _pnode->_data;
    }
    
    Self& operator++()
    {
        _pnode = _pnode->_next;
        return *this;
    }


    
    Self operator++(int)
    {
        Self tmp(*this);
        _pnode = _pnode->_next;
        return tmp;
    }

    Self& operator--()
    {
        _pnode = _pnode->_prev;
        return *this;
    }

    Self operator--(int)
    {
        Self tmp(*this);
        _pnode = _pnode->_prev;
        return tmp;
    }
    
    bool operator!=(const Self& it) const
    {
        return _pnode != it._pnode;
    }

    bool operator==(const Self& it) const
    {
        return _pnode == it._pnode;
    }
};

3. Overloading of the -> operator

We have learned the -> operator in C language before, when we said that a structure pointer p can have the following effects:

#include<stdio.h>
struct test
{
    int a;
};

int main()
{
    struct test example;
    example.a = 0;
    
    struct test* p = &example;
    printf("%d",p->a);
    printf("%d",(*p).a);
    //二者打印的结果是相同的
    return 0;
}

However, the overloading of -> is not the same as in C language. Its overloaded function must return a pointer. This pointer can be any type, but it must be a pointer.

Then in C++, how should we understand p->a based on the above struct test?

I write another piece of code:

#include<iostream>
class test
{
public:
    test()
        :a(0)
    {}
    int* operate->()
    {
        return &a;
    }
private:
    int a;
};

int main()
{
    test example;
    printf("%d",(example->a));
    return 0;
}

It should be like this, p->a can be understood as p->->a, a -> is omitted in the middle, the first arrow is a function call, returning the address of a, and the following -> can be understood as dereferencing. So don't say it is a dereference as soon as you see the -> operator, it may also be a function call.

We also want to overload this -> at this time, like this:

T* operator->()
{
    return &_pnode->_data;
}

But at this time the problem happened again. The * operator overload we use can achieve the effect of const or non-const, but if we only write a non-const version, people can borrow this pointer to change the content they don't want to modify. In other words, we still have to face the same situation as above, generating an extra class for a function, which is true, but this function can be placed in the const iterator class, just one more parameter to control it. At this point we have introduced the third parameter of the template, and the compiler still forms two classes.

They are: ordinary iterator __list_iterator and const iterator __list_iterator

template<class T, class Ref, class Ptr>
struct __list_iterator
{
    typedef list_node<T> node;
    typedef __list_iterator<T, Ref, Ptr> Self;
    node* _pnode;
    
    __list_iterator(node* p)
        :_pnode(p)
    {}

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

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

    Self& operator++()
    {
        _pnode = _pnode->_next;
        return *this;
    }

    Self operator++(int)
    {
        Self tmp(*this);
        _pnode = _pnode->_next;
        return tmp;
    }

    Self& operator--()
    {
        _pnode = _pnode->_prev;
        return *this;
    }

    Self operator--(int)
    {
        Self tmp(*this);
        _pnode = _pnode->_prev;
        return tmp;
    }

    bool operator!=(const Self& it) const
    {
        return _pnode != it._pnode;
    }

    bool operator==(const Self& it) const
    {
        return _pnode == it._pnode;
    }
};

4. Reverse iterator

The purpose of our learning STL is that these data structures have already been implemented by the big guys, unlike C language where we use various data structures and need to implement various interfaces of the data structure. Since there are ready-made, why not use it? The way to use other people's code to achieve another function is called code reuse.

In other words, the reverse iterator is actually the encapsulation of the forward iterator, and it is the same in the implementation of STL.

The rbegin of the reverse iterator is the end of the forward iterator, and the rend of the reverse iterator is the begin of the forward iterator, but we need to move one bit before the current forward iterator position when overloading and dereferencing For dereferencing, the ++ and -- of all reverse iterators can be reversed with the forward direction.

self& operator++() 
{
    --current;
    return *this;
}
self operator++(int) 
{
    self tmp = *this;
    --current;
    return tmp;
}
self& operator--() 
{
    ++current;
    return *this;
}
self operator--(int) 
{
    self tmp = *this;
    ++current;
    return tmp;
}

 

Guess you like

Origin blog.csdn.net/qq_65285898/article/details/128978410