目录
节点的准备
list类
push_back
stl容器的遍历和修改
begin
end
!=
++
重载*
效果展示:
const迭代器
方法一:
方法二:
->的重载
insert
push_front
erase
展示效果
pop_back && pop_front
效果展示
clear()&& 析构函数
节点的准备
我们在创建节点的时候,选用struct,而不是class,因为struct是默认公有的,我们在进行类成员访问的时候就不会受到约束
template<class T>struct ListNode {ListNode<T>* _prev;ListNode<T>* _next;T _data;ListNode(const T& x=T()):_prev(nullptr),_next(nullptr),_data(x){}};
为什么在初始化的时候,传值的时候用const T& x = T(),初始化的时候也是要用x
因为我们在创建链表的时候,不一定就是整型链表,可能是double链表,可能string链表,甚至有可能是自定义链表
所以试想一下,0可以作为string的初始值吗,显然是不能的
我们又知道,现在内置类型现在也支持初始化了,所以为了保证不会出错,统一采用T()初始化
知道了节点的基本属性后,就可以利用节点类(ListNode类)来创建链表(list类)了
list类
因为ListNode是struct,其类成员是公开的,所以在创建list类的时候,所以可以在类里面访问ListNode成员
template<class T>class list {typedef ListNode<T> Node;private:Node* _head;public:list() {_head = new Node;_head->_next = _head;_head->_prev = _head;}
push_back
void push_back(const T& x) {Node* newnode = new Node(x);Node* tail = _head->_prev;tail->_next = newnode;newnode->_prev = tail;newnode->_next = _head;_head->_prev = newnode;}
在push_back成功后,我们该如何访问遍历我们的list呢
我们访问的方式普遍是用迭代器
stl容器的遍历和修改
我们在之前比如string,vector之类的,在底层实现的迭代器直接通过使用指针即可完成目的,原因就是它们的空间地址在底层是连续的,但是链表的空间地址的底层不连续,也就是说我们通过使用指针++得到的指针指向的下一个数据可能不是下一个节点
我们又该如何实现它呢?
我们可以创建一个迭代器类,通过迭代器来实现链表的遍历访问,将节点交给迭代器类(__list_iterator),从而实现访问效果
list<int>::iterator it = l1.begin();while (it != l1.end()) {cout << *it << " ";it++;}cout << endl;
那么我们就会知道我们缺少begin, end, 重载!= ,重载++,以及重载*
begin
因为此链表是带有哨兵位,所以一开始访问的地方应该是哨兵位的下一位,即_head->_next;
iterator begin() {return _head->_next;}
end
我们需要在end位置结束,end又是带头双向循环列表,所以我们要在链表访问绕一圈后再回来之时在头节点处停止,即_head;
iterator end() {return _head;}
!=
我们知道内置类型的是可以直接使用运算符,但是自定义类型则需要自己重新重载
需要注意的是这些运算符的比较是在迭代器类里,即__list_iterator类,所以不能在list类里重载
其次,比较也是通过迭代器类比较,创建的it是属于迭代器的,即iterator it
比什么不同呢,比地址不同就可以了
bool operator!=(const self& x) {return _node != x._node;}
++
++依旧分为前置和后置,很简单,令其指向下一节点即可,唯一的是后置++需要返回的是不变的值,返回不变值的话,就需要一个值来代替,tmp
self& operator++() {_node = _node->_next;return *this;}self operator++(int) {self tmp(*this);_node = _node->_next;return tmp;}
关于self& operator++(int)中,self tmp(*this)里,一开始我的疑惑是:欸,人家_node不是指针类吗,拷贝构造我也没写,用它进行拷贝构造的话,只会进行值拷贝哇,不会进行深度拷贝呀。
到后面想想,在实现list类里,我们本身就是需要通过迭代器实现间接控制list的内容,所以要修改的东西也同样一样
不写析构函数也是如此道理,迭代器不用析构,因为这些成员都是类模板的,只是借给你用一下的,结果没道理说你用了之后还把我的成员给释放掉
重载*
T& operator*() {return _node->_data;}
效果展示:
const迭代器
void print_list(const list<int>& lt) {list<int>::iterator it = lt.begin();while (it != lt.end()) {cout << *it << " ";it++;}cout << endl;}
当我们传const类型的对象的时候,因为我们写的end,begin等等是非const的
那么我们便会想通过给list加const来改变此情况
const list<int>::iterator it = lt.begin();
报出大量的错误,究其原因
const对象it不能够对其进行修改,因此it++是错误的
相对于我们想要的const类型的迭代器,我们想要的是it能够进行++,而不是相反,我们要对其进行遍历,只是不要对里面的内容进行修改而已,所以我们在list<T>::iterator it 前面加上const,就直接限定了我连遍历都不能遍历
那我们应该如何做到我们能够遍历链表,但又不会修改里面的值呢,即实现const版的iterator
方法一:
实现一个const_iterator,在iterator的基础上,复制粘贴一份,唯一需要改变的就是operator*
template<class T>struct __list_const_iterator{typedef ListNode<T> Node;typedef __list_const_iterator<T> self;Node* _node;__list_const_iterator(Node* node):_node(node){}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;}const T& operator*() {return _node->_data;}bool operator!=(const self& s) {return _node != s._node;}bool operator==(const self& s) {return _node == s._node;}};
但是这样有会显得很冗余,因为不同的只是类名跟operator*
那么我们的解决方法是:
方法二:
在原来的类模板上增加一个参数
将__list_const_iterator 舍弃掉,将__list_iterator中的类模板从template<class T>变为template<class T,class Ref>
将operator的返回类型在重新定义一下
在list类中,对 iterator 和 const_iterator 重新typedef一下
经过此番操作,我们就可以将原本需要两个类来实现的遍历通过修改类模板参数,能够整合到一个类里面
->的重载
当我们的链表存储的不是内置类型的时候,或者链表存储的结构能够存放多个数值,比如类,结构体之类的时候,我们就会通常用 -> 来进行访问我们想要的数值
先看一串代码
struct BB {int _b1;int _b2;BB(int b1=1,int b2=1):_b1(b1),_b2(b2){}
};int main() {list<BB> l;l.push_back({1,1});l.push_back({2,2});l.push_back({3,3});list<BB>::iterator it = l.begin();while (it != l.end()) {cout << it->_b1 << "," << it->_b2 << endl;it++;}return 0;
}
上面这串代码,在STL模板类当中是很自然的,没有任何问题
但在我们的手撕链表中,好像就出现了些许问题
编译器给出的解释也是比较明显了
不是类成员,没有重载等,所以我们在自定义list的时候,需要我们自己重载一下operator->
T* operator->(){return &_node->_data;
}
其在对上 it->_a1 或者 it -> _a2 的原型就是
cout << it.operator->()->_a1 << endl;
cout << it.operator->()->_a2 << endl;
因此本来应该是两个箭头的,但是为了可读性省略一个箭头,即原来是it -> -> _a1,现在为了可读性变成了 it->_a1
当然箭头所表示的指针也有const和 非const,因此要对类模板参数进行扩充
template<class T,class Ref,class Ptr>struct __list_iterator{typedef ListNode<T> Node;typedef __list_iterator<T,Ref,Ptr> self;Node* _node;...Ptr operator->() {return &_node->_data;}}template<class T>class list{typedef ListNode<T> Node;public:typedef __list_iterator<T,T&,T*> iterator;typedef __list_iterator<T,const T&,const T*> const_iterator;...}
insert
先写insert是因为,后面的其它函数可以适当的复用insert
要记录下插入位置的前一个节点,不然随意更换的话,会找不到前一个节点
iterator insert(iterator pos,const T& x) {Node* newnode = new Node(x);Node* cur = pos._node;Node* prev = cur->_prev;cur->_prev = newnode;newnode->_next = cur;prev->_next = newnode;newnode->_prev = prev;return newnode;}
push_front
void push_front(const T& x) {insert(begin(), x);}
erase
删除某一节点
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;return next;}
展示效果
pop_back && pop_front
void pop_back() {erase(--end());}void pop_front() {erase(begin());}
在实现pop_back之前,我们还要实现一下重载--
self& operator--() {_node = _node->_prev;return *this;}self operator--(int) {self tmp(*this);_node = _node->_prev;return tmp;}
效果展示
clear()&& 析构函数
void clear() {iterator it = begin();while (it != end()) {it = erase(it);}}~list() {clear();delete _head;_head = nullptr;}
以上便是此次博文的学习内容,如有错误,还望大佬指正,谢谢阅读!