文章目录
- 一、模拟实现
-
- [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. 文档查看
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的实现了,简单实现一下就行了,这里重要的是理解迭代器,迭代器不一定是指针,这里变成管理指针的类了
加油吧!少年