【C++】List模拟实现过程中值得注意的点
时间:2024-03-26 11:15:46 来源:网络cs 作者:纳雷武 栏目:运营工具 阅读:
👀樊梓慕:个人主页
🎥个人专栏:《C语言》《数据结构》《蓝桥杯试题》《LeetCode刷题笔记》《实训项目》《C++》《Linux》《算法》
🌝每一个不曾起舞的日子,都是对生命的辜负
目录
前言
1.List迭代器
2.适配器
3.迭代器失效
4.->操作符的重写
5.模拟实现源码
前言
本篇文章旨在记录博主在模拟实现vector容器中遇到的一些问题,都是一些需要注意的细节问题,希望与大家共勉。
欢迎大家📂收藏📂以便未来做题时可以快速找到思路,巧妙的方法可以事半功倍。
=========================================================================
GITEE相关代码:🌟fanfei_c的仓库🌟
=========================================================================
1.List迭代器
在之前模拟实现『 String和Vector』时,我们都采用的『 原生指针』实现迭代器,那么到List这里还可以么?
这我们就要搞清楚『 迭代器存在的意义』。
我们希望可以通过迭代器来实现对某一容器的遍历访问,例如对迭代器++或--;
迭代器:让使用者可以不必关心容器的底层实现,可以用简单统一的方式对容器内的数据进行访问。
之前的String和Vector我们可以简单的利用原生指针实现迭代器是因为String和Vector底层的物理空间是连续的,所以++和--当然可以访问到对应的对象。
可到了List中,List的底层是带头双向循环链表,链表的物理空间并『 不连续』,所以我们需要对迭代器进行一定的『 封装』,让迭代器『 符合统一的迭代器使用规则』。
如何封装呢?
提示:比如重载各种运算符,++运算符的底层可以为链表的p=p->next;
注意:end迭代器是最后一个元素的下一个位置,所以end一般指向的是『 头节点』。
头节点不存储数据。// List的节点类template<class T>struct ListNode{ ListNode<T>* _next; ListNode<T>* _prev; T _data; ListNode(const T& val = T()) :_next(nullptr) , _prev(nullptr) , _data(val) {}};//List的迭代器类template<class T, class Ref, class Ptr>class __list_iterator{public: typedef ListNode<T> Node; typedef __list_iterator<T, Ref, Ptr> Self; Node* _node; __list_iterator(Node* x) :_node(x) {} Ref operator*() { return _node->_data; } Ptr operator->() { return &_node->_data; } 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& s) { return _node != s._node; } bool operator==(const Self& s) { return _node == s._node; }};
2.适配器
我们知道迭代器一般需要两个形式,一种为非const迭代器,一种为const迭代器用来满足不同的使用场景,但你有没有发现上面的代码,似乎只有一种形式???
其实并不是,如果我们按照以前的写法,应为:
//非const迭代器类template<class T>class __list_iterator{public: typedef ListNode<T> Node; typedef __list_iterator<T> Self; Node* _node; __list_iterator(Node* x) :_node(x) {} T& operator*() { return _node->_data; } T* operator->() { return &_node->_data; } 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& s) { return _node != s._node; } bool operator==(const Self& s) { return _node == s._node; }};//const迭代器类template<class T>class __list_const_iterator{public:typedef ListNode<T> Node;typedef __list_const_iterator<T> self;Node* _node;__list_const_iterator(Node* x):_node(x){} const T& operator*() { return _node->_data; } const T* operator->() { return &_node->_data; }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& s){return _node != s._node;}bool operator==(const self& s){return _node == s._node;}};
但这样看起来代码非常冗余,那么有没有什么方式来简化代码呢?
其实模板的用法不仅有vector<int>这类,还可以这么使用:
所以,编译器可以通过模板自动推演出当前使用场景为const迭代器还是非const迭代器。
这种方式可以唤作为『 适配器』,当然这部分后面还会有涉及。
3.迭代器失效
在之前学习vector时我们发现了迭代器失效的问题,对于vector来讲迭代器失效往往发生在扩容之后,那么对于List来讲,是不需要扩容的,那么List会不会发生迭代器失效的问题呢?
List可能会在『 erase』操作之后,迭代器失效。
我们来分析一下:
vector迭代器失效的原因是因为扩容导致迭代器使用前后底层指针位置发生了改变,换句话说就是开辟了新的空间,指针指向了新的地址,而迭代器没有更新从而导致迭代器失效。
而List并不会扩容,也不会开辟新的连续的空间,所以对于插入来说无从谈起迭代器失效,而erase是会导致迭代器失效的,因为删除了该位置的对象,迭代器底层指针成为野指针,但并不会影响其他迭代器,只需要重新赋值即可,比如设计为删除当前迭代器位置对象,并指向下一位置:
// 删除pos位置的节点,返回该节点的下一个位置iterator erase(iterator pos){ assert(pos != end());//list是带头双向循环链表,当pos是end()位置时,证明没有可删除的节点了 Node* cur = pos._node; Node* prev = cur->_prev; Node* next = cur->_next; prev->_next = next; next->_prev = prev; delete cur; return next;}
4.->操作符的重写
->操作符的重写是一个特例,大家只需要记住即可,因为这是STL设计者故意设计为这样的。
Ptr operator->()//为什么返回的是地址??{ return &_node->_data;}
那放在实际的场景中我们使用一下看看:
struct AA{ int _a1; int _a2; AA(int a1 = 1, int a2 = 1) :_a1(a1) , _a2(a2) {}};void test_list5(){ list<AA> lt; AA aa1; lt.push_back(aa1); lt.push_back(AA()); AA aa2(2, 2); lt.push_back(aa2); lt.push_back(AA(2, 2)); list<AA>::iterator it = lt.begin(); while (it != lt.end()) { cout << it->_a1 << ":" << it->_a2 << endl; //这里打印的是_a2的内容并不是_a2的地址 ++it; } cout << endl;}
正常来讲这里本来是应该有两个->的,第一个箭头是it->去调用重载的operator->返回AA* 的指针,第二个箭头是AA* 的指针去访问对象当中的成员变量_a2。
cout << it.operator->()->_a1 << ":" << it.operator->()->_a2 << endl;//实际
但是为了程序可读性,所以编译器做了特殊识别处理,省略了一个箭头。
5.模拟实现源码
#pragma once#include<iostream>#include<assert.h>namespace F{ // List的节点类 template<class T> struct ListNode { ListNode<T>* _next; ListNode<T>* _prev; T _data; ListNode(const T& val = T()) :_next(nullptr) ,_prev(nullptr) ,_data(val) {} }; //List的迭代器类 template<class T, class Ref, class Ptr> class __list_iterator { public: typedef ListNode<T> Node; typedef __list_iterator<T, Ref, Ptr> Self; Node* _node; __list_iterator(Node* x) :_node(x) {} Ref operator*() { return _node->_data; } Ptr operator->() { return &_node->_data; } 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& s) { return _node != s._node; } bool operator==(const Self& s) { return _node == s._node; } }; //list类 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; public: /// // List的构造 void empty_init() { _head = new Node; _head->_next = _head; _head->_prev = _head; } list() { empty_init(); } list(int n, const T& value = T()) { empty_init(); while (n--) { push_back(value); } } list(const list<T>& l) { empty_init(); for (const auto& e : l) { push_back(e); } } list<T>& operator=(list<T> l) { swap(l); return *this; } ~list() { clear(); delete _head; _head = nullptr; } /// // List Iterator iterator begin() { return _head->_next; } iterator end() { return _head; } const_iterator begin() const { return _head->_next; } const_iterator end() const { return _head; } /// // List Capacity size_t size()const { size_t count = 0; const_iterator it = begin(); while (it != end()) { ++count; ++it; } return count; } bool empty()const { return _head->_next == _head; } // List Access T& front() { return *begin(); } const T& front()const { return *begin(); } T& back() { return *(--end()); } const T& back()const { return *(--end()); } // List Modify void push_back(const T& val) { insert(end(), val); } void pop_back() { erase(--end()); } void push_front(const T& val) { insert(begin(), val); } void pop_front() { erase(begin()); } // 在pos位置前插入值为val的节点 iterator insert(iterator pos, const T& val) { Node* cur = pos._node; Node* prev = cur->_prev; Node* newnode = new Node(val); prev->_next = newnode; newnode->_next = cur; newnode->_prev = prev; cur->_prev = newnode; //return iterator(newnode); return newnode;//单参数的构造函数支持隐式类型转换 } // 删除pos位置的节点,返回该节点的下一个位置 iterator erase(iterator pos) { assert(pos != end());//list是带头双向循环链表,当pos是end()位置时,证明没有可删除的节点了 Node* cur = pos._node; Node* prev = cur->_prev; Node* next = cur->_next; prev->_next = next; next->_prev = prev; delete cur; return next; } void clear() { iterator it = begin(); while (it != end()) { it = erase(it); } } void swap(list<T>& l) { std::swap(_head, l._head); } private: Node* _head; };};
模拟实现的主要目的在于学习优秀的代码,比如这里适配器的使用、优秀的框架设计。
注意为了迭代器统一使用:
typedef __list_iterator<T, T&, T*> iterator;typedef __list_iterator<T, const T&, const T*> const_iterator;
这样我们就可以不需要知道迭代器的底层是如何实现的,只需要调用迭代器的接口就可以了,从而实现了通用的目的。
=========================================================================
如果你对该系列文章有兴趣的话,欢迎持续关注博主动态,博主会持续输出优质内容
🍎博主很需要大家的支持,你的支持是我创作的不竭动力🍎
🌟~ 点赞收藏+关注 ~🌟
=========================================================================
本文链接:https://www.kjpai.cn/news/2024-03-26/148815.html,文章来源:网络cs,作者:纳雷武,版权归作者所有,如需转载请注明来源和作者,否则将追究法律责任!