目录
list需要实现的接口
结点类的创建
迭代器类的实现
构造函数
++运算符的重载
--运算符的重载
!=运算符重载和==运算符重载
operator*
operator->
list的模拟实现
构造函数
拷贝构造函数
赋值运算符重载函数
析构函数
迭代器相关函数
begin和end
front和back
push_front和push_back
pop_front和pop_back
其他函数
size
clear
empty
swap
list需要实现的接口
namespace cl
{//模拟实现list当中的结点类template<class T>struct _list_node{//成员函数_list_node(const T& val = T()); //构造函数//成员变量T _val; //数据域_list_node<T>* _next; //后继指针_list_node<T>* _prev; //前驱指针};//模拟实现list迭代器template<class T, class Ref, class Ptr>struct _list_iterator{typedef _list_node<T> Node;typedef _list_iterator<T, Ref, Ptr> self;_list_iterator(node* node); //构造函数//各种运算符重载函数self operator++();self operator--();self operator++(int);self operator--(int);bool operator==(const self& s) const;bool operator!=(const self& s) const;Ref operator*();Ptr operator->();//成员变量Node* _node; //一个指向结点的指针};//模拟实现listtemplate<class T>class list{public:typedef _list_node<T> Node;typedef _list_iterator<T, T&, T*> iterator;typedef _list_iterator<T, const T&, const T*> const_iterator;//默认成员函数list();list(const list<T>& lt);list<T>& operator=(const list<T>& lt);~list();//迭代器相关函数iterator begin();iterator end();const_iterator begin() const;const_iterator end() const;//访问容器相关函数T& front();T& back();const T& front() const;const T& back() const;//插入、删除函数void insert(iterator pos, const T& x);iterator erase(iterator pos);void push_back(const T& x);void pop_back();void push_front(const T& x);void pop_front();//其他函数size_t size() const;void clear();bool empty() const;void swap(list<T>& lt);private:Node* _head; //指向链表头结点的指针size_t _szie;};
}
结点类的模拟实现:
list在底层中,就是以带头双向循环链表实现的
所以实现list我们首先的创建一个结点类。
该结点类需要三个成员变量:
1. 数据
2. 上一个结点的指针
3.下一个结点的指针
结点类的创建
_list_node
template<class T>
struct list_node
{list_node* _next;list_node* _prev;T _val;list_node(list_node<T> val = T()):_next(nullptr),_prev(nullptr),_val(val){}
};
在结点类中我们需要一个构造函数来帮我们完成结点以及数据的初始化,因为结点类需要被_list_iterator和list使用,所以这里我们使用struct
迭代器类的实现
之前string和vector我们都没说需要实现迭代器类,为什么list需要实现一个迭代器类呢?
因为string和vector对象都将其数据存储在了一块连续的内存空间,我们通过指针的自增自减以及解引用操作等一系列操作,就可以对相应位置的数据进行操作,所以string和vector当中的迭代器就是原生指针
对于list来说,它的构成并不是一块连续的空间,它的结点在内存中的位置是随机的,我们不能通过结点指针的自增自减以及解引用操作对相应结点的数据进行操作
迭代器的意义是,让使用者不必在意容器的底层实现原理,可以用简单统一的方式对容器内数据进行访问
因为迭代器·结点指针的行为不能满足迭代器定义,那么我们可以对这个结点指针进行封装,对结点指针的各种运算符进行重载,使得我们可以像使用string和vector当中的迭代器一样去使用list当中的迭代器
迭代器类的模板参数说明
_list_iterator<class T, class Ref, class Ptr>
在list类中,我们typedef了两种类型的模板类,分别实现的是const 迭代器和非 const 迭代器
typedef _list_iterator<T, T&, T*> iterator
typedef _list_iterator<T, const T&, const T*> const_ierator
从这里就可以看出,Ref是引用类型,Ptr是指针类型
当我们使用不同迭代器时编译器就会根据模板来实例化出一个普通迭代器,当我们使用const迭代器时,编译器就会实例化出一个const迭代器对象
若没有设计这三种模板参数,那么就不能很好的区分普通迭代器和const迭代器
构造函数
迭代器类实际上就是对结点指针进行了封装,成员变量只有一个,那就是结点指针,其构造函数直接根据所给结点指针构造出一个迭代器对象即可
_list_iterator(Node* node):_node(ndoe)
{}
++运算符的重载
前置++,作用是让结点指针指向后面一个位置,然后返回自增的结点指针即可
self& operator++()
{_node = _node->next;return *this;
}
对于后置++,我们需要记录结点指针当前的位置,让结点指针向指向后一个结点,然后返回自增前的结点指针即可
self operator++(int)
{self tmp(*this);_node = _node->next;return tmp;
}
注意:self是当前迭代器对象的类型:
_list_iterator<class T, class Ref, class Ptr>
--运算符的重载
前置--,作用是让结点指针指向前面一个位置,然后返回自减的结点指针即可
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
}
operator*
当我们使用解引用操作符时,是想得到该位置的数据内容,所以我们直接返回当前结点指针所指向的数据即可,但是这里需要解引用返回,得到数据之后,我们后面可能会对这个数据进行修改
Ref operator*()
{return _node->_val;
}
operator->
指向运算符重载我们只需要返回数据的地址就可以了
Ptr operator->()
{return &_node->val
}
但是看到这里,你可能觉得有点问题
这里应该是两个箭头去访问it,第一个->调用指向运算符重载函数,返回一个指针,然后再通过指针加指向运算符去访问对象当中的变量
但是一个地方出现两个箭头可读性太差了,为了增加可读性,编译器做了优化处理,优化了一个箭头
list的模拟实现
list是一个带头双向循环链表
构造函数
将初始化操作进行封装,后续会多次使用到,创建一个指针结点_head,并且让前驱指针和后驱指针指向自己即可完成初始化
void empty_init()
{_head = new Node;_head->_next = _head;_head->_prev = _head;
}list()
{empty_init();
}
拷贝构造函数
创建一个新头结点_head,使前驱指针和后驱指针指向自己,遍历lt对象,将lt的数据通过尾插的方式给新的容器
list(const list<T>& lt)
{empty_init();//这里加引用提高效率,减少拷贝//加const是防止e不被篡改for (const auto& e : lt){push_back(e);}
}
赋值运算符重载函数
传统写法:
list<T>& opertor=(const list<T>& lt){if (*this != <){clear();//清空容器for (const auto& e : lt){push_back(e);}}return *this;}
现代写法:
这个写法代码量较少,首先利用编译器机制,故意不使用引用接收参数,通过编译器自动调用list的拷贝构造函数生成一个临时对象,使用这个临时对象给新容器进行交换即可完成操作,结束之后lt会调用析构函数销毁
//现代写法:
list<T>& opertor = (list<T> lt)
{swap(lt);return *this;
}
析构函数
使用clear函数清理容器当中的数据,然后将头结点释放,最后将头指针置空
~list()
{clear();delete _head;_head = nullptr;
}
迭代器相关函数
begin和end
begin()迭代器指向第一个元素,end()迭代器指向末尾元素的后一个元素
iterator begin()
{//隐式类型转换// return _head;//强制类型转换return iterator(_head->_next);
}iterator end()
{//隐式类型转换return _head;//强制类型转换//return iterator(_head);
}
以上两种方式都可以
第一种是Node*类型对iterator的强制类型转换
第二种是Node*类型对itreator的隐式类型转换
还有const类型的迭代器
const_iterator end() const
{//隐式类型转换// return _head->prev;//强制类型转换return iterator(_head);
}const_iterator begin() const
{//隐式类型转换// return _head;//强制类型转换return iterator(_head->_next);
}
front和back
//获取尾部元素
T& back() const
{return *(end() - 1);
}
//获取头部元素
T& front() const
{return *begin();
}//获取尾部元素
const T& back() const
{return *(end() - 1);
}
//获取头部元素
const T& front() const
{return *begin();
}
push_front和push_back
pop_front和pop_back
void push_front(const T& x)
{insret(begin(), x);
}void push_back(const T& x)
{insert(end(), x);
}void pop_front()
{erase(begin());
}void pop_back()
{erase(--end());
}
插入,删除函数
insert
pos位置前插入新结点
先通过所给迭代器得到该位置出的结点指针cur,然后通过cur指针找到前一个位置的结点指针prev
然后根据所给val的值建立一个新结点指针,然后实现和prve,cur的双向连接连接
iterator insert(iterator pos, const T& val)
{//在pos位置前插入,保存pos前面的地址,以防找不到Node* cur = pos->_node;Node* prev = cur->_prev;Node* newnode = new Node(val);cur->_prev = newnode;newnode->_next = cur;newnode->_prev = prev;prev ->_next = newnode;++_size;//返回插入结点的地址,更新迭代器,以免迭代器失效return newnode;
}
erase
先根据所给迭代器得到该位置处的结点指针cur,通过cur指针找到前一个位置的结点prev和后一个位置的结点next,然后释放cur,最后将prev与next两个结点连接起来
iterator erase(iterator pos)
{assert(pos != end());Node* cur = pos._nodeNode* Prev = cur->prev;Node* Next = cur->next;Prev->next = next;Next->_prev = prev;delete cur;--_size;return pos;
}
其他函数
size
这里size的设计是,创建_size成员,使用增加结点的函数(例:push_back insert时)时,对_size进行++处理,进行删除操作时_size做--操作,即_size就是当前结点个数
size_t size() const
{return _size;
}
clear
清理链表,删除所有结点,erase涉及迭代器失效问题,这里的处理方案是,让erase返回下一个结点的指针,不断去更新lt.
clear这里不能给clear加const,加上const了,就代表给我需要删除的对象加上了const,*this就不能进行删除操作了
void clear(){iterator it = begin();while (it != end()){//接收下一个位置的指针,避免迭代器失效it = erase(it);}_size = 0;}
empty
判断链表是否为空
bool empty()
{return begin() == end();
}
swap
我们访问的是标准库里面的swap,所有需要再swap前面加上::(域作用限定符)
void swap(list<T>& lt)
{::swap(_node, lt._node);::swap(_size, lt._size);
}