当前位置:   article > 正文

C++——list类及其模拟实现

C++——list类及其模拟实现

前言:这篇文章我们继续进行C++容器类的分享——list也就是数据结构中的链表,而且是带头双向循环链表


一.基本框架

  1. namespace Mylist
  2. {
  3. template<class T>
  4. //定义节点
  5. struct ListNode
  6. {
  7. ListNode<T>* _next;
  8. ListNode<T>* _prev;
  9. T _data;
  10. ListNode(const <T>& x = T())
  11. :_next(nullptr)
  12. ,_prev(nullptr)
  13. ,_data(x)
  14. {}
  15. };
  16. template<class T>
  17. class list
  18. {
  19. typedef ListNode<T> Node;
  20. public:
  21. //构造函数
  22. list()
  23. {
  24. _head = new Node;
  25. _head->_next = _head;
  26. _head->_prev = _head;
  27. }
  28. //析构函数
  29. ~list()
  30. {
  31. iterator it = begin();
  32. while (it != end())
  33. {
  34. it = erase(it);
  35. }
  36. delete _head;
  37. _head = nullptr;
  38. }
  39. //数据个数
  40. size_t size()
  41. {
  42. iterator it = begin();
  43. size_t Size = 0;
  44. while (it != end())
  45. {
  46. Size++;
  47. it++;
  48. }
  49. return Size;
  50. }
  51. private:
  52. Node* _head;
  53. };
  54. }

由于要满足存储任意类型的数据,所以我们必须要使用模版来进行定义。 


迭代器

关于list类中的最难之处,就是迭代器了。

因为迭代器的原理即为指针,对于string和vector这种创建的对象的物理空间是连续的类来说,我们可以直接对迭代器进行“++”、“--”等数学运算

而对于本质为链表的list来说,由于每个节点的物理空间都是随机创建,各个节点的地址并不连续,所以我们没法直接进行迭代器的数学运算,而需要对迭代器的各种功能进行重新定义,所以我们创建一个专门为迭代器服务的类

  1. //迭代器
  2. template<class T>
  3. struct ListIterator
  4. {
  5. typedef ListNode<T> Node;
  6. typedef ListIterator<T> Self;
  7. ListIterator(Node* node)
  8. :_node(node)
  9. {}
  10. //解引用
  11. T& operator*()
  12. {
  13. return _node->_data;
  14. }
  15. //前置++
  16. Self& operator++()
  17. {
  18. _node = _node->_next;
  19. return *this;
  20. }
  21. //后置++
  22. Self operator++(int)
  23. {
  24. Self tmp(*this);
  25. _node = _node->_next;
  26. return tmp;
  27. }
  28. //前置--
  29. Self& operator--()
  30. {
  31. _node = _node->_prev;
  32. return *this;
  33. }
  34. //后置--
  35. Self operator--(int)
  36. {
  37. Self tmp(*this);
  38. _node = _node->_prev;
  39. return tmp;
  40. }
  41. //不相等
  42. bool operator!=(const Self& it)
  43. {
  44. return _node != it._node;
  45. }
  46. //相等
  47. bool operator==(const Self& it)
  48. {
  49. return _node == it._node;
  50. }
  51. Node* _node;
  52. };

随后在list类中将该类名重定义为iterator,便可正常使用迭代器了

  1. typedef ListIterator<T> iterator;
  2. iterator begin()
  3. {
  4. return _head->_next;
  5. }
  6. iterator end()
  7. {
  8. return _head;
  9. }

 这里值得注意的是,因为是带头双向循环链表,所以链表的开始即哨兵位的下一个,而结尾就是哨兵位


但是现在的迭代器是存在问题的,它并不能实现对const修饰的数据的操作,所以我们还需要一个const迭代器。因为我们的普通迭代器就是用模版来实现的,所以这里可以直接通过模版来实现const迭代器

  1. //迭代器
  2. template<class T,class Ref>
  3. struct ListIterator
  4. {
  5. typedef ListNode<T> Node;
  6. typedef ListIterator<T,Ref> Self;
  7. //构造函数
  8. ListIterator(Node* node)
  9. :_node(node)
  10. {}
  11. //解引用
  12. Ref operator*()
  13. {
  14. return _node->_data;
  15. }
  16. //前置++
  17. Self& operator++()
  18. {
  19. _node = _node->_next;
  20. return *this;
  21. }
  22. //后置++
  23. Self operator++(int)
  24. {
  25. Self tmp(*this);
  26. _node = _node->_next;
  27. return tmp;
  28. }
  29. //前置--
  30. Self& operator--()
  31. {
  32. _node = _node->_prev;
  33. return *this;
  34. }
  35. //后置--
  36. Self operator--(int)
  37. {
  38. Self tmp(*this);
  39. _node = _node->_prev;
  40. return tmp;
  41. }
  42. //不相等
  43. bool operator!=(const Self& it)
  44. {
  45. return _node != it._node;
  46. }
  47. //相等
  48. bool operator==(const Self& it)
  49. {
  50. return _node == it._node;
  51. }
  52. Node* _node;
  53. };
  1. typedef ListIterator<T,T&> iterator;
  2. typedef ListIterator<T,const T&> const_iterator;

这里有一个细节,因为T同时还要服务于Node类,所以不能直接对其进行修改,而是另用一个模版参数。 

 因为const对象与非const对象最大的不同之处在于对数据的访问,所以定义一个名为Ref(引用)的模版参数,来对解引用运算符重载函数进行改造


二.常用操作

1.插入

先来看任意位置的插入需要传入某个位置的指针pos

  1. //pos前插入
  2. void insert(iterator pos, const T& val)
  3. {
  4. Node* cur = pos._node;
  5. Node* newnode = new Node(val);
  6. Node* prev = cur->_prev;
  7. prev->_next = newnode;
  8. newnode->_next = cur;
  9. cur->_prev = newnode;
  10. newnode->_prev = prev;
  11. }

 现在对于我们来说就是非常简单,测试如下:

这里有一个小细节,如果我们插入的位置是第一个节点之前,由于it迭代器的指向并未改变,所以如果进行遍历,他就不会遍历出我们新插入的数据,所以需要更新一下it。


有了pos位置的插入之后,就可以用它来扩展头插和尾插

  1. //尾插
  2. void push_back(const T& x)
  3. {
  4. insert(end(), x);
  5. }
  6. //头插
  7. void push_front(const T& x)
  8. {
  9. insert(begin, x);
  10. }

2.删除

我们同样先写出pos位置的删除

  1. //pos删除
  2. iterator erase(iterator pos)
  3. {
  4. Node* cur = pos._node;
  5. Node* next = cur->_next;
  6. Node* prev = cur->_prev;
  7. next->_prev = prev;
  8. prev->_next = next;
  9. delete cur;
  10. return iterator(next);
  11. }

由于删除会导致迭代器成为野指针,所以我们要对其进行更新, 测试如下:


同样由其扩展出头删和尾删

  1. //头删
  2. void pop_front()
  3. {
  4. erase(begin());
  5. }
  6. //尾删
  7. void pop_back()
  8. {
  9. erase(--end());
  10. }

3.拷贝

和string和vector一样,list的拷贝也需要使用深拷贝,那么它的拷贝构造函数该怎么写?

同样是要开辟新的空间,需要一个自己的头结点,随后按照被拷贝的链表的数据进行尾插即可:

  1. //拷贝构造
  2. list(const list<T>& lt)
  3. {
  4. _head = new Node;
  5. _head->_next = _head;
  6. _head->_prev = _head;
  7. for (auto& e : lt)
  8. {
  9. push_back(e);
  10. }
  11. }

测试如下:


此外,还有“=”运算符重载的方法:

  1. //交换
  2. void swap(list<T>& it)
  3. {
  4. std::swap(_head, it._head);
  5. }
  6. //=运算符重载
  7. list<T>& operator=(list<T> lt)
  8. {
  9. swap(lt);
  10. return *this;
  11. }

这里仍然是巧妙的运用swap函数,因为lt是一个临时拷贝,有自己的空间和地址,所以直接让两者进行交换,lt在退出函数时即被销毁,而拷贝者则继承了它的地址空间,测试如下:


总结

关于list类的基本知识就分享到这里啦。

因为与string和vector都存在很多相似之处,所以建议将这三者放在一起学习。

喜欢本篇文章记得一键三连,下期再见!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/羊村懒王/article/detail/433199
推荐阅读
相关标签
  

闽ICP备14008679号