前言
这里我们学习的是gcc下STL版本的list。STL里的list容器底层是一个双向带头节点的一个链表,不再是单链表,单链表现实运用很少,更多的是双向带头链表。
正文
list利用
默认成员函数
构造函数 | 接口说明 | list (size_type n, const value_type& val = value_type()) | 构造的list中包含n个值为val的元素 | list() | 构造空的list | list (const list& x) | 拷贝构造函数 | list (InputIterator first, InputIterator last) | 用[first, last)区间中的元素构造list |
迭代器
函数声明 | 接口说明 | begin + end | 返回第一个元素的迭代器+返回末了一个元素下一个位置的迭代器 | rbegin + rend | 返回第一个元素的reverse_iterator,即end位置,返回末了一个元素下一个位置的 reverse_iterator,即begin位置 |
容量
函数声明 | 接口说明 | empty | 检测list是否为空,是返回true,否则返回false | size | 返回list中有效节点的个 |
增删查改
函数声明 | 接口说明 | push_front | 在list首元素前插入值为val的元素 | pop_front | 删除list中第一个元素 | push_back | 在list尾部插入值为val的元素 | pop_back | 删除list中末了一个元素 | insert | 在list position 位置中插入值为val的元素 | erase | 删除list position位置的元素 | swap | 互换两个list中的元素 | clear | 清空list中的有效元素 |
代码演示
这里list利用很简朴,我就不一个一个演示了。就用一个测试案例测试下吧- void Print(const li::list<int>& lt)
- {
- li::list<int>::const_iterator it = lt.begin();
- while (it != lt.end())
- {
- //(*it)++;
- cout << *it << " ";
- ++it;
- }
- }
- void list_test01()
- {
- li::list<int> lt;
- lt.push_back(1);
- lt.push_back(2);
- lt.push_back(3);
- lt.push_back(4);
- Print(lt);
- cout << endl;
- //这里是拷贝构造 内置类型 浅拷贝
- li::list<int>::iterator it = lt.begin();
- while (it != lt.end())
- {
- (*it)++;
- cout << *it << " ";
- ++it;
- }
- }
复制代码 这里我就简朴测试下,必要注意的是,erase之后迭代器被开释失效,记得用返回值担当。clear这里底层上就是开释掉了所有的节点,只留下一个头节点。因为链表结构决定了它是可以部分删除容量的,不像连续地址的顺序表。因此,list的clear就直接删除节点了,而不是仅仅清除数据。
list模仿实现
成员变量
- template<class T>
- struct list_node
- {
- list_node<T>* _next;
- list_node<T>* _prev;
- T _val;
- list_node(const T& x = T())
- :_next(nullptr),
- _prev(nullptr),
- _val(x)
- {
- }
- };
- template<class T>
- class list
- {
- typedef list_node<T> Node;
- //...
- //
- private:
- Node* _head;
- size_t _size;
- }
复制代码 从链表开始,就有点复杂了,利用许多封装和重命名。这里我们必要先实现一个结构体(结点),
这里必要重名命下这个类模板,这是为啥呢?这是一个风俗问题。祖师爷在研究STL的时候,他本身也是从c语言过度来的,虽然类模板也是它老人家研究出来的。但是在声明一个变量的时候,加上一个模板,很容易忘记,尤其风俗了c语言的命名变量的语法。以是,这里重命名下是一种风俗问题,并且也可以简化代码。这也是一个好风俗,因为你的模板可不肯定只有一个,如果你的leader突然让你改一下这个容器,增加一个模板,你难不成一个一个该吗?这时候重命名就香许多了,只必要改一下。
这里还有一个size,有这个参数我们就可以很直观的观察链表,和判定链表是否为空。有利于我们更直观的利用链表和调试。
默认成员函数
- void empty_init()
- {
- _head = new Node;
- _head->_next = _head;
- _head->_prev = _head;
- _size = 0;
- }
- list()
- {
- empty_init();
- }
- //不能用const引用
- void swap(list<T>& lt)
- {
- std::swap(_head, lt._head);
- std::swap(_size, lt._size);
- }
- list<T>& operator=(list<T> lt)
- {
- swap(lt);
- return *this;
- }
- list(const list<T>& lt)
- {
- empty_init();
- for (auto& e : lt)
- {
- push_back(e);
- }
- }
- ~list()
- {
- clear();
- delete _head;
- _head = nullptr;
- }
复制代码 这里构造函数里面套了一个函数初始化,这也是向库里版本靠齐。这里的赋值重载是用的现代写法,直接swap互换,传统的写法是直接手搓一遍(不推荐)。这里的拷贝构造函数里面复用了push_back,析构函数复用了clear(),这也体现list的高度封装性,其实库里的比这里还复杂。动态内存分配也不是new而是封装了一个空间配置器,就是效率高点。我在vector的博客也提及过。我们学习STL的代码,强烈不发起一行一行看,否则你会直接想放弃的。这里我们先知道个大要逻辑。到下文会解释clear和push_back()这两个函数,其实push_back里面也是套用了insert的。就像套娃一样,这里的代码。
迭代器
重点 重点 重点 重要的事说三遍,list的迭代器可谓是学习list的重点也是一个难点,先问一下,如何设计list的迭代器?可以像vector里那样吗?直接指针吗?可是这里是一个自定义类型呀?解引用也不是结点的值呀?因此,这里肯定是要对*赋值重载的。以是,就可以先否定直接指针了,这肯定不行的!既然要赋值重载,那我们就肯定要自定义类型呀?对,这里的迭代器就是一个自定义类型,里面包含着一个结点指针,还有构造函数,完成初始化。这里不介绍反向迭代器,因为反向迭代器的设计和正向有点不一样,这里我们先学习正向迭代器,正向迭代器就够喝一壶了。- 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* node)
- :_node(node)
- {}
- Ref operator*()
- {
- return _node->_val;
- }
- Ptr operator->()
- {
- return &(_node->_val);
- }
- 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& it) const
- {
- return _node != it._node;
- }
- bool operator==(const self& it) const
- {
- return _node == it._node;
- }
- };
复制代码 这里先给大家看一下完整版的迭代器设计,是不是很蒙,发现和我想的有点不一样呀!-
- struct _list_iterator
- {
- typedef list_node<T> Node;
- Node* _node;
- _list_iterator(Node* node)
- :_node(node)
- {}
- };
-
复制代码 这里先看我上面这个代码,是不是发现又看懂了。这里我们学习迭代器必要一步一步拆开学,他的设计其实有点难理解。我们上面讲了,迭代器必要设计成一个自定义类型,必要包装一下结点的指针,以便我们访问。这里的代码是一个雏形,有了这个雏形,对于begin(),end(),我们就完成的差不多了。但还必要大大的完善。这里还要对*,++等运算符赋值重载。那为啥有三个模板呢?这里是根据需求来设定的。首先*赋值重载返回的就是数据本身,以是数据的类型模板,我们必要你上传,直接引用,纵然结构体的数据还是什么其他自定义类型数据,都可以返回精确的值。末了一个模板为啥要传一个指针呢?迭代器模仿的是指针的举动?既然是指针那么就可以用箭头访问结构体。以是,这里的迭代器也满意了这种要求。利用箭头访问结点的元素。同理,本身上传一个指针,这样后续更改代码,我们只用修改这部分代码。这里是如何设计const修饰的迭代器呢?这时候我们的模板就香许多了,我们这必要在上传模板的时候加上一个 const,这样子你必要什么我就返回什么。这也证实我们为啥要搞三个模板,就是因为,后面的代码设计是必要利用的。其实刚开始学这里的迭代器的时候,我也很蒙蔽,但是学到后面,你才发现祖师爷的这套设计有多厉害!这里的命名也是向库里靠拢的,发起大家以后命名也尽量向库里靠拢。- typedef list_node<T> Node;
- public:
- typedef _list_iterator<T,T&,T*> iterator;
- typedef _list_iterator<T, const T&,const T*> const_iterator;
- iterator begin()
- {
- return _head->_next; //单参数的构造函数支持隐式类型转换
- //return iterator(_head->_next);
- }
- iterator end()
- {
- return _head;
- }
- const_iterator begin() const
- {
- return _head->_next;
-
- }
- const_iterator end() const
- {
- return _head;
- }
复制代码 虽然这里链表里的迭代器看起来很简朴,和vector差不多。但底层是很复杂的,这里也体现了迭代器的重要。为啥我们要用迭代器遍历数据,祖师爷为啥要研究出个迭代器。就是因为,有了迭代器,我们可以统一用法访问任何容器的任何形式数据。不像数组就要用下标,链表就要用指针。不方便程序员的开发。有了迭代器,STL里的任何容器都可以用迭代器访问。从list开始,迭代器才真正开始上难度,而不是简朴的复用指针,这里我们学习list的迭代器的时候,不仅仅要懂list迭代器是如何实现,更要体会的是祖师爷设计的理念和头脑。更重要的是人家这种头脑,极大进步了编程简化。其实学习c++,c++意义不仅仅在利用c++开发软件和编写底层体系上,而且c++的设计的语法和理念为其他语言提供了模板,许多语言都有c++的影子,尤其像java语言,可以说是一个接近满分的抄作业选手。
容量
- bool empty()
- {
- return _size == 0;
- }
- size_t size()
- {
- return _size;
- }
复制代码 这里就是简朴返回,没啥好说的。
增删查改
- void clear()
- {
- iterator it = begin();
- while (it != end())
- {
- it = erase(it);
- }
- _size = 0;
- }
- void push_back(const T& x)
- {
- /*Node* tail = _head->_prev;
- Node* newNode = new Node(x);
- tail->_next = newNode;
- newNode->_prev = tail;
- newNode->_next = _head;
- _head->_prev = newNode;*/
- insert(end(), x);
- }
- void push_front(const T& x)
- {
- insert(begin(), x);
- }
- void pop_back()
- {
- erase(--end());
- }
- void pop_front()
- {
- erase(begin());
- }
- iterator insert(iterator pos, const T& x)
- {
- Node* cur = pos._node;
- Node* prev = cur->_prev;
- Node* next = cur->_next;
- Node* newNode = new Node(x);
- prev->_next = newNode;
- newNode->_prev = prev;
- newNode->_next = cur;
- cur->_prev = newNode;
- _size++;
- return newNode;
- }
- iterator erase(iterator pos)
- {
- assert(pos != end());
- Node* cur = pos._node;
- Node* prev = cur->_prev;
- Node* next = cur->_next;
- prev->_next = next;
- next->_prev = prev;
- delete cur;
- _size--;
- return next;
- }
- void swap(list<T>& lt)
- {
- std::swap(_head, lt._head);
- std::swap(_size, lt._size);
- }
复制代码 这里要说一下,push_back和pop_back分别内部调用了insert和erase, 这里增删查改不消一个一个写,库里就是把大量重复的代码,封装起来。以后利用到这段代码就调用这个封装的函数,险些全是封装。现实上,底层还是那几个指针互换位置。
源码
- #pragma once
- #include <assert.h>
- #include <iostream>
- namespace li
- {
- template<class T>
- struct list_node
- {
- list_node<T>* _next;
- list_node<T>* _prev;
- T _val;
- list_node(const T& x = T())
- :_next(nullptr),
- _prev(nullptr),
- _val(x)
- {
- }
- };
- 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* node)
- :_node(node)
- {}
- Ref operator*()
- {
- return _node->_val;
- }
- Ptr operator->()
- {
- return &(_node->_val);
- }
- 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& it) const
- {
- return _node != it._node;
- }
- bool operator==(const self& it) const
- {
- return _node == it._node;
- }
- };
- template<class T>
- class list
- {
- typedef list_node<T> Node;
- public:
- typedef _list_iterator<T,T&,T*> iterator;
- typedef _list_iterator<T, const T&,const T*> const_iterator;
- iterator begin()
- {
- return _head->_next; //单参数的构造函数支持隐式类型转换
- //return iterator(_head->_next);
- }
- iterator end()
- {
- return _head;
- }
- const_iterator begin() const
- {
- return _head->_next;
-
- }
- const_iterator end() const
- {
- return _head;
- }
- void empty_init()
- {
- _head = new Node;
- _head->_next = _head;
- _head->_prev = _head;
- _size = 0;
- }
- list()
- {
- empty_init();
- }
- //不能用const引用
- void swap(list<T>& lt)
- {
- std::swap(_head, lt._head);
- std::swap(_size, lt._size);
- }
- list<T>& operator=(list<T> lt)
- {
- swap(lt);
- return *this;
- }
- list(const list<T>& lt)
- {
- empty_init();
- for (auto& e : lt)
- {
- push_back(e);
- }
- }
- ~list()
- {
- clear();
- delete _head;
- _head = nullptr;
- }
- bool empty()
- {
- return _size == 0;
- }
- size_t size()
- {
- return _size;
- }
- void clear()
- {
- iterator it = begin();
- while (it != end())
- {
- it = erase(it);
- }
- _size = 0;
- }
- void push_back(const T& x)
- {
- /*Node* tail = _head->_prev;
- Node* newNode = new Node(x);
- tail->_next = newNode;
- newNode->_prev = tail;
- newNode->_next = _head;
- _head->_prev = newNode;*/
- insert(end(), x);
- }
- void push_front(const T& x)
- {
- insert(begin(), x);
- }
- void pop_back()
- {
- erase(--end());
- }
- void pop_front()
- {
- erase(begin());
- }
- iterator insert(iterator pos, const T& x)
- {
- Node* cur = pos._node;
- Node* prev = cur->_prev;
- Node* next = cur->_next;
- Node* newNode = new Node(x);
- prev->_next = newNode;
- newNode->_prev = prev;
- newNode->_next = cur;
- cur->_prev = newNode;
- _size++;
- return newNode;
- }
- iterator erase(iterator pos)
- {
- assert(pos != end());
- Node* cur = pos._node;
- Node* prev = cur->_prev;
- Node* next = cur->_next;
- prev->_next = next;
- next->_prev = prev;
- delete cur;
- _size--;
- return next;
- }
- private:
- Node* _head;
- size_t _size;
- };
- }
复制代码 总结
学习list,学会利用是其次,更重要的是理解list底层的编写的理念和设计。这才是最终要的,关于list的学习,此中最重要的就是他的迭代器的设计,这里是最难的。其他部分都是高度的重复,像增删查改就是简朴的链表编写。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
|