【C++】模拟实现list
🦄个人主页:修修修也
🎏所属专栏:实战项目集
⚙️操作环境:Visual Studio 2022
目录
一.了解项目及其功能
📌了解list官方标准
了解模拟实现list
📌了解更底层的list实现
二.list迭代器和vector迭代器的异同
📌迭代器的分类
🎏单向迭代器(Forward Iterator)
🎏双向迭代器(Bidirectional Iterator)
🎏随机迭代器(Random Access Iterator)
📌list和vector的底层实现差异对迭代器的影响
三.逐步实现项目功能模块及其逻辑详解
📌分析list的组成结构
📌实现list结点类模板
🎏构造list结点类成员变量
🎏实现list结点类构造函数
📌实现list迭代器类模板
🎏构造list迭代器类成员变量
🎏实现list迭代器类构造函数
🎏实现operator*重载函数
🎏实现前置operator++重载函数
🎏实现后置operator++重载函数
🎏实现前置operator--重载函数
🎏实现后置operator--重载函数
🎏实现operator!=重载函数
🎏实现operator==重载函数
🎏实现operator->重载函数
🎏实现const修饰的list迭代器类模板
📌实现list类模板
🎏构造list类成员变量
🎏实现list类empty_init()函数
🎏实现list类无参构造函数
🎏实现list类拷贝构造函数
🎏实现list类swap()函数
🎏实现list类operator=()函数
🎏实现list类clear()函数
🎏实现list类析构函数
🎏实现list类begin()函数
🌳实现普通list类的begin()函数
🌳实现const修饰list类的begin()函数
🎏实现list类end()函数
🌳实现普通list类的end()函数
🌳实现const修饰list类的end()函数
🎏实现list类insert()函数
🎏实现list类erase()函数
🎏实现list类push_front()函数
🎏实现list类pop_front()函数
🎏实现list类push_back()函数
🎏实现list类pop_back()函数
🎏实现list类size()函数
四.项目完整代码
test.cpp文件
list.h 文件
结语
一.了解项目及其功能
📌了解list官方标准
在本次项目中我们的目标是模拟实现一个list,先一起看一下C++标准文档中list的定义:cplusplus : C++ list标准文档
https://legacy.cplusplus.com/reference/list/list/?kw=list
总结一下:
- list是可以在O(1)范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。
- list的底层是带头双向循环链表结构,带头双向循环链表中每个数据元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素。
- list与forward_list(单链表)非常相似:最主要的不同在于forward_list是单链表,只能朝前迭代,让其更简单高效。
- 与其他的序列式容器相比(array,vector,deque),list通常在任意位置进行插入、移除元素的执行效率更好。
- 与其他序列式容器相比,list和forward_list最大的缺陷是不支持任意位置的随机访问,比如:要访问list的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;list还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的list来说这可能是一个重要的因素)
声明:
该模拟实现仅适用于STL初学小白了解list的简单实现,会结合一些STL源码作为参照,但是源码中涉及的空间配置器部分我们暂不做涉及!
在本篇博文中,将使用new/delete等方式来完成C++的动态内存的申请与释放.该方式相比于空间配置器效率有所降低,但是初学者较为友好的一种初步了解STL的一种方式.
了解模拟实现list
该list类模板使用动态内存分配空间,可以用来存储任意数量的同类型数据.
带头双向循环链表结点(Node)需要包含三个成员:前指针域prev,数据域val,后指针域next.
结点(Node)逻辑结构图示如下:
list提供的功能有:
- list的默认成员函数
- list的swap()函数
- list的clear()函数
- list的begin()函数
- list的end()函数
- list的insert()函数
- list的erase()函数
- list的push_front()函数
- list的pop_front()函数
- list的push_back()函数
- list的pop_back()函数
- list的size()函数
- list的operator=运算符重载函数
注意,因为我们要实现的list类并不只满足于只能存储一种固定类型的元素,我们在一个项目中,可能会创建几个存储不同类型元素的list,如:
list<int> lage; //存放学生年龄 list<string> lname; //存放学生姓名 list<double> lhigh; //存放学生身高 list<Student> lstu; //存放自定义学生类
因此,我们需要将list实现为一个类模板,这样才可以满足上面的需求,有关C++泛型编程模板相关知识还不是很了解的朋友可以先移步:
【C++】初阶模板
https://blog.csdn.net/weixin_72357342/article/details/137910913?spm=1001.2014.3001.5502
📌了解更底层的list实现
由于在之前C语言模拟实现双向带头循环链表的时候我们已经对链表的结构和操作进行的详细的解析,所以本篇博客的侧重点将会是C++的语法特性,而不会很细致的深入探究链表在操作上的结构特性,如果有对链表操作的底层原理和逻辑感兴趣的朋友可以先移步更偏底层逻辑实现的C语言实现双向循环链表的文章:
【数据结构】C语言实现带头双向循环链表万字详解(附完整运行代码)
https://blog.csdn.net/weixin_72357342/article/details/134513792?spm=1001.2014.3001.5502
![]()
该图为文章部分节选
二.list迭代器和vector迭代器的异同
📌迭代器的分类
迭代器是一种对象,允许顺序访问其元素而无需暴露底层表示。根据其访问方式和特性,迭代器可以被分为不同类型。这里我们介绍三种主要的迭代器:单向迭代器、双向迭代器和随机迭代器。
🎏单向迭代器(Forward Iterator)
单向迭代器只允许顺序访问元素,且只能从头到尾进行迭代。它不能返回到先前的元素。C++的forward_list、unordered_map、unordered_set等数据结构的迭代器都可以被视作单向迭代器。
特点:
- 只能向前移动,即只能进行++操作。
- 没有回退功能。
🎏双向迭代器(Bidirectional Iterator)
双向迭代器允许在迭代时向前和向后移动,可以在遍历过程中回退。这种迭代器常用于需要访问前一个元素的场景。C++的list、map、set等数据结构的迭代器都可以被视作双向迭代器。
特点:
- 支持向前和向后移动,即支持 ++ / -- 操作.
- 能够在迭代过程中返回到先前的元素。
🎏随机迭代器(Random Access Iterator)
随机迭代器允许在任意顺序中访问元素,可以直接通过索引访问。这种迭代器在数组等数据结构中常见,因为它们支持快速随机访问。C++的vector、string、deque等数据结构的迭代器都可以被视作随机迭代器。
特点:
- 可以跳转到任何元素,即可以进行 ++ / -- / + / - 操作。
- 对每个元素的访问时间复杂度通常是 O(1)。
📌list和vector的底层实现差异对迭代器的影响
我们知道,vector的底层实现是传统顺序表的形式,这种形式下对象的指针天然的就是迭代器,并且是随机迭代器,它可以通过物理的++ / -- / + / - 来精确定位要找到的元素位置.
vector底层示意图:
但是由于list在底层实现是带头双向循环链表的形式,这种形式下对象的指针就无法承担起迭代器的功能,因为它在物理内存中 ++ / -- 的结果是未知的内存块,我们想要的是对迭代器 ++ / -- 后它就能自动的指向链表的下一个/前一个结点,因此想要达到迭代器的效果,就只能自己将结点指针封装成一个类,然后重载结点指针的 ++ / -- / != /的执行逻辑,使其可以正常完成迭代器的功能,后面我们会着重分析并搭建迭代器的结构.
list底层示意图:
三.逐步实现项目功能模块及其逻辑详解
通过第一部分对项目功能的介绍,我们已经对list的功能有了大致的了解,虽然看似需要实现的功能很多,貌似一时间不知该如何下手,但我们可以分步分模块来分析这个项目的流程,最后再将各部分进行整合,所以大家不用担心,跟着我一步一步分析吧!
!!!注意,该部分的代码只是为了详细介绍某一部分的项目实现逻辑,故可能会删减一些与该部分不相关的代码以便大家理解,需要查看或拷贝完整详细代码的朋友可以移步本文第四部分。
📌分析list的组成结构
我们在之前C语言阶段就已经一起模拟实现过带头双向循环链表,可以知道C语言中带头双向循环链表的结构是由两部分组成的,一部分是链表结点,一部分是链表本身.因此我们至少要封装两个类模板才能够组成带头双向循环链表.
但是在C++中,由于STL引入了迭代器,并且因为list的结点指针在空间上不像vector那样连续,不能承担起迭代器的功能,所以我们必须将结点指针封装起来,设计一个迭代器类模板来完成迭代器相关操作的重载以便完成迭代器的功能.
综上所述,我们在本篇中要实现的结构如下图所示:
根据前面的分析,我们可以先搭建一个代码框架(该部分删除了大量的注释,只是为了把框架先拎出来给大家参考一下,后续会在具体实现部分有详解):
namespace mfc {template<class T>//链表结点类模板//struct定义结点就直接是公有的,如果用class就要将其设为publicstruct list_node {//成员变量list_node<T>* _next;list_node<T>* _prev;T _val;//构造函数list_node(const T& val = T()){}};template<class T,class Ref,class Ptr>//链表迭代器类模板struct __list_iterator{//成员变量typedef list_node<T> Node;typedef __list_iterator<T, Ref, Ptr> self;Node* _node;//构造函数__list_iterator(Node* node){}//操作符重载函数Ref operator*(){}Ptr operator->(){}self& operator++(){}self operator++(int){}self& operator--(){}self operator--(int){}bool operator!=(const self& it) const{}bool operator==(const self& it) const{}};template<class T>//链表类模板class list{typedef list_node<T> Node;//成员变量private:Node* _head;//成员函数public:typedef __list_iterator<T,T&,T*> iterator;typedef __list_iterator<T,const T&,const T*> const_iterator;//迭代器相关函数iterator begin(){}iterator end(){}const_iterator begin() const{}const_iterator end() const{}//默认成员函数void empty_init(){}list(){}list(const list<T>& lt){}void swap(list<T>& lt){}list<T>& operator=(list<T> lt){}~list(){}//成员函数void clear(){}void push_front(const T& x){}void pop_front(){}void push_back(const T& x){}void pop_back(){}iterator insert(iterator pos, const T& x){}iterator erase(iterator pos){}size_t size(){}}; }
📌实现list结点类模板
🎏构造list结点类成员变量
list结点的成员比较简单,我们在第一部分也已经大致介绍过了,即:
带头双向循环链表结点(Node)需要包含三个成员:前指针域prev,数据域val,后指针域next.结点(Node)逻辑结构图示如下:
这里还有一个小的点需要提一下,我们在这里实现的list_node类,后续是要给list类使用的,
考虑到后续我们在链表的操作函数中会有直接操作结点成员变量的情况,如:prev->_next = newnode; //相当于在list_node外部直接通过类指针访问了类成员变量_next
基于class的封装特性,class的成员变量一般都是默认为私有的,如果我们要允许其他类直接访问class的成员变量和函数,就要将其都设置为public,或者通过友元/内部类来解决成员访问问题.
既然如此,我们不妨直接使用struct定义结点成员变量和函数,因为struct定义的类的成员变量和函数默认就是公有的,完全可以满足我们的需求.
综上所述,该部分代码如下:
template<class T>//链表结点类模板struct list_node {list_node<T>* _next;list_node<T>* _prev;T _val; };
🎏实现list结点类构造函数
list结点的构造函数我们实现两个即可,一个是有参构造,一个是无参构造,而无参构造又可以通过给缺省值的方式和有参构造合二为一,所以我们用初始化列表来实现一下list_node的构造函数:
//缺省值的作用是在无参调用时直接去调用模板实例化的类的无参构造函数 //这里一定不能将缺省值给0/nullptr!因为你不知道模板实例化的类具体到底是内置类型还是自定义类型 list_node(const T& val = T()):_next(nullptr),_prev(nullptr),_val(val) {}
📌实现list迭代器类模板
🎏构造list迭代器类成员变量
list迭代器类的目的是改造并封装结点指针,使其行为符合我们期望的迭代器行为,所以list迭代器的成员变量就是list结点的指针:
struct __list_iterator {typedef list_node<T> Node;//这里typedef是为了防止我们把成员类型写成list_node*//对于类模板来说,类名不是类型,类名实例化出的类才是类型//即 list_node 不是类的类型 而 list_node<int> 才是类的类型Node* _node; };
🎏实现list迭代器类构造函数
list的迭代器构造的情况都是将一个已存在的结点指针传给iterator,使之成为list的迭代器变量,不存在无参构造的情况,因此迭代器的构造都是接收一个Node*的指针参数,然后将它赋值给_node成员变量即可,代码如下:
__list_iterator(Node* node):_node(node) {}
🎏实现operator*重载函数
对迭代器解引用的操作是为了获取list结点里面的数据,所以我们重载的operator*函数要完成的就是返回给调用函数这个迭代器指向结点的数据_val,代码如下:
//const迭代器的设计让这里的返回值又套了一个模板参数Ref //在这里不理解没关系,我们最后实现const迭代器时会详细讲 //在这里可以先认为根据模板的实例化不同 Ref = T& / const T&Ref operator*() {return _node->_val; }
🎏实现前置operator++重载函数
迭代器前置++的功能是使迭代器指向下一个链表结点,并返回下一个链表的结点,要指向下一个结点,其实就是迭代器的结点指针改为_node->_next,代码如下:
//因为我们把__list_iterator也写成类模板了,而返回值必须写的是类模板实例化的类型 //即__list_iterator<T, Ref, Ptr> //为了方便,我们不妨typedef一下这个为self 方便后续用//++前置 self& operator++() {//更新一下*this的_node指针,然后再返回*this_node = _node->_next;return *this; }
🎏实现后置operator++重载函数
迭代器后置++的功能是使迭代器指向下一个链表结点,并返回链表没有++前的结点,要指向下一个结点,其实就是迭代器的结点指针改为_node->_next,代码如下:
//后置++ self operator++(int) {self tmp(*this);//更新一下*this的_node指针,然后再返回*this当前的值_node = _node->_next;return tmp; }
🎏实现前置operator--重载函数
迭代器前置--的功能是使迭代器指向上一个链表结点,并返回上一个链表的结点,要指向上一个结点,其实就是迭代器的结点指针改为_node->_prev,代码如下:
//--前置 self& operator--() {//更新一下*this的_node指针,然后再返回*this_node = _node->_prev;return *this; }
🎏实现后置operator--重载函数
迭代器后置--的功能是使迭代器指向上一个链表结点,并返回没有--前链表的结点,要指向上一个结点,其实就是迭代器的结点指针改为_node->_prev,代码如下:
//后置-- self operator--(int) {self tmp(*this);//更新一下*this的_node指针,然后再返回*this_node = _node->_prev;return tmp; }
🎏实现operator!=重载函数
判断两个迭代器是否不相等,就是判断它们是否不指向同一个结点,即地址是否不相同,代码如下:
bool operator!=(const self& it) const {return _node != it._node; }
🎏实现operator==重载函数
判断两个迭代器是否相等,就是判断它们是否指向同一个结点,即地址是否相同,代码如下:
bool operator==(const self& it) const {return _node == it._node; }
🎏实现operator->重载函数
我们先来看一下下面代码里库里的list的使用上有没有什么特别的地方:
可以看到,对迭代器it而言,下面两行代码的结果是一样的,都是取出成员变量x:
it->x ; (*it).x ;
但是如果把这段代码用我们实现的list来完成,就会出错:
再看一下 . 操作符和 ->操作符的定义:
📖
.
操作符
- 用法:用于直接访问对象的成员(属性或方法)。
- 适用场景:当你有一个对象的实例时,使用
.
操作符来访问其成员。📖
->
操作符
- 用法:用于通过指向对象的指针来访问对象的成员。
- 适用场景:当你有一个对象的指针时,使用
->
操作符来访问其成员。
综上所述,我们还需要实现 operator-> ()函数,以便可以通过迭代器直接访问Node类型的成员_val,对迭代器而言, operator->()函数的作用就是通过迭代器指针直接返回结点Node的数据_val的地址.以便->操作符能够通过operator->()函数的返回值访问_val的成员,代码如下:
//const迭代器的设计让这里的返回值又套了一个模板参数Ptr //在这里不理解没关系,我们最后实现const迭代器时会详细讲 //在这里可以先认为根据模板的实例化不同 Ptr = T* / const T*Ptr operator->() {return &_node->_val; }
可能有朋友不理解为什么上面的函数里的 it->x 有些奇怪,分析一下好像写成it->->x才对,我画图帮大家分析并解释一下原因:
🎏实现const修饰的list迭代器类模板
我们在实现list迭代器时有一个无法忽视的问题,就是要实现正常对象迭代器和const修饰对象迭代器,先来看一下我们以往在实现vector的时候是怎么实现const迭代器的:
先画图演示一下两种const修饰指针的区别,可见上图中vector的const迭代器显然是第一张图示的情况:
我们创建const迭代器的目的就是保证const修饰的对象在使用迭代器时不能通过迭代器*或者->的访问修改其指向的数据值.那么我们要实现的list的迭代器当然是第一种const修饰的指针,那么我们list就直接套用vector的const迭代器的写法可以吗?像下面这样?
答案是绝对不可以的,简单画图分析一下大家就明白了:
基于这个原因,我们没法通过直接使用const来修饰list迭代器的方式来获得const_list迭代器,只能寻找其他的方法来实现迭代器.
通过前面的分析,我们知道,我们要实现的const迭代器就是要实现迭代器指向的_val是不能改变的.那我们为什么不在解引用重载函数operator*()部分直接让它的返回值是const修饰的类型引用呢?就像下面这样:
const T& operator*() {return _node->_val; }
同理,operator->()函数也可以通过限制其返回值来限制_val不能被更改:
const T* operator->() {return &_node->_val; }
只要完成这两个限制,我们就可以达到const修饰迭代器的要求,那么再写一个一模一样的类模板,只是operator*()函数和operator->()函数的返回值和普通迭代器不同就可以了.
但是,既然我们都用类模板了,为什么不顺便把这两个合成一下,写成一个多参数的类模板?当我们使用普通迭代器时,参数就传< T , T& , T* > ; 当我们使用const迭代器时,参数就传
< T , const T& , const T* > . 综上所述,list迭代器部分完整代码如下:
//迭代器的本质是通过自定义类型的封装,改变了类型的行为 //内置类型的行为不符合我们的要求时,C++就可以用一个类来封装这个类型,然后自己定义这个类型的行为 //这里的思路是,我们本身就想用结点的指针来做list的迭代器,但是因为list的结构原因 //它的++ / -- / * / != 等操作得到的结果不是我们想要的 //所以我们就把这个结点指针封装起来,重新定义它的++ / -- / * / != 等操作 //使这些操作得到的结果是我们想要的template<class T,class Ref,class Ptr> struct __list_iterator {typedef list_node<T> Node;typedef __list_iterator<T, Ref, Ptr> self;//成员变量Node* _node;//成员函数__list_iterator(Node* node):_node(node){}Ref operator*(){return _node->_val;}Ptr operator->(){return &_node->_val;}//++前置self& operator++(){//更新一下*this的_node指针,然后再返回*this_node = _node->_next;return *this;}//后置++self operator++(int){self tmp(*this);//更新一下*this的_node指针,然后再返回*this_node = _node->_next;return tmp;}//--前置self& operator--(){//更新一下*this的_node指针,然后再返回*this_node = _node->_prev;return *this;}//后置--self operator--(int){self tmp(*this);//更新一下*this的_node指针,然后再返回*this_node = _node->_prev;return tmp;} bool operator!=(const self& it) const{return _node != it._node;}bool operator==(const self& it) const{return _node == it._node;} };
📌实现list类模板
🎏构造list类成员变量
list类成员变量组成比较简单,就是一个哨兵位的头结点指针(如果介意size()函数每次调用O(n)的时间复杂度,那么可以在这里再加一个_size成员,后续size()函数只需要返回这个成员变量就行,但是每个更改链表结点个数的函数都要记得更新_size,这里我们就只是实现一个O(n)的size()函数吧)
代码如下:
template<class T> class list {typedef list_node<T> Node; public:/*因为iterator和const_iterator只有operator*和operator->的返回值不同所以我们不如把operator*和operator->的返回值也设置成模板参数,当是iterator的时候,返回值模板就是T&/T*当是const_iterator的时候,返回值模板就是const T&/const T*本质上iterator和const_iterator还是两个类,但是用模板可以简化一些*/typedef __list_iterator<T,T&,T*> iterator;typedef __list_iterator<T,const T&,const T*> const_iterator;private:Node* _head; };
🎏实现list类empty_init()函数
因为后面无参构造函数和拷贝构造函数都需要创建一个哨兵位的头结点,并初始化其成员,我们直接将这个操作封装成一个empty_init()函数,代码如下:
void empty_init() {_head = new Node;_head->_prev = _head;_head->_next = _head; }
🎏实现list类无参构造函数
list类无参构造函数主要功能是创建一个哨兵位的头结点,并初始化其成员.这个代码块我们之前已经封装成了一个函数,直接调用就行,代码如下:
list() {empty_init(); }
🎏实现list类拷贝构造函数
拷贝构造函数的功能是深拷贝一个一模一样的链表,那我们第一步是创建一个新的链表头结点,再把原链表的结点一个一个拷贝链接在新链表上就可以了,代码如下:
list(const list<T>& lt) {empty_init();for (auto& e : lt){push_back(e);} }
🎏实现list类swap()函数
swap()函数的功能是交换两个list类,只需要交换两个类里的成员变量即可,我们前面实现时成员只有_head,那就直接交换_head就行,如果有加了_size成员的朋友记得还要交换_size,代码如下:
void swap(list<T>& lt) {std::swap(_head, lt._head); }
🎏实现list类operator=()函数
我们在string和vector的模拟实现中都使用过一种简单的赋值操作符重载方式,思路就是直接把形参lt的值直接拷贝给*this,这样*this里的值就是我们想要赋给的值了,代码如下:
list<T>& operator=(list<T> lt) {swap(lt);return *this; }
🎏实现list类clear()函数
clear()函数的作用就是清空链表中的所有结点,但不包括头结点.我们按顺序将链表中的结点逐一删除即可,代码如下:
void clear() {iterator it = begin();while (it != end()){it = erase(it);} }
🎏实现list类析构函数
析构函数的作用就是销毁整个链表,其执行逻辑实际上就是删除所有结点+销毁头结点,我们复用一下clear()再销毁头结点就可以,代码如下:
~list() {clear();delete _head;_head = nullptr; }
🎏实现list类begin()函数
🌳实现普通list类的begin()函数
begin()函数就是要返回链表第一个有效结点的迭代器,实际上就是头结点后一个结点的指针,因为迭代器的底层实现是Node*,所以这里直接返回Node*,不强转成迭代器类型也没问题,因为单参数的构造函数支持隐式类型转换,代码如下:
iterator begin() {//return _head->_next;//上下两种方式都可以return (iterator)_head->_next; }
🌳实现const修饰list类的begin()函数
const修饰list的begin()函数和普通list类的begin()函数的区别就是函数接收的是const修饰的参数,返回的是const修饰的迭代器,代码如下:
const_iterator begin() const {return (const_iterator)_head->_next; }
🎏实现list类end()函数
🌳实现普通list类的end()函数
end()函数就是要返回链表最后一个有效结点后一个结点的迭代器,实际上就是头结点的指针,因为迭代器的底层实现是Node*,所以这里直接返回Node*,不强转成迭代器类型也没问题,因为单参数的构造函数支持隐式类型转换,代码如下:
iterator end() {return (iterator)_head; }
🌳实现const修饰list类的end()函数
const修饰list的end()函数和普通list类的end()函数的区别就是函数接收的是const修饰的参数,返回的是const修饰的迭代器,代码如下:
const_iterator end() const {return (const_iterator)_head; }
🎏实现list类insert()函数
这里插入的逻辑如下图:
不同之处在于C++插入的位置参数和返回的位置都是迭代器类型,具体代码逻辑见代码注释,代码如下:
//pos位置之前插入 iterator insert(iterator pos, const T& x) {//纪录下pos位置和pos的上一个位置Node* cur = pos._node;Node* prev = cur->_prev;//创建新结点Node* newnode = new Node(x);//将pos的上一个结点和新结点相互链接prev->_next = newnode;newnode->_prev = prev;//将新结点和pos结点相互链接cur->_prev = newnode;newnode->_next = cur;//返回新插入的结点的迭代器return newnode; }
🎏实现list类erase()函数
这里删除的逻辑如下图:
不同之处在于C++删除的位置参数和返回删除后下一个结点的位置都是迭代器类型,具体代码逻辑见代码注释,代码如下:
//删除pos位置的结点 iterator erase(iterator pos) {//不能删哨兵位的头结点assert(pos != end());//纪录下pos位置, pos的上一个位置和下一个位置Node* cur = pos._node;Node* prev = cur->_prev;Node* next = cur->_next;//将pos的上一个位置和下一个位置相互链接prev->_next = next;next->_prev = prev;//删除pos位置结点delete cur;//返回pos位置的下一个结点的迭代器return next; }
🎏实现list类push_front()函数
push_front()函数就是在第一个结点之前插入结点,我们直接复用insert()函数即可,代码如下:
void push_front(const T& x) {insert(begin(), x); }
🎏实现list类pop_front()函数
pop_front()函数就是删除第一个结点,我们直接复用erase()函数即可,代码如下:
void pop_front() {erase(begin()); }
🎏实现list类push_back()函数
push_back()函数就是在最后一个结点之后插入结点,即头结点之前插入结点,我们直接复用insert()函数即可,代码如下:
void push_back(const T& x) {insert(end(), x); }
🎏实现list类pop_back()函数
pop_back()函数就是删除最后一个结点,即删除头结点之前的那个结点,我们直接复用erase()函数即可,代码如下:
void pop_back() {erase(--end()); }
🎏实现list类size()函数
size()有两种求法:
- 如果list没有成员变量_size,就写一个函数手动遍历一遍计算一下有多少个结点,该方法时间复杂度是O(n).
- 如果有成员变量_size,就可以直接返回_size的值.
我们这里采用第一种方式,代码如下:
size_t size() {size_t sz = 0;//通过迭代器遍历链表iterator it = begin();while (it != end()){++sz;++it;}return sz; }
四.项目完整代码
因为模板定义和声明不能分离,所以我们将程序运行的代码分别在两个工程文件中编辑,完整代码如下:
test.cpp文件
//仅测试list功能用
#include"STL_List.h"int main()
{mfc::test_list1();mfc::test_list2();return 0;
}
list.h 文件
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;namespace mfc
{template<class T>//链表的链和结点分装成两个类struct list_node //struct定义结点就直接是公有的,如果用class就要将其设为public{list_node<T>* _next;list_node<T>* _prev;T _val;list_node(const T& val = T()):_next(nullptr),_prev(nullptr),_val(val){}};template<class T,class Ref,class Ptr>//迭代器的本质是通过自定义类型的封装,改变了类型的行为//内置类型的行为不符合我们的要求时,C++就可以用一个类来封装这个类型,然后自己定义这个类型的行为//这里的思路是,我们本身就想用结点的指针来做list的迭代器,但是因为list的结构原因//它的++ / -- / * / != 等操作得到的结果不是我们想要的//所以我们就把这个结点指针封装起来,重新定义它的++ / -- / * / != 等操作//使这些操作得到的结果是我们想要的struct __list_iterator{typedef list_node<T> Node;typedef __list_iterator<T, Ref, Ptr> self;Node* _node;__list_iterator(Node* node):_node(node){}//const迭代器的设计Ref operator*(){return _node->_val;}Ptr operator->(){return &_node->_val;}//++前置self& operator++(){//更新一下*this的_node指针,然后再返回*this_node = _node->_next;return *this;}//后置++self operator++(int){self tmp(*this);//更新一下*this的_node指针,然后再返回*this_node = _node->_next;return tmp;}//--前置self& operator--(){//更新一下*this的_node指针,然后再返回*this_node = _node->_prev;return *this;}//后置--self operator--(int){self tmp(*this);//更新一下*this的_node指针,然后再返回*this_node = _node->_prev;return tmp;} bool operator!=(const self& it) const{return _node != it._node;}bool operator==(const self& it) const{return _node == it._node;}};template<class T>class list{typedef list_node<T> Node;public://因为iterator和const_iterator只有operator*和operator->的返回值不同//所以我们不如把operator*和operator->的返回值也设置成模板参数,//当是iterator的时候,返回值模板就是T&/T*//当是const_iterator的时候,返回值模板就是const T&/const T*//本质上iterator和const_iterator还是两个类,但是用模板可以简化一些typedef __list_iterator<T,T&,T*> iterator;typedef __list_iterator<T,const T&,const T*> const_iterator;iterator begin(){//因为迭代器的底层实现是Node*,所以这里直接返回Node*没问题//单参数的构造函数支持隐式类型转换//return _head->_next;//所以上下两种方式都可以return (iterator)_head->_next;}iterator end(){return (iterator)_head;}const_iterator begin() const{return (const_iterator)_head->_next;}const_iterator end() const{return (const_iterator)_head;}void empty_init(){_head = new Node;_head->_prev = _head;_head->_next = _head;}list(){empty_init();}list(const list<T>& lt){empty_init();for (auto& e : lt){push_back(e);}}void swap(list<T>& lt){std::swap(_head, lt._head);}list<T>& operator=(list<T> lt){swap(lt);return *this;}~list(){clear();delete _head;_head = nullptr;}void clear(){iterator it = begin();while (it != end()){it = erase(it);}}void push_front(const T& x){insert(begin(), x);}void pop_front(){erase(begin());}void push_back(const T& x){/*//找尾Node* tail = _head->_prev;Node* newnode = new Node(x);//链接tail->_next = newnode;newnode->_prev = tail;newnode->_next = _head;_head->_prev = newnode;*/insert(end(), x);}void pop_back(){erase(--end());}//pos位置之前插入iterator insert(iterator pos, const T& x){//纪录下pos位置和pos的上一个位置Node* cur = pos._node;Node* prev = cur->_prev;//创建新结点Node* newnode = new Node(x);//将pos的上一个结点和新结点相互链接prev->_next = newnode;newnode->_prev = prev;//将新结点和pos结点相互链接cur->_prev = newnode;newnode->_next = cur;//返回新插入的结点的迭代器return newnode;}//删除pos位置的结点iterator erase(iterator pos){//不能删哨兵位的头结点assert(pos != end());//纪录下pos位置, pos的上一个位置和下一个位置Node* cur = pos._node;Node* prev = cur->_prev;Node* next = cur->_next;//将pos的上一个位置和下一个位置相互链接prev->_next = next;next->_prev = prev;//删除pos位置结点delete cur;//返回pos位置的下一个结点的迭代器return next;}//size求法1:写函数//方法2:加一个成员变量_size.然后insert就++,erase就--,初始化记得附0size_t size(){size_t sz = 0;iterator it = begin();while (it != end()){++sz;++it;}return sz;}private://写成list_node* _head;是错误的//因为有了类模板后,类名就不是类型了,只有类模板显示实例化出的类才是类型Node* _head;};//迭代器测试打印函数void Print(const list<int>& lt){list<int>::const_iterator it = lt.begin();while (it != lt.end()){cout << *it << " ";++it;}cout << endl;}//测试函数1void test_list1(){list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);list<int>::iterator it = lt.begin();//这个地方把begin()赋给it是拷贝构造//而iterator没有实现拷贝构造,则默认就是浅拷贝//没有出错的原因就是,这里本身就是应该浅拷贝//我把begin()赋值给it,本身就是希望it指向这个结点,而不是指向这个结点的拷贝//那么,多个指针指向一个结点没有崩溃的原因是,我们没有进行二次释放操作//一般来讲,指针的销毁都伴随着空间的销毁,但是迭代器并没有销毁链表结点的权力//所以我们不写迭代器的析构函数,迭代器的销毁不导致结点的释放,所以不会有二次释放的问题while (it != lt.end()){cout << *it << " ";++it;}cout << endl;for (auto e : lt){cout << e << " ";}cout << endl;Print(lt);}//测试函数2void test_list2(){class Point {public:int x;int y;Point(int xCoord = 0, int yCoord = 0): x(xCoord), y(yCoord){}};list<Point> lt;lt.push_back(Point(1, 1));lt.push_back(Point(2, 2));lt.push_back(Point(3, 3));lt.push_back(Point(4, 4));list<Point>::iterator it = lt.begin();while (it != lt.end()){cout << it->x << "," << it->y << endl;cout << (*it).x << "," << (*it).y << endl;cout << endl;++it;}}
}
结语
希望这篇list的实现详解能对大家有所帮助,欢迎大佬们留言或私信与我交流.
学海漫浩浩,我亦苦作舟!关注我,大家一起学习,一起进步!
相关文章推荐
【C++】模拟实现vector
【C++】标准库类型vector
【C++】模拟实现string类
【C++】构建第一个C++类:Date类
【C++】类的六大默认成员函数及其特性(万字详解)
【C++】什么是类与对象?【C++】什么是类与对象?
相关文章:

【C++】模拟实现list
🦄个人主页:修修修也 🎏所属专栏:实战项目集 ⚙️操作环境:Visual Studio 2022 目录 一.了解项目及其功能 📌了解list官方标准 了解模拟实现list 📌了解更底层的list实现 二.list迭代器和vector迭代器的异同 📌迭…...
怎么使用git merge合并两个分支?
在Git中,git merge命令用于将两个或多个开发历史(通常指分支)合并到一起。以下是一个基本的步骤指南,说明如何使用git merge来合并两个分支。 ### 前提条件 - 确保你已经安装了Git,并且熟悉基本的Git命令,如…...
ios 5.5寸、ipad13英寸如何截屏
ios上架的时候,你可能会发现,上架需要ios 5.5寸,ipad需要13英寸的屏幕截屏。 但是尴尬了,我们手头上的手机,可能是最新的iphone 15,并没有远古时代iphone 8 plus的5.5寸,那么我们该如何截屏呢&…...
spdlog日志库--输出格式(fmt 库集成)
系列目录 spdlog日志库–基础介绍 spdlog日志库–源码解析 文章目录 1. 格式输出fmt格式输出2. format_spec 格式空间正数和负数的格式#号控制输出格式3. %s占位符 切换 {}占位符 (fmtlib(fmt::format)){}占位符 -> %s等占位符%s占位符 -> {}占位符4. 不使用占位符({}、%…...

Docker简介 MacM1安装Docker
文章目录 1 Docker简介2 Docker VS 虚拟机1 Docker优势2 Docker用途 3 MacM1 下载安装Docker1 配置环境变量 4 配置Docker2 设置Docker资源3 设置Docker镜像 参考 1 Docker简介 Docker主要解决了软件开发和运行配置的问题,但是由于其功能的强大,也被应用…...

【Linux】yum软件包管理器(使用、生态、yum源切换)
目录 1.yum-软件包管理器😸1.1yum使用方法1.2什么是yum?😸1.3yum的周边生态1.4yum源切换1.4.1 查看系统本身yum源1.4.2 软件源1.4.3yum源配置 1.yum-软件包管理器 以下操作需要联网的情况下进行 😸1.1yum使用方法 安装软件时由于需…...

群晖NAS安装Video Station结合内网穿透实现远程访问本地存储的影音文件
文章目录 前言1.使用环境要求:2.下载群晖video station:3.公网访问本地群晖video station:4.公网条件下访问本地群晖video station5.公网条件下使用移动端(安卓,ios等系统)访问本地群晖video station 前言 …...
Vue中@click.stop与@click.prevent
Vue中click.stop与click.prevent 一、click.stop 问题:父元素中添加了一个click事件,其下面的子元素中也添加了click事件,此时,我想点击子元素获取子元素的点击事件,但却触发的是父元素的事件: <view …...

沐风老师3DMax对象随机颜色插件使用方法
3DMax对象随机颜色插件使用教程 3DMax对象颜色插件,是一个功能强大的脚本,它通过提供高级工具来操纵场景中的对象颜色、材质和实例,从而增强了3D设计师和艺术家的工作流程。这个多功能脚本提供了一系列功能,旨在简化对象、组和实例的着色过程。 3DMAX对象颜色插件主要具有…...

安卓将子模块打aar包,并将其远程依赖打包进去
生成 AAR 包 在Android Studio Terminal 窗口输入以下命令: ./gradlew :monitor:assembleRelease把 monitor 换成你子模块的名称,不出意外的话 就会在下面目录生成相应aar文件 注意:如果你的Java运行环境是Java 8 则在老一点的AS上 可以运…...
python 提取视频中的音频 采用ffmpeg-python 库
要使用 ffmpeg-python 库从视频文件中提取音频,首先需要确保你的系统中已经安装了 FFmpeg 和 ffmpeg-python 库。以下是详细的步骤: 步骤 1: 安装 FFmpeg 确保你的系统中已经安装了 FFmpeg。如果你使用的是 CentOS,可以参照前面的回答来安装 …...

区块链的搭建和运维4
区块链的搭建和运维4 (1) 搭建基于MySQL分布式存储的区块链 1.构建单群组网络节点 使用开发部署工具构建单群组网络节点,命令如下: bash build_chain.sh -l 127.0.0.1:4 -p 30300,20200,85452. 启动 MySQL 并设置账户密码 输入如下命令,…...

数据驱动决策:内容数据产品经理的成长与价值
数据驱动决策:内容数据产品经理的成长与价值 内容数据产品经理以数据为媒介,在用户与决策之间搭建桥梁,通过理解分析模型和用户决策路径,设计产品以促成更多决策产出,创造用户价值。例如,在衡量数据产品经理…...
pyinstaller 打包python 提示 object has no attribute
参考: 错误:gi.repository.BlockDev’ object has no attribute plugin_specs_from_names 查看包路径 rpm -ql python3-blockdev/usr/lib64/python3.7/site-packages/gi/overrides/BlockDev.py /usr/lib64/python3.7/site-packages/gi/overrides/__pyca…...

ubuntu20.04搭建RUST开发环境并与C语言交互
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 ubuntu20.04搭建RUST开发环境并与C语言交互 前言开战一、确认环境版本二、环境搭建三、hello world!四、跟c语言进行交互1.rust调用C静态库2.C调用rust库 总结参考…...

C语言 ——— 学习、使用memmove函数 并模拟实现
目录 memmvoe函数的功能 学习memmove函数编辑 模拟实现memmove函数 memmvoe函数的功能 memmvoe函数的功能类似于memcpy函数,都是内存拷贝,唯一的区别是memcpy函数不能成功拷贝原数据,而memmvoe函数可以 举例来说: [1, 2, 3…...

职场中必须明白的三个道理,不明白无出头之日,你越早知道越好
职场中有很多优秀的人才,他们工作能力出众,为人处事也非常的善良,但是有时候,这样的优点反而成了他们在职场中被欺负的原因,因为他们太善良,很容易被别人利用,为了自己的利益,有些人…...
做webserver项目的一些问题和思路总结
1.webserver是做什么的?这个项目最后想实现什么? 网络服务器,是一个处理HTTP请求并返回HTTP响应的程序。(socket实现的是网络编程,不一定是HTTP,还有其他协议,具体协议由端口来确定)…...

大数据-70 Kafka 高级特性 物理存储 日志存储 日志清理: 日志删除与日志压缩
点一下关注吧!!!非常感谢!!持续更新!!! 目前已经更新到了: Hadoop(已更完)HDFS(已更完)MapReduce(已更完&am…...

基于S7-200 SMART实现PID控制仿真实验
关键字:Matalb;S7-200 SMART;Modbus TCP;PID控制 系列文章目录 基于S7-200 SMART实现一键启停 顺序功能图——(二)设计机组延时关机程序 基于S7-200 SMART实现Modbus TCP通信 基于S7-200 SMART实现MATLAB写…...
浏览器访问 AWS ECS 上部署的 Docker 容器(监听 80 端口)
✅ 一、ECS 服务配置 Dockerfile 确保监听 80 端口 EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]或 EXPOSE 80 CMD ["python3", "-m", "http.server", "80"]任务定义(Task Definition&…...

树莓派超全系列教程文档--(61)树莓派摄像头高级使用方法
树莓派摄像头高级使用方法 配置通过调谐文件来调整相机行为 使用多个摄像头安装 libcam 和 rpicam-apps依赖关系开发包 文章来源: http://raspberry.dns8844.cn/documentation 原文网址 配置 大多数用例自动工作,无需更改相机配置。但是,一…...
rknn优化教程(二)
文章目录 1. 前述2. 三方库的封装2.1 xrepo中的库2.2 xrepo之外的库2.2.1 opencv2.2.2 rknnrt2.2.3 spdlog 3. rknn_engine库 1. 前述 OK,开始写第二篇的内容了。这篇博客主要能写一下: 如何给一些三方库按照xmake方式进行封装,供调用如何按…...

K8S认证|CKS题库+答案| 11. AppArmor
目录 11. AppArmor 免费获取并激活 CKA_v1.31_模拟系统 题目 开始操作: 1)、切换集群 2)、切换节点 3)、切换到 apparmor 的目录 4)、执行 apparmor 策略模块 5)、修改 pod 文件 6)、…...

Cilium动手实验室: 精通之旅---20.Isovalent Enterprise for Cilium: Zero Trust Visibility
Cilium动手实验室: 精通之旅---20.Isovalent Enterprise for Cilium: Zero Trust Visibility 1. 实验室环境1.1 实验室环境1.2 小测试 2. The Endor System2.1 部署应用2.2 检查现有策略 3. Cilium 策略实体3.1 创建 allow-all 网络策略3.2 在 Hubble CLI 中验证网络策略源3.3 …...

WordPress插件:AI多语言写作与智能配图、免费AI模型、SEO文章生成
厌倦手动写WordPress文章?AI自动生成,效率提升10倍! 支持多语言、自动配图、定时发布,让内容创作更轻松! AI内容生成 → 不想每天写文章?AI一键生成高质量内容!多语言支持 → 跨境电商必备&am…...
C# SqlSugar:依赖注入与仓储模式实践
C# SqlSugar:依赖注入与仓储模式实践 在 C# 的应用开发中,数据库操作是必不可少的环节。为了让数据访问层更加简洁、高效且易于维护,许多开发者会选择成熟的 ORM(对象关系映射)框架,SqlSugar 就是其中备受…...

dify打造数据可视化图表
一、概述 在日常工作和学习中,我们经常需要和数据打交道。无论是分析报告、项目展示,还是简单的数据洞察,一个清晰直观的图表,往往能胜过千言万语。 一款能让数据可视化变得超级简单的 MCP Server,由蚂蚁集团 AntV 团队…...
Web 架构之 CDN 加速原理与落地实践
文章目录 一、思维导图二、正文内容(一)CDN 基础概念1. 定义2. 组成部分 (二)CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 (三)CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 …...
MySQL 部分重点知识篇
一、数据库对象 1. 主键 定义 :主键是用于唯一标识表中每一行记录的字段或字段组合。它具有唯一性和非空性特点。 作用 :确保数据的完整性,便于数据的查询和管理。 示例 :在学生信息表中,学号可以作为主键ÿ…...