list(c++)
前言
这里我们学习的是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的学习,其中最重要的就是他的迭代器的设计,这里是最难的。其他部分都是高度的重复,像增删查改就是简单的链表编写。