list类的使用和模拟实现

news2024/11/24 12:22:22

目录 

一、list类的介绍

二、list的使用

1.构造、拷贝构造函数和迭代器

2.数据的增删查改

三、list的部分接口实现

1.节点定义

2.list类的组织

四、list的迭代器

1.迭代器的设计思路

2.const迭代器

3.->操作符的重载

4.反向迭代器


一、list类的介绍

list就是C++库中对链表的实现,它的底层是一个带头双向循环链表。

与vector这样的数据结构相比,链表最大的缺陷是不支持随机访问。访问list的第n个元素就必须从一个已知位置(常见的是头部或者尾部)向前或向后走到该位置,需要一定的时间开销。同时list还需要额外的空间保存前后节点的地址。

二、list的使用

1.构造、拷贝构造函数和迭代器

explicit list (const allocator_type& alloc = allocator_type());————默认构造,缺省参数alloc我先不讲

explicit list (size_type n, const value_type& val = value_type(), const allocator_type& alloc = allocator_type());————构造的list中包含n个值为val的元素

template

list (InputIterator first, InputIterator last, const allocator_type& alloc = allocator_type());————用[first, last)区间中的元素构造list

list (const list& x);————拷贝构造函数

#include<iostream>
#include<list>
using namespace std;

void test()
{
    list<int> l1;//构造空list
    list<int> l2(4, 100);//l2中放4个值为100的元素
    list<int> l3(l2.begin(), l2.end());//用l2的[begin(), end())左闭右开区间构造l3
    list<int> l4(l3);//用l3构造l4
    
    //迭代器区间的构造可以是任何一种数据结构的迭代器,甚至是原生指针,下面就是以数组的原生指针为迭代器的构造
    int arr[] = { 16, 2, 77, 29 };
    list<int> l5(arr, arr + sizeof(arr) / sizeof(arr[0]));
    
    //C++11中引入了列表式的初始化
    list<int> l6{ 1, 2, 3, 4, 5 };
    
    //用迭代器可以遍历元素,范围for底层也是使用迭代器
    list<int>::iterator it = l6.begin();
    while (it != l6.end())
    {
        cout << *it << " ";
        ++it;
    }
    cout << endl;
}

int main()
{
    test();
    return 0;
}

2.数据的增删查改

void push_front (const value_type& val);————在list头部插入值为val的元素

void pop_front ();————删除list中第一个元素

void push_back (const value_type& val); 在list尾部插入值为val的元素

iterator insert(iterator position, const value_type& val);————在position位置插入值为val的元素

iterator erase (iterator position);————删除position位置的元素

void swap (list& x);————交换两个list中的元素

void clear();————清空list中的有效元素

void empty();————检测list是否为空

size_t size() const;————返回list中有效节点的个数

#include<iostream>
#include<list>
using namespace std;

void test()
{
    list<int> l1;
    l1.push_front(2);
    l1.push_front(3);
    l1.push_front(4);
    l1.insert(l.begin(), 1);
    list<int>::iterator it = l1.begin();
    while (it != l1.end())
    {
        cout << *it << " ";
        ++it;
    }
    //1234
    cout << endl;
    l1.pop_front();
    l1.erase(l1.begin());
    vector<int> l2;
    l2.swap(l1);
    it = l2.begin();
    while (it != l2.end())
    {
        cout << *it << " ";
        ++it;
    }
    //34
    cout << l2.empty();
    l2.clear();
    it = l2.begin();
    while (it != l2.end())
    {
        cout << *it << " ";
        ++it;
    }
    //
}

int main()
{
    test();
    return 0;
}

三、list的部分接口实现

在list的实现中需要三个类,一个是节点类,一个是list类,还有一个是迭代器类,全部都存放在my_list的命名空间中。

1.节点定义

list我们使用一个双向链表实现,那么一个节点就需要指向前节点和后节点的指针还有储存的数据本身

template<class T>
struct list_node
{
    list_node<T>* _next;
    list_node<T>* _prev;
    T _data;
    //构造函数

    list_node(const T& x)
         :_next(nullptr)
         , _prev(nullptr)
         , _data(x)
     {}
};

2.list类的组织

在list类中我们需要一个保存哨兵卫的成员变量。由于链表不支持随机访问,所以也可以加上一个size_t _size的变量保存节点的个数,如果不加的化需要一个O(N)的遍历函数,相比直接获取会快得多。

template <class T>
class list
{
    typedef list_node<T> node;
    public:
        //成员函数
    private:
        node* _head;//哨兵卫头节点
};

下面是大部分接口的模拟实现:

template <class T>
class list
{
    typedef list_node<T> node;
    public:
    //构建空链表
    void empty_initialize()
    {
        _head = new node(T());//创建一个哨兵卫头
    _head->_next = _head;
    _head->_prev = _head;//prev和next都指向哨兵卫
        _size = 0;
    }
    
    //默认构造          

    list()
    {
        empty_initalize();
    }
    
    //拷贝构造        

    list(const list<T> & lt)
    {
        empty_initalize();//创建空链表          
        for (const auto &e : lt)//将每一个数据按顺序尾插
        {
            push_back(e);
        }
        return *this;
    }
    
    //析构函数
    ~list()
    {
        clear();//清空数据
        //释放哨兵卫并置空
        delete _head;
    _head = nullptr;
    }
    
    //清空数据                        
        
    void clear()
    {
        iterator it = begin();
        while (it != end())
        {
            it = erase(it);//一个一个释放
        }
    }
    
    //赋值运算符重载可能会涉及深拷贝
    list<T>& operator=(const list<T>& lt)
    {
        if (this != &lt)//避免自己给自己赋值
        {
            clear();//清空数据
            for (const auto&e : lt)
            //将每一个数据按顺序尾插,而且用引用还避免了深浅拷贝的问题
            {
                push_back(e);
            }
        }
    }
    
    //任意迭代器位置插入
    iterator insert(iterator pos, const T& x)
    {
        node* newnode = new node(x);
        node* cur = pos._pnode;//迭代器时一个类,类中找这个原生指针
        node* prev = cur->_prev;

    prev->_next = newnode;//链接前面
    newnode->_prev = prev;
    newnode->_next = cur;//链接后面
    cur->_prev = newnode;

        return iterator(newnode);
    }
    
    //头插
    void push_front(const T& x)
    {
        insert(begin(), x);//调用insert
    }
    
    //头删
    void pop_front()
    {
        erase(begin());//调用erase
    }
    
    //尾删
    void pop_back()
    {
        erase(--end());
    }
    
    //尾插
    void push_back(const T& x)
    {

        insert(end(),x)
    }
 
        
 
    iterator erase(iterator pos)
    {
        assert(pos != end());//不能删除头节点
        node* prev = pos._pnode->_prev;//保存pos前的节点
        node* next = pos._pnode->_next;//保存pos后的节点
        prev->_next = next;//将后节点链接前节点
        next->_prev = prev;//将前节点链接后节点
        delete pos._pnode;//删除pos节点--会失效
        return iterator(next);//返回pos节点后的节点
    }
    
     size_t size()
     {
         int i = 0;
         list<int>::iterator it = begin();
         while(it != end())
         {
             it++;
             i++;         
         }
         return i;     
     }
     
    private:
    node* _head;
};

四、list的迭代器

1.迭代器的设计思路

迭代器在各种不同的数据结构中有着巨大的优势:迭代器可以提供统一的迭代方式。

它的设计思路一般有两种:一种是连续的内存访问可以使用原生指针的封装加以实现,另一种是链表这样即开即用的数据结构,它的迭代器增减是通过多次向前或向后移动节点指向实现的,解引用是通过获取迭代器内部的变量来实现,list使用的思路就是后者。

所以虽然各种数据结构的访问方式是一致的但是底层实现可能完全不同,这也是封装特征作为面向对象语言的基本特征巨大优势。

迭代器会专门设计一个类,成员变量保存节点指针。

template<class T>
struct __list_iterator
{
    typedef list_node<T> node;

    node* _pnode;
    __list_iterator(node* p)
        :_pnode(p)
    {}



    
    T operator*()//非const
    {
        return _pnode->_data;
    }
    
    //前置++,迭代器内部指针后挪
    __list_iterator<T>& operator++()
    {
        _pnode = _pnode->_next;
        return *this;
    }


    //后置++,迭代器内部指针后挪
    __list_iterator<T> operator++(int)
    {
        Self tmp(*this);
        _pnode = _pnode->_next;
        return tmp;
    }
    
    //前置--,迭代器内部指针前挪
    __list_iterator<T>& operator--()
    {
        _pnode = _pnode->_prev;
        return *this;
    }
    
    //后置--,迭代器内部指针前挪      

    __list_iterator<T> operator--(int)
    {
        Self tmp(*this);
        _pnode = _pnode->_prev;
        return tmp;
    }
    
    //迭代器的相同和不同不应该是结构体成员的一致,而应该是内部储存的节点的指针一致才可以
    bool operator!=(const __list_iterator<T>& it) const
    {
        return _pnode != it._pnode;
    }

    bool operator==(const __list_iterator<T>& it) const
    {
        return _pnode == it._pnode;
    }
};

2.const迭代器

有些人会简单地这样写:const list::iterator lt1 = lt.begin();

只是在迭代器前加上const的话只是让迭代器本身不能被修改,当然也不能+或者-。

const迭代器应该是迭代器中的元素不能被修改。所以我们只需要一个用const修饰返回值的operate*函数,那么我们可以试试函数重载,但根据上面的错误,我们可以明确认定这个函数的重载一定不能用const修饰this指针。

所以这两个函数应该是这样的:

T& operator*()
{
    return _pnode->_data;
}
 
const T& operator*()
{
    return _pnode->_data;
}

但是我们需要重新熟悉以下函数重载的要求:两个函数的函数名和参数相同则两个函数构成重载。

它们只是返回类型不同,不构成重载,而是重定义。此时我们只能为了一个函数的正当存在而去为它创建另一个类:const_iterator

这个const_iterator中,相比于非const迭代器其实功能发生改变的只有:T& operator*()的返回类型用const修饰。

结果大概就是这样的:

struct __list_iterator
{
    //省略重复的代码
    T& operator*()
    {
        return _pnode->_data;
    }
    //省略重复的代码
};
struct const_iterator
{
    //省略重复的代码
    const T& operator*()
    {
        return _pnode->_data;
    }
    //省略重复的代码
};

但是又写一堆重复的代码,不觉得很难受吗?

所以我们在其中引入了迭代器类的第二个参数claas Ref,这个Ref的类型不同,它实例化出来的类也是不同的。所以我们可以通过:__list_iterator和__list_iterator两种显示实例化来做到让编译器生成两个类。

也就是说,为了解引用重载前面的const我们必须要定义一个新类,但是我们生成这个类的工作交给了编译器,而不是我们自己来完成。

而且把__list_iterator这个类命名为Self,这样也方便我们对代码的修改。

template<class T, class Ref>
struct __list_iterator
{
    typedef list_node<T> node;
    typedef __list_iterator<T, Ref> Self;
    node* _pnode;
    __list_iterator(node* p)
        :_pnode(p)
    {}

    
    Ref operator*()
    {
        return _pnode->_data;
    }
    
    Self& operator++()
    {
        _pnode = _pnode->_next;
        return *this;
    }


    
    Self operator++(int)
    {
        Self tmp(*this);
        _pnode = _pnode->_next;
        return tmp;
    }

    Self& operator--()
    {
        _pnode = _pnode->_prev;
        return *this;
    }

    Self operator--(int)
    {
        Self tmp(*this);
        _pnode = _pnode->_prev;
        return tmp;
    }
    
    bool operator!=(const Self& it) const
    {
        return _pnode != it._pnode;
    }

    bool operator==(const Self& it) const
    {
        return _pnode == it._pnode;
    }
};

3.->操作符的重载

我们之前在C语言就学过->操作符,当时我们说对于一个结构体指针p可以有以下效果:

#include<stdio.h>
struct test
{
    int a;
};

int main()
{
    struct test example;
    example.a = 0;
    
    struct test* p = &example;
    printf("%d",p->a);
    printf("%d",(*p).a);
    //二者打印的结果是相同的
    return 0;
}

但是->的重载和C语言中不太一样,它的重载函数返回的一定是指针,这个指针可以是任何一种,但是必须是一个指针。

那在C++中,我们又该如何理解以上面的struct test为基础的p->a呢?

我再写一段代码:

#include<iostream>
class test
{
public:
    test()
        :a(0)
    {}
    int* operate->()
    {
        return &a;
    }
private:
    int a;
};

int main()
{
    test example;
    printf("%d",(example->a));
    return 0;
}

应该是这样的,p->a可以理解为p->->a,中间省略了一个->,第一个箭头是函数调用,返回a的地址,而后面的->可以理解为解引用。所以不要一看到->操作符就说是解引用,它还可能是函数调用。

我们此时也想重载一下这个->,像下面这样:

T* operator->()
{
    return &_pnode->_data;
}

但是此时问题就又发生了。我们使用的*操作符重载可以达到const或非const的效果,但是如果我们只写一个非const版本,人们就可以借用这个指针来改掉本不想修改的内容。也就是说,我们还要面对和上面一样的情况,为一个函数而多生成一个类,是这样没错,但是这个函数可以放到const迭代器的类中,只是多一个参数控制它而已。此时我们就引入了模板的第三个参数,编译器依旧形成两个类。

它们是:普通迭代器__list_iterator和const迭代器__list_iterator

template<class T, class Ref, class Ptr>
struct __list_iterator
{
    typedef list_node<T> node;
    typedef __list_iterator<T, Ref, Ptr> Self;
    node* _pnode;
    
    __list_iterator(node* p)
        :_pnode(p)
    {}

    Ptr operator->()
    {
        return &_pnode->_data;
    }

    Ref operator*()
    {
        return _pnode->_data;
    }

    Self& operator++()
    {
        _pnode = _pnode->_next;
        return *this;
    }

    Self operator++(int)
    {
        Self tmp(*this);
        _pnode = _pnode->_next;
        return tmp;
    }

    Self& operator--()
    {
        _pnode = _pnode->_prev;
        return *this;
    }

    Self operator--(int)
    {
        Self tmp(*this);
        _pnode = _pnode->_prev;
        return tmp;
    }

    bool operator!=(const Self& it) const
    {
        return _pnode != it._pnode;
    }

    bool operator==(const Self& it) const
    {
        return _pnode == it._pnode;
    }
};

4.反向迭代器

我们学习STL的目的就在于这些数据结构都有大佬已经实现好了,不像C语言中我们使用各种数据结构还需要实现该数据结构的各种接口。既然有现成的为什么不用呢?使用他人的代码实现另一个功能的方式就叫做代码的复用。

也就是说反向迭代器其实就是正向迭代器的封装,在STL的实现中也是一样的。

反向迭代器的rbegin就是正向迭代器的end,而反向迭代器的rend就是正向迭代器的begin,只是我们在重载解引用时需要在当前正向迭代器位置先前挪一位再解引用,所有反向迭代器的++和--都和正向对调就可以了。

self& operator++() 
{
    --current;
    return *this;
}
self operator++(int) 
{
    self tmp = *this;
    --current;
    return tmp;
}
self& operator--() 
{
    ++current;
    return *this;
}
self operator--(int) 
{
    self tmp = *this;
    ++current;
    return tmp;
}

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/337115.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Elasticsearch基本概念和索引原理

一、Elasticsearch是什么&#xff1f; Elasticsearch是一个基于文档的NoSQL数据库&#xff0c;是一个分布式、RESTful风格的搜索和数据分析引擎&#xff0c;同时也是Elastic Stack的核心&#xff0c;集中存储数据。Elasticsearch、Logstash、Kibana经常被用作日志分析系统&…

[CCS 2022] 皇帝没有衣服:用于网络安全的AI/ML

AI/ML for Network Security: The Emperor has no ClothesCCS 22: Proceedings of the 2022 ACM SIGSAC Conference on Computer and Communications Securityhttps://dl.acm.org/doi/abs/10.1145/3548606.3560609摘要最近的一些研究工作提出了基于机器学习&#xff08;ML&…

工业废水的种类及其处理工艺有哪些?特定目标污染物的深度去除

随着工业的迅速发展&#xff0c;工业废水的种类和数量随之增加&#xff0c;对水体的污染也日趋严重&#xff0c;威胁人类的健康和正常生活。所以工业废水的处理对于环保至关重要。 保护生态环境、更好地做好工业废水的处理&#xff0c;了解工业废水及其种类与处理非常必要。 工…

MySQL 删除数据库

使用普通用户登陆 MySQL 服务器&#xff0c;你可能需要特定的权限来创建或者删除 MySQL 数据库&#xff0c;所以我们这边使用 root 用户登录&#xff0c;root 用户拥有最高权限。 当数据库不再使用时应该将其删除&#xff0c;以确保数据库存储空间中存放的是有效数据。删除数据…

【Hello Linux】 Linux基础命令

作者&#xff1a;小萌新 专栏&#xff1a;Linux 作者简介&#xff1a;大二学生 希望能和大家一起进步&#xff01; 本篇博客简介&#xff1a;介绍Linux的基础命令 Linux基础命令ls指令lsls -als -dls -ils -sls -lls -nls -Fls -rls -tls -Rls -1总结思维导图pwd指令whoami指令…

Python 条件语句

Python条件语句是通过一条或多条语句的执行结果&#xff08;True或者False&#xff09;来决定执行的代码块。可以通过下图来简单了解条件语句的执行过程:Python程序语言指定任何非0和非空&#xff08;null&#xff09;值为true&#xff0c;0 或者 null为false。Python 编程中 i…

【正点原子FPGA连载】第八章UART串口中断实验 摘自【正点原子】DFZU2EG_4EV MPSoC之嵌入式Vitis开发指南

1&#xff09;实验平台&#xff1a;正点原子MPSoC开发板 2&#xff09;平台购买地址&#xff1a;https://detail.tmall.com/item.htm?id692450874670 3&#xff09;全套实验源码手册视频下载地址&#xff1a; http://www.openedv.com/thread-340252-1-1.html 第八章UART串口中…

【SSD 代码精读】之 数据增强(Data Augmentation)

SSD 数据增强前言1、Compose2、SSDCropping3、Resize4、ColorJitter5、ToTensor6、RandomHorizontalFlip7、Normalization8、AssignGTtoDefaultBox前言 原论文 根据原论文&#xff0c;我们需要处理的有以下&#xff1a; data_transform {"train": transforms.Com…

vue2.x中使用vuex

Vuex是什么&#xff1f; Vuex是一个专门为Vue.js应用程序开发的状态管理模式。它采用集中式存储来管理应用程序中所有组件的状态&#xff0c;并以相应的规则保证状态以一种可预测的方式发生变化。Vuex也被集成到了Vue的官方调试工具vue-devtools中&#xff0c;提供了诸如零配置…

Java:使用Java功能确保应用程序安全的方法

与添加新功能一样重要的是&#xff0c;应用程序开发人员需要开始更加重视他们设计的应用程序的安全性。毕竟&#xff0c;更多的应用程序功能意味着更多的数据驻留在应用程序中。如果没有适当的安全控制&#xff0c;这些数据很容易被入侵者窃取。Java是目前世界上最安全、最流行…

如何去阅读源码,我总结了18条心法

在聊如何去阅读源码之前&#xff0c;先来简单说一下为什么要去阅读源码&#xff0c;大致可分为以下几点原因&#xff1a;最直接的原因&#xff0c;就是面试需要&#xff0c;面试喜欢问源码&#xff0c;读完源码才可以跟面试官battle提升自己的编程水平&#xff0c;学习编程思想…

不要慌,我们谈一谈如何用好 ChatGPT

别人贪婪时我恐惧&#xff0c;别人恐惧时我贪婪。 ——巴菲特 ChatGPT 火了&#xff0c;技术领域的社交媒体、自媒体几乎被 ChatGPT 刷屏&#xff0c;这些内容当中最让人惶恐不安的是我们是否会被 AI 取代之类的文章。 比如以下几个文章标题&#xff1a; 《ChatGPT可能马上…

Transformer结构解读

咱们还是照图讨论&#xff0c;transformer结构图如下&#xff0c;本文主要讨论Encoder部分&#xff1a;图一一、首先说一下Encoder的输入部分&#xff1a;在NLP领域&#xff0c;个人理解&#xff0c;这个inputs就是我们的句子分词之后的词语&#xff0c;比如“我&#xff0c;喜…

符号让人疯狂

符号让人疯狂 判断背了个LV符号的包就想可能有钱 趣讲大白话&#xff1a;人是通过符号区分生活的 聪明人想想&#xff1a;能超越或摆脱符号依赖吗&#xff1f; *********** 信息社会加速符号的传递和创造 我们已经被各种信息传递的符号淹没 信息符号的筛选成了人的主要工作 再…

GRB非隔离系列宽电压输入负高电压输出 电压控制型

特点● 效率高达70%以上● 1*2英寸标准封装● 单电压负输出● 价格低● 电压控制,输出电压随控制电压变化线性变化● 工作温度: -40℃~85℃● 阻燃封装&#xff0c;满足UL94-V0 要求● 温度特性好● 可直接焊在PCB 上应用GRB 系列模块电源是一种DC-DC升压变换器。该模块电源的输…

十、Linux文件 - fread函数讲解

目录 1.fread函数讲解 2.fread函数实战 1.fread函数讲解 从文件中读入数据到指定的地址中 函数原型&#xff1a; size_t fread(void*buff , size_t size, size_t count , FILE* stream) /* * description :对已打开的流进行数据读取 * param ‐ ptr &#xff1a;指向 数据块的…

好用的电脑备份软件推荐

现在几乎每个人都有一台电脑&#xff0c;上面存储着大量的数据&#xff0c;比如宝贵的照片、视频、工作文档等等。但电脑也随时存在许多威胁&#xff0c;比如病毒、Windows 更新错误、死机黑屏、驱动程序问题、系统崩溃等。为防止任何数据丢失&#xff0c;你需要一个专业的电脑…

Oracle数据库故障处理-单块读hang存储异常导致hang死,数据库大量的db file seq read等待(p1 p2无反映)

1 故障描述 2023年1月27日下午接到业务反馈数据库存在大量的锁表阻塞信息&#xff0c;并且业务的页面以及数据库的一些查询均处于阻塞状态&#xff0c;简单的查询sql也需要查询很长时间且未返回结果,数据库hang状态。 问题现象2 1 数据库进程无法杀除。 2 操作系统进程使用…

也许你应该学学 postman了

使用 最简单的方法就是直接在浏览器中复制 Copy as cURL &#xff0c;然后把数据导入 postman&#xff0c;然后 send &#xff0c;收工。 我们这里拿 知乎首页 举例 在对应的请求下复制 cURL 打开 postman &#xff0c; 点击左上角的 Import &#xff0c; 选择Paste Raw Tex…

如何使用逻辑分析仪,解析通信数据

如何使用逻辑分析仪&#xff0c;解析通信数据使用工具&#xff1a;逻辑分析仪&#xff08;几十块买的裸板&#xff09;&#xff0c;软件是&#xff1a;PulseView一、在开发或者移植某一个模块时&#xff0c;你可能遇到这样的问题&#xff1a;二、逻辑分析仪的使用使用工具&…