STL3--手把手带你实现list
创作初心:在加深个人对知识系统理解的同时希望可以帮助到更多需要的同学
😄柯一梦的专栏系列
🚀柯一梦的gitee
🛠️柯一梦的CSDN主页
座右铭:功不唐捐,玉汝于成
list的大体框架,三个类及其主要的成员函数总览
结点类的实现
要学习list,我们可以从vector入手,vector是一个顺序表,而list只不过就是把每个结点分散开来。如果list要想像vector靠拢,我们只需要保存每一个结点的地址,通过地址来进行节点之间的访问
对于节点类的构造:
1 | template<class T> |
有些同学肯定会有这样的疑问:为什么结点的构造函数要传一个缺省值?
我们在哪些情况下会使用到这个缺省值呢?
1.我们在初始化list的时候,可以直接通过new一个node从而实现哨兵结点的无参构造
2.在实现push_back的时候,它的底层我们可以使用new node并且直接传入缺省值
llist迭代器的封装
我们在实现vector,string的时候都没有进行iterator的封装,而是直接使用typedef 将原生指针重命名为迭代器,为什么偏偏到了list就要开始封装迭代器了呢?要回答这个问题我们就要了解一下迭代器的作用。在进行迭代器遍历的时候,我们需要使用iterator类型的变量进行±,还有解引用得到每个迭代器指向的空间里的值。但是如果我们直接使用node的结点指针作为迭代器可以吗?当然不行,我们没办法实现±法,也没办法实现解引用,所以我们要封装一个类。
迭代器的类模版参数说明
这个类有三个模版参数
因为我们重定义了两个迭代器,分别是const迭代器和普通迭代器
因为const迭代器和普通迭代器的唯二区别就是解引用运算符重载和->运算符重载的返回值的类型的不同。
构造函数
这个类构造出来的对象相当于指向每个节点的指针,所以它唯一的成员变量就是一个node*。我们在写构造函数的时候缺省值要写成一个nullptr这样我们的代码会更健壮
1 | __list_iterator(Node* node = nullptr) |
++运算符重载
++运算符重载分为前置++和后置++(参数部位需要添加int进行区分)
前置++
我们返回的是++对象本身,所以我们的返回值是一个类型的引用,因为iterator对象李爱你存放的是ndoe指针,我们在更改——iterator指向的时候,只需要_node = _node->_next
1 | self& operator++()//前置++ |
后置++
后置++的返回值就是值返回了,我们要先拷贝构造一个iterator(在这里我们不需要单独去写一个,我们可以直接使用编译器生成的拷贝构造)
1 | self operator++(int)//后置++ |
–运算符重载
和++运算符同理,这里不展开介绍
前置–
1 | self& operator--()//前置-- |
后置–
1 | self operator--(int)//后置-- |
*运算符重载
返回值:我们解引用得到的就是node节点里面存放的Data元素,并且是可以修改的,所以是引用返回。但是我们为什么要将返回值弄成模版参数呢?答案是如果我们不这样写成模版参数,我们只能写两个类。
- 1、我们为什么不直接在将普通迭代前面加上一个const使其变成const迭代器呢?
因为const迭代器修饰的是指向的内容本身不能修改【因为我们封装的是迭代器,我们能访问内容的唯一途径就是通过解引用的引用返回,我们只要在这里实现封装,也就是加上一个const就好】,而不是迭代器本身,迭代器本身还要能够实现+±-功能.
- 2.我们可以使用函数重载(参数的个数,顺序,类型不同)来实现目的吗?
有的同学希望使用const T& operator*(const)来实现不同的调用,但是const修饰的是*this指针,也就是iterator,这样的话就又转回到第一个问题了。所以我们无法通过函数重载,来实现const迭代器与普通迭代器对*的调用
1 | Ref operator*() |
==运算符重载
1 | bool operator==(const self& it) |
!=运算符重载
1 | bool operator!=(const self& it) |
->运算符重载
->运算符编译器会进行一些优化,也就是会对返回值进行->,简单的说,我们要实现->运算符的返回值是节点所存内容的地址:
1 | ptr operator->() |
如果我们存入的是一自定义类型pos,这个自定义类型pos有两个成员变量,row和col,iterator里面存放的是node的指针,我们使用迭代器it->row的时候编译器会自动调用两次->,编译器会先调用我们自定义的 operator->,得到一个普通指针,然后再对这个指针使用内置的 -> 运算符。
list类的实现
首先我们要在list里面对node以及iterator的两个类进行重定义,而且node与iterator两个类都用Struct来写,方便我们调用node和iterator里面的东西
默认成员函数
构造函数
- 1、默认构造
默认构造函数就是生成一个节点,并且让这个节点形成循环,但由于考虑到很多场景下都会用到这个默认构造,所以我们干脆直接生成一个函数empty_init
1 | void empty_init() |
所以默认构造函数就是:
1 | void empty_init() |
- 2、initializer_list构造
initializer_list的写法和vector的一样,形参是一个initializer_list类,然后使用for循环和push_back将initializer_list里面的值赋到使用empty_list初始化了的对象之中
1 | list(initializer_list<T> li) |
拷贝构造函数
拷贝构造函数就是使用empty_init构造一个新的对象,然后使用for循环和push_back将元素插入构造的对象中
1 | list(const list<T>& lt) |
赋值运算符重载函数
有传统写法和现代写法两种,和之前的类似
传统写法
1 | list<T>& operator=(const list<T>& lt) |
现代写法
1 | void swap(list<T>& lt) |
析构函数
先使用clear释放除头结点外的所有节点空间,然后再释放头结点
1 | void clear() |
迭代器相关函数
begin和end、cosnt_begin与const_end
1 | iterator begin() |
插入与删除函数
push_back与pop_back
1 | void push_back(const T& x) |
Insert函数
insert函数里重要的一点就是我们传入的是一个迭代器,要使用.得到node节点,然后我们使用Node*类型的变量来接收
1 | iterator Insert(iterator pos,const T& val) |
Erase函数
重要的是我们要判断删除的是否是空链表
1 | iterator Erase(iterator pos) |
其他函数
clear
在这里我们使用的是Erase去进行挨个删除
1 | void clear() |
swap
1 | void swap(list<T>& lt) |
size
1 | size_t size()const |
最后一个函数Print
在我们使用类里面的类型的时候,我们需要在前面加上一个typename,告诉编译器,这个不是成员变量,而是一个类型
1 | void Print(const list<T>& lt) |

