C++中list的简单实现

文章目录

  • 一、模拟实现
    • [1. 文档查看](#1. 文档查看)
    • [2. 实现部分](#2. 实现部分)
  • 二、开始模拟实现
    • [1. 初始结构](#1. 初始结构)
    • [2. 迭代器实现](#2. 迭代器实现)
      • [a. 迭代器类初始结构](#a. 迭代器类初始结构)
      • [b. 迭代器最后规模](#b. 迭代器最后规模)
      • [c. 测试自定义类型Date的->指向功能](#c. 测试自定义类型Date的->指向功能)
    • [3. begin()、end()](#3. begin()、end())
    • [4. insert()、push_back()、push_front()](#4. insert()、push_back()、push_front())
    • [5. erase()、pop_back()、pop_front()](#5. erase()、pop_back()、pop_front())
    • [6. swap()](#6. swap())
    • 7.size()、empty()、clear()
    • [8. front()、back()](#8. front()、back())
    • [9. 完善const迭代器](#9. 完善const迭代器)
      • [a. 迭代器类](#a. 迭代器类)
      • [b. list类的改动](#b. list类的改动)
    • [10. 完善构造函数](#10. 完善构造函数)
      • [a. n个value构造](#a. n个value构造)
      • [b. 迭代区间构造](#b. 迭代区间构造)
      • [c. 拷贝构造](#c. 拷贝构造)
      • [d. 列表初始化构造](#d. 列表初始化构造)
      • [e. 赋值重载](#e. 赋值重载)
      • [f. 析构](#f. 析构)
  • 总结

一、模拟实现

注意,这里的list是带头结点的双链表

1. 文档查看

Cpulsplus网站

cppreference网站

2. 实现部分

因为模拟实现,如果展开std命名空间就会出现名字冲突

因此可以用命名空间囊括起来

cpp 复制代码
// List的节点类
    template<class T>
    struct ListNode
    {
        ListNode(const T& val = T());
        ListNode<T>* _pPre;
        ListNode<T>* _pNext;
        T _val;
    };


    //List的迭代器类
    template<class T, class Ref, class Ptr>
    class ListIterator
    {
        typedef ListNode<T>* PNode;
        typedef ListIterator<T, Ref, Ptr> Self;
    public:
        ListIterator(PNode pNode = nullptr);
        ListIterator(const Self& l);
        T& operator*();
        T* operator->();
        Self& operator++();
        Self operator++(int);
        Self& operator--();
        Self& operator--(int);
        bool operator!=(const Self& l);
        bool operator==(const Self& l);
    private:
        PNode _pNode;
    };

    //list类
    template<class T>
    class list
    {
        typedef ListNode<T> Node;
        typedef Node* PNode;
    public:
        typedef ListIterator<T, T&, T*> iterator;
        typedef ListIterator<T, const T&, const T&> const_iterator;
    public:
        ///
        // List的构造
        list();
        list(int n, const T& value = T());
        template <class Iterator>
        list(Iterator first, Iterator last);
        list(const list<T>& l);
        list<T>& operator=(const list<T> l);
        ~list();


        ///
        // List Iterator
        iterator begin();
        iterator end();
        const_iterator begin();
        const_iterator end();


        ///
        // List Capacity
        size_t size()const;
        bool empty()const;


        
        // List Access
        T& front();
        const T& front()const;
        T& back();
        const T& back()const;


        
        // 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);
        // 删除pos位置的节点,返回该节点的下一个位置
        iterator erase(iterator pos);
        void clear();
        void swap(list<T>& l);
    private:
        void CreateHead();
        PNode _pHead;
    };
以下模拟实现需要自己查看文档,我根据自己所需要和方法传参、返回值来实现功能
这里就不展示文档了,不知道方法怎么实现就看上面的方法

二、开始模拟实现

顺序看需求定

1. 初始结构

1、这里把LisiNode分离出去当一个结构体,到后面插入/删除好申请和销毁空间
2、这里在list类里使用typedef ListNode<T> Node;(改一个名字,如果重复利用的话就比较方便)
3、实现了一个方法,用于创建头结点
4、实现默认构造器,自动生成头结点,让_size初始为0
5、结构体指针_pHead指向头结点
6、声明_size,记录结点个数(用于后面计算返回大小)

直到现在才发现,我们插入是依靠迭代器实现的

这时我们需要想到一个问题,如果使用指针,迭代器的加加减减怎么实现
例如:
list<int> ls;
list<int>::iterator lt = ls.begin();
while(lt != ls.end())
{
	lt++;
}
lt如果使用原生指针加加,怎么能加加就跳到下一个结点(毕竟链表是有val、next、prev三个成员属性的)
链表结点在这里也是一个结构体(类也行),加加结构体指针能到下一个结构体,那不现实

那就:
那迭代器分离出去,独自实现运算符重载,每次定义一个迭代器变量就算是实例化一个类对象
这样就能实现加加减减了,为啥这样?
因为如果是指针,想到运算符重载加加减减方法?指针是内置类型,无法运算符重载重载

如果在类里面定义一个结构体指针,调用类的运算符重载?
这样子是脱离类了,你的结构体指针加加怎么可能调用的是类里的运算符重载
为啥之前的vector迭代器可以?
(因为人家本来就脱离类的,连续空间对于指针的加加就是下一个位置)

2. 迭代器实现

a. 迭代器类初始结构

看着上面需要的方法实现:
1、迭代器也是管理一个指向结构体的指针,但我把指针包装成类,因为我需要加加到下一个结点
2、两个typedef是为了方便,第一个是指向结点的指针,第二个就是自己类重命名(为了方便后面返回迭代器)
3、构造函数,使用的是外部结构体地址(指针)给本迭代器赋值,让迭代器指向结点
3、拷贝构造,防止隐式类型转换
4、声明结构体指针(可以说整个类都是为了管理这个指针)

b. 迭代器最后规模

管理一个结构体指针的类,迭代器需要支持加加减减,可以根据运算符重载走向下一个结点
在这里得理解,为什么管理一个指针需要独立出来一个类
1、如果只是单纯的结构体指针放进list类里,那么它的加加减减就会使用的是指针的加加减减
(因为结点不是连续的,因此会非法访问)
2、为啥不会调用list类里重载的加加减减?因为这时指针算是独立在类外面了,并且定义的对象才能用重载的加加减减
简单来说就是单纯走了指针加加减减的道路,不是对象,不能调用类里运算符重载的加加减减

里面的方法很常见,通常不理解就是:
1、对类和结点之间指针的不融洽,比如:
	a. 构造函数参数是外面传进来的地址,类管理就需要类里面的属性接收地址
	b. 拷贝构造是为了下面的(中间变量)对象,比如后置加加需要返回的是没更新前的值
	c. 返回的T*和T&,T*是我需要迭代器(指针)里的结点的地址,T&是需要结点,这里重载的是解引用和指向
2、无法从管理指针转换成管理类
	a. 加加减减的运算符重载,需要的是指针,意思是需要的是迭代器
	(这时迭代器是类,所以返回的是对象,毕竟可以抛弃底层把迭代器看成指针)
	b. 不等于与等于运算符重载,就是为了比较指针指向的结点是否相同

c. 测试自定义类型Date的->指向功能

这是完成了下面的插入才好做,不然没结点咋看

好玩吧!!!

这里玩的比较奇怪,但如果不理解可能也觉得可以,但如果带入->运算符重载呢
是迭代器类对象,这里走了->运算符重载,如operator->()
这里看应该是:(对象->)得出来的是一个地址,但为什么地址紧接着Date里面的属性就能引用了呢?
如同:地址属性(地址紧接属性)

其实这里编译器简化了一个指向,本来是:
迭代器.operator->()->_day
如果以正常思维就是
迭代器->->_day
优化了之后就好看还好理解,我不用理解底层就能直接找结构体里面的值:
迭代器->_day

3. begin()、end()


因为迭代器没有实现const版本,因此这里begin、end先只实现普通版本
这里最重要的思维就是走了隐式类型转换,主要过程:
1、typedef,把类类型改名成迭代器(记住,这个迭代器类是为了管理一个结构体指针)
2、begin是头结点的下一个结点,end就是头结点
3、return的是指针(地址),但返回类型是iterator(迭代器),迭代器里的构造函数是传结点地址
(结点指针与迭代器类类型不匹配,但是节点指针可以实例化成迭代器,因此在返回的时候转换成迭代器了)
想不明白吗?
看string s1 = "abcdef";
编译器不优化的时候,先把后面字符串转换成string对象,再走拷贝构造给s1,上面的原理也一样,只不过形式有点改变
也因为begin、end返回时是匿名对象,然后被外面迭代器对象接收

4. insert()、push_back()、push_front()

insert是在该迭代器的前一个位置插入结点(这里是迭代器对象的指针指向的结点之前插入)
这里有细节:
1、如果迭代器里的_pNode是私有成员属性的话,在insert里面就无法访问
(因此可以是公有成员属性,当然也可以用方法来获取,但迭代器不用那么繁琐)
2、insert可以在链表任意插入结点,这里只需要申请一个新节点就好了
3、可以先使用指针来指向各个结点,这样子连接起来方便
4、因为insert可以在任何地方插入,因此头插尾插都可以复用insert方法
(begin是第一个结点的迭代器,在前插入就是头插,end是头结,在它前面插入就是尾插)
5、返回pos位置的迭代器用于更新迭代器

5. erase()、pop_back()、pop_front()

erase是删除迭代器所指向的结点,主要过程思维是:
1、断言不让迭代器对象指向的结点是头结点
2、用结构体指针(结点指针)指向迭代器对象指向的结点
(还是得知道,迭代器对象是管理结构体指针的)
3、定义两个指针分别指向需要删除的结点的前一个结点和后一个节点
4、让前后两个节点连接起来
5、释放需要删除的结点,并把指针置空(规范化)
6、记录结点个数的_size减1
7、返回后一个结点的指针,因为走了隐式类型转换,所以在外面是接收迭代器对象
(迭代器类是以结点地址(指针)实例化的,因此结点地址是可以隐式类型转换的)

pop_back()和pop_from()是复用Insert,主要删除头结点的前一个结点和后一个节点

6. swap()

这里也主要是交换两个对象内容,指向头结点的指针内容交换和大小交换
这里链表是不需要变动的,只要两个对象管理的结点指针指向交换就好了

7.size()、empty()、clear()

1、size()可以利用之前的属性_size返回,里面记录着大小,这是用空间换时间的思维
2、empty()直接判断_size是否是0来返回真假
(这两个const修饰是为了const对象和普通对象都能使用)
3、clear()是除了头结点之外的结点都删除,需要遍历,可以自己手搓,也可以复用erase来删除,_size来判断是否还有结点删除

8. front()、back()

这里主要是不理解返回的T&或const T&
可以看实例化时的list<int>,这时的T就是int
这时可以思考,怎么返回,如果返回pre或next就会发现,指针不是int,那就说明不是结构体指针
如果解引用,那也是结构体ListNode,并不是int,这时可以往结构体里面看,存在属性val是T类型
所以这里返回的是结构体里面存的值
这里const对象和普通对象一样的返回,会隐式类型转换

9. 完善const迭代器

以上都是没有实现const迭代器的,如果是const对象,那么就会出现const对象的结点被修改的问题

根据上面需要完善的list类就大概知道,const迭代器是在原有的迭代器类上修改的

因此从几个不同方向改动:

a. 迭代器类


这里把类模版难度加深了一下,后面两个是传引用和指针,这时就比较灵活
1、假设传的是普通引用和普通指针,那么下面解引用和指向返回类型也是普通类型
2、假设传的是const引用和const指针,那么下面两个函数就返回const类型
(这类型是结构体存储的数据类型,因为迭代器解引用和指向返回都是需要结构体所存储的数据或地址)
这时就可以实现普通迭代器和const迭代器
(const迭代器和普通迭代器的区别就是能不能改变结构体存储的内容)

b. list类的改动

改动上,之前typedef的迭代器加上了T&和T*的指定,这样可以和const迭代器区分
1、加多了typedef迭代器类成const_iterator
2、加多两个const对象对应的返回const_iterator的方法begin、end
(这里方法后面用const修饰是为了重写方法和对应const对象)
不然不小心使用了(假设是const对象ls)list<int>iterator lt = ls.begin();
这里ls是const对象,假设调用了非const函数,这里就权限放大了,编译器不允许
假设有了const版本的begin、end,如果没有const_iterator对应,那在迭代器类里也权限放大了,妥妥报错
(毕竟上面解引用运算符重载返回的是内容的引用,如果没有const修饰,那么外部就能修改了)

10. 完善构造函数

a. n个value构造

使用n个value构造:
这里参数列表里使用的T()可以算一个跟随类型自动生成初始值,如指针就nullptr,int就是0,还有其他默认值
1、先建立头结点(这里把_size放到CreateHead里面置0了)
2、根据n来循环的push_back(value);反正都是同一个值,可以直接乱插入
(当然也可以insert或push_front)

b. 迭代区间构造

根据迭代区间初始化对象:
1、因为可能是不同类型的迭代器,不能用list的迭代器定死这个构造函数,因此是函数模版
2、CreateHead();生成头结点
3、while循环从first到last区间取值,结束条件是first==last,循环条件相反
(这里别人的迭代器解引用、加加减减是人家迭代器实现的,可以走)

c. 拷贝构造

拷贝构造的函数都是类似这种结构的,也只能走引用(应该构造那个有讲)
这里直接使用迭代器循环遍历,把值直接push_back();进要初始化的对象里
(这里也可以使用范围for)

d. 列表初始化构造

用列表初始化,例如:list<int> ls = { 1,2,3,4,5 };
这个隐式类型转换也是一个对象,有自己的迭代器
因此可以在创建头结点之后直接用范围for然后push_back();
(这里只能push_back();,因为遍历这个列表也是从头遍历,从后面插入数据就会顺序相同)

e. 赋值重载

赋值重载这里走现代方式,s1 = s2;
走赋值重载,可以直接用一个中间对象(中间变量)生成一份和s2相同的
然后使用swap互换两个的指向内容,之后可以直接返回*this(因为this是当前对象的指针,用着没感觉而已)
为啥不用释放这个中间对象l,因为它的生命周期就只在这个方法内,出了作用域,生命周期结束自动调用析构函数

f. 析构

析构这里调用了clear清除结点(因为里面也是erase删除)
如果自己手搓循环变量释放也是可以的,注意防止野指针就好
清除完结点之后释放头结点,让_pHead置空
(这里在clear或erase已经让_size变动了,不清零也行)
如果是自定义类型,delete释放的时候会调用它的析构函数

总结

建议实现前先使用一下容器库的list里面的方法,理解使用方法就好实现

这里上面的实现可能会有小问题,但我一边实现一边总结,我找不到
(CreateHead那里new Node那里不能放值,容易出错,我已经把-1删了,因为不一定是整型那些内置类型)
这里可以说是一步一步走了,缺什么补什么,不缺的放后边实现,这样子可以减少分支
以上就是这里list的实现了,简单实现一下就行了,这里重要的是理解迭代器,迭代器不一定是指针,这里变成管理指针的类了

加油吧!少年

相关推荐
机器视觉知识推荐、就业指导1 小时前
C++设计模式:建造者模式(Builder) 房屋建造案例
c++
阿龟在奔跑2 小时前
引用类型的局部变量线程安全问题分析——以多线程对方法局部变量List类型对象实例的add、remove操作为例
java·jvm·安全·list
Yang.993 小时前
基于Windows系统用C++做一个点名工具
c++·windows·sql·visual studio code·sqlite3
熬夜学编程的小王3 小时前
【初阶数据结构篇】双向链表的实现(赋源码)
数据结构·c++·链表·双向链表
zz40_3 小时前
C++自己写类 和 运算符重载函数
c++
六月的翅膀3 小时前
C++:实例访问静态成员函数和类访问静态成员函数有什么区别
开发语言·c++
liujjjiyun4 小时前
小R的随机播放顺序
数据结构·c++·算法
¥ 多多¥4 小时前
c++中mystring运算符重载
开发语言·c++·算法
ashane13144 小时前
Java list
java·windows·list