• 【C++】list的模拟实现


    目录

    前言:list的简单介绍

    一、Member variables

    二、iterator

    1、__list_iterator

    2、begin()、end()

    3、const_begin()、const_end()

    4、operator!=、==

    5、operator++

    6、operator--

    7、operator*

    8、operator->

    三、Modifiers

    1、push_back

    2、push_front

    3、insert

    4、pop_back

    5、pop_front

    6、erase

    7、swap

    8、clear

    四、constructor

    1、无参构造

    2、迭代器区间构造

    3、拷贝构造

    五、destructor

    六、operator=

    总结


    前言:list的简单介绍

    list是C++STL中十分重要的容器,这个容器不可以使用[ ]来对链表进行操作

    list的优点是按需索取,不会占用过多的空间,同时它的头部和尾部的插入删除时间复杂度是O(1)

    因为它是带头双向循环链表,找头和尾的时间复杂度都是O(1)

    同时还有会造成内存碎片的问题,因为它的节点是通过new出来的,频繁的new和delete必然会导致内存碎片。

    list的遍历只能通过迭代器,因此它的迭代器是我们要重点实现的部分

     


    一、Member variables

    链表的成员变量很明显就是,链表的节点,而链表的节点是一个聚合类型,所以很明显链表节点是一个类,一个带头双向循环链表的成员一定有它当前节点的值,指向下一个节点的指针,和指向上一个节点的指针。同时还要有它的构造函数,构造函数就比较简单了,类似于数据结构带头双向循环链表的BuyNode函数,开一个空间,将当前节点的next和prev指针都指向空,然后在插入时链接到链表中就可以了,经过前面的叙述,我们可以得知这个类应该使用struct来定义是最好的,因为我们要访问节点的成员变量。

    1. //带头双向循环链表
    2. //链表节点
    3. template<class T>
    4. struct list_node
    5. {
    6. list_node(const T& value = T())
    7. :_value(value)
    8. ,_next(nullptr)
    9. ,_prev(nullptr)
    10. {}
    11. T _value;
    12. list_node* _next;
    13. list_node* _prev;
    14. };

    二、iterator

    链表的迭代器与我们的vector和string有很大的不同,因为vector和string的空间是连续的,而list的空间是不连续的,它是一个一个的节点组成的

    迭代器的定义是使用起来像指针,vector和string空间连续,所以直接是原生指针

    而list空间不连续.。

    C++相比于C语言的优势在于函数重载和封装,所以我们可以将指针封装成一个迭代器,分别实现++ -- * 等操作符重载就可以实现迭代器

    1、__list_iterator

    这个类就是迭代器,既然是迭代器,它会看起来像是一个指针,而这个"指针"是指向链表结点的,所以要想完成上面的操作,必然会需要我们向__list_iterator传一个节点

    所以__list_iterator的成员函数就是节点list_node

    1. template<class T>
    2. struct __list_iterator
    3. {
    4. typedef list_node Node;
    5. typedef __list_iterator iterator;
    6. __list_iterator(Node* node)
    7. :_node(node)
    8. {}
    9. Node* _node;
    10. };

    这就是__list_iterator的简单框架

    2、begin()、end()

    要实现这两个成员函数,首先要想清楚begin()和end()到底指向哪个位置?

    在STL的规范中begin是指向第一个元素的,end是指向尾元素的下一个位置的

    因为我们的list是带头的,而头是不存储有效数据的,所以begin不能指向头,所以begin指向的应该是_head->next,而尾的下一个元素就是_head,因为是双向循环链表

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

    这里特别说明一点,begin和end是list这个类的成员函数,并不是__list_iterator类里面的成员函数

    下面的const_begin和const_end同理

    因为都是迭代器的功能所以放在了这里,这里的排列顺序是根据实现顺序来排列的

    3、const_begin()、const_end()

    到了const版本的时候这里就出现了问题,如果还是按照我们的vector和string的方式写const迭代器就会出现问题

    因为const迭代器必然是为了const对象也能够调用,函数重载的规则可没有根据返回值不同来区分重载的函数,而如果我们在函数后面加上const

    1. const_iterator begin()const
    2. {
    3. return _head->next;
    4. }

    它也会出现编译错误,无法解引用const对象的成员变量,因为我们要返回的是head的next

    这个操作一定会将list类的成员变量_head解引用来找到它的下一个节点,所以不能在函数后面加const,那么这到底要怎么写才能够使const对象也能够调用呢?

    我们可以看一下STL的源码

    1. template<class T, class Ref, class Ptr>
    2. struct __list_iterator {
    3. typedef __list_iterator iterator;
    4. typedef __list_iteratorconst T&, const T*> const_iterator;
    5. typedef __list_iterator self;
    6. typedef bidirectional_iterator_tag iterator_category;
    7. typedef T value_type;
    8. typedef Ptr pointer;
    9. typedef Ref reference;
    10. typedef __list_node* link_type;
    11. typedef size_t size_type;
    12. typedef ptrdiff_t difference_type;
    13. //…………………………
    14. };

    我们发现它的迭代器一共有三个模板参数,分别是T, Ref, Ptr

    const迭代器有三个参数T, const T&, const T*

    STL源码是通过实例化出的对象类型不同来区分是普通对象还是const对象的

    因为无论是const对象还是普通对象,调用begin()的操作基本类似,唯一不同的是返回值类型,const对象要返回的是const 类型,普通对象返回普通类型

    因此我们使用模板参数显示的控制就可以了,模板参数分别是T(普通类型),Ref(引用类型),Ptr(指针类型)

    这三个参数还要在后面的操作符重载函数中有用,具体细节在下面

    1. const_iterator begin()const
    2. {
    3. return const_iterator(_head->_next);
    4. }
    5. const_iterator end()const
    6. {
    7. return const_iterator(_head);
    8. }

    4、operator!=、==

    在前面的内容铺垫下这两个操作符是很容易实现的,这就是判断两个迭代器想不想等,换句话来说就是判断这两个迭代器所指向的节点想不想同

    1. bool operator!=(const iterator& it)const
    2. {
    3. return _node != it._node;
    4. }
    5. bool operator==(const iterator& it)const
    6. {
    7. return _node == it._node;
    8. }

    5、operator++

    ++分为前置++和后置++

    所谓的++就是将迭代器移动到下一个节点,就是cur->next

    1. iterator& operator++()
    2. {
    3. _node = _node->_next;
    4. return *this;
    5. }
    6. iterator operator++(int)
    7. {
    8. //浅拷贝就够用了,返回的是tmp的拷贝
    9. iterator tmp(*this);
    10. _node = _node->_next;
    11. return tmp;
    12. }

    细心的同学会发现我们的__list_iterator并没有实现=操作符重载,因为没有必要,浅拷贝就足够了

    后置++返回的是我们没有++的值,我们直接传值返回临时对象就可以了,我们不可能会对++之前的值进行操作。

    6、operator--

    --与++同理,因为是双向链表,--就是将迭代器移动到cur->prev

    1. iterator& operator--()
    2. {
    3. _node = _node->_prev;
    4. return *this;
    5. }
    6. iterator operator--(int)
    7. {
    8. iterator tmp(*this);
    9. _node = _node->_prev;
    10. return tmp;
    11. }

    7、operator*

    在实现这个成员函数时,我们应该仔细想一想,迭代器解引用得到的是list节点的值,而不是整个节点,所以只要返回节点值就可以了,对应的返回值类型应该是Ref

    1. Ref operator*()
    2. {
    3. return _node->_value;
    4. }

    8、operator->

    这个操作符也是,要想一想它的应用场景,它一般应用在结构体指针中,并且它的左操作数必须是指针类型,那么我们自然就很清楚,我们要返回的是指针类型Ptr

    与*操作符重载的不同就在于返回的类型不同而已

    1. Ptr operator->()
    2. {
    3. return &(operator*());
    4. }

    三、Modifiers

    这几个函数都是十分的简单,在链表的博客中都已经写明了如何实现,在这里就不再赘述

    1、push_back

    1. void push_back(const T& x)
    2. {
    3. Node* newNode = new Node(x);
    4. Node* tail = _head->_prev;
    5. tail->_next = newNode;
    6. newNode->_prev = tail;
    7. newNode->_next = _head;
    8. _head->_prev = newNode;
    9. }

    2、push_front

    1. void push_front(const T& x)
    2. {
    3. insert(begin(), x);
    4. }

    3、insert

    1. iterator insert(iterator pos, const T& x)
    2. {
    3. Node* newNode = new Node(x);
    4. Node* cur = pos._node;
    5. Node* prev = cur->_prev;
    6. prev->_next = newNode;
    7. newNode->_prev = prev;
    8. newNode->_next = cur;
    9. cur->_prev = newNode;
    10. return iterator(newNode);
    11. }

    4、pop_back

    1. void pop_back()
    2. {
    3. erase(--end());
    4. }

    5、pop_front

    1. void pop_front()
    2. {
    3. erase(begin());
    4. }

    6、erase

    1. //返回的是删除元素的下一个位置
    2. iterator erase(iterator pos)
    3. {
    4. assert(pos != end());
    5. Node* cur = pos._node;
    6. Node* prev = cur->_prev;
    7. Node* next = cur->_next;
    8. delete cur;
    9. prev->_next = next;
    10. next->_prev = prev;
    11. return iterator(next);
    12. }

    7、swap

    1. void swap(list& lt)
    2. {
    3. std::swap(_head, lt._head);
    4. }

    8、clear

    主要说一下clear与析构函数的区别:clear是清空list,但是list中还保留这头节点

    而析构函数是将整个list全部清理

    1. void clear()
    2. {
    3. iterator it = begin();
    4. while(it != end())
    5. {
    6. it = erase(it);
    7. }
    8. }

    四、constructor

    list的构造函数与vector的特别像

    1、无参构造

    list的无参构造与带头双向循环链表的init函数类似

    不过STL中的无参构造调用了empty_init()这个函数,我们为了仿照库中的实现,也定义了这个函数

    1. void empty_init()
    2. {
    3. _head = new Node;
    4. _head->_next = _head;
    5. _head->_prev = _head;
    6. }

    无参构造,我们直接让无参构造调用empty_init就可以

    1. list()
    2. {
    3. empty_init();
    4. }

    2、迭代器区间构造

    这个构造与vctor相同,完全的一模一样就不再赘述

    1. template<class InputIterator>
    2. list(InputIterator first, InputIterator last)
    3. {
    4. empty_init();
    5. while(first != last)
    6. {
    7. push_back(*first);
    8. first++;
    9. }
    10. }

    3、拷贝构造

    拷贝构造我们使用现代写法,调用迭代器区间的构造函数,然后将临时对象的头节点与我们的头节点交换就完成了操作

    1. list(const list& lt)
    2. {
    3. empty_init();
    4. list tmp(lt.begin(), lt.end());
    5. swap(tmp);
    6. }

    这里调用了empty_init是为了先要有头节点,只有有了头节点push_back才不会崩溃

    五、destructor

    析构函数也与vector类似,不过我们可以先调用clear,然后手动释放头节点

    1. ~list()
    2. {
    3. clear();
    4. delete _head;
    5. _head = nullptr;
    6. }

    六、operator=

    =操作符重载我们也使用现代写法,与拷贝构造类似

    1. list& operator=(list lt)
    2. {
    3. swap(lt);
    4. return *this;
    5. }


    总结


    以上就是今天要讲的内容,本文仅仅简单的模拟实现了list,list的空间配置器和反向迭代器还没有实现

  • 相关阅读:
    二维数组零碎知识梳理
    C语言之自定义类型------结构体
    PHP房贷计算器算法
    电商日报:Shopee五一打款安排;韩国消费者热衷中国跨境电商
    Java常用日志框架详解
    如何使用java雪花算法在分布式环境中生成唯一ID?
    手写操作系统
    06-分布式中间件-Mycat-ShardingJDBC
    Linux-查询目录下包含的目录数或文件数
    磺化-β-环糊精两亲性蒽醚聚集诱导发光微球/AIE环糊精超分子微球/氧化硅修饰AIE微球
  • 原文地址:https://blog.csdn.net/m0_62179366/article/details/127035799