初识C++ · 模拟实现list

目录

前言

1 push_back pop_back

2 迭代器类

2.1 ==  !=

2.2 ++ -- 

2.3 *

3 Print_List

4 有关自定义类型

5 有关const迭代器

6 拷贝构造 赋值 析构 Insert erase


前言

有了string,vector的基础,我们模拟实现list还是比较容易的,这里同样,先看源码进行简单的分析,这里直接说了就,list的模拟实现难就难在于,需要三个自定义类型,所以我们的重难点就是如何捋清它们三个之间的关系:

一共三个自定义类型,分别是用来控制节点的,控制迭代器的,控制链表的,那么为什么会这么复杂呢?尤其是在迭代器部分的模板有三个参数。

对于vector来说,空间是连续的,所以我们想要访问它的内容是很容易的,在vector和string中的迭代器可以理解为指针,指针++,就可以找到下一个空间,但是链表不同,链表的空间不是连续的,所以内置类型指针的++满足不了访问下一个空间的目的,那么为了能操纵迭代器的行为,我们这里就需要一个自定义类型,来让迭代器按照我们的想法去移动。

在list类中,我们看到只有一个成员变量,即node,那么随着typedef看过去,就知道link_type是控制节点的类的指针类型。

迭代器具体等会再细说,现在大体模式了解了,就开始进入吧。


1 push_back pop_back

文档里面说list是双向带头循环链表,所以我们需要一个哨兵位,也需要两个指针,所以,我们先创建一个节点类,节点类的模板也是必要的,因为节点里面不可能存的只有一种类型,除此之外还有调用对应的构造函数,因为是带头循环,所以创建好一个节点之后需要让它自己指向自己,这是构造函数的写法:

template <class T>
struct ListNode
{ListNode<T>* _next;ListNode<T>* _prev;T _data;ListNode(const T& val = T()):_next(nullptr),_prev(nullptr),_data(val){}
};

对此代码稍微有点陌生的是ListNode<T>*的写法,其实就是该节点类型的指针,便于指向下一块空间而已,对于构造函数的参数是和vector的resize很像的,给一个缺省值方便初始化,T()的写法也不陌生了吧。

节点类里面存在的是指针域和数值域,其余的也没有什么要特殊注意的。

节点类我们就创建好了,那么就该创建一个list类了:

template<class T>
class list
{
public:typedef ListNode<T> Node;private:Node* _head;size_t _size;
};

对于计数问题我们可以遍历的时候单独创建一个变量,也可以在类里面直接创建一个变量,insert的时候++,erase的时候--,两种方式任选其一都是没问题的。

这里的所以typedef最好都放在限定符的后面,有时候是会报错的,比如找不到什么的,为了方便,这里就把ListNode<T>重命名了Node。

那么,现在就满足了实现push_back的基本条件,push_back实现本身是没啥问题的,在数据结构中就提到了连接的问题,这里就直接连接了:

void push_back(const T& val)
{Node* newnode = new Node(val);newnode->_next = _head;newnode->_prev = _head->_prev;_head->_prev->_next = newnode;//下两个的顺序不能变_head->_prev = newnode;_size++;
}

这里的连接推荐的是先连接newnode,防止动其他节点的时候被修改了,比如_head->_prev要后连接,不然就会变成了newnode->_prev = newnode,就会出问题了。

尾删的操作也是很简单的,但是链表为空的时候不能删除,所以我们需要一个判断链表是否为空的函数:

bool empty_list()
{return _head->_next != _head;
}

当然,链表为空的时候_size是为0的,所以判断为空的条件有两个,我们选取任意一个都可以的。

void pop_back()
{assert(empty_list());Node* tail = _head->_prev;Node* new_tail = tail->_prev;new_tail->_next = _head;_head->_prev = new_tail;_size--;
}

那么现在我们就可以对数据进行添加和删除了,现在的情况就是,如何打印数据呢?前言提及,对于链表的迭代器不是像普通迭代器一样那么简单,所以我们这里,需要创建一个迭代器类。


2 迭代器类

对于迭代器里面,我们要搞懂一个问题就是,这个类的用处是什么?成员变量有哪些?成员函数有什么?

对于第一个问题,不用多说,是用来遍历链表的,那么第二个问题,成员变量有什么?

我们要遍历一个链表,无非就是要下一个位置的地址,在一个节点类里面,我们有前后两个节点的地址,所以我们要遍历一个链表,就需要一个当前节点,有一个节点就够了,所以:

template <class T>
struct ListIterator
{typedef ListNode<T> Node;typedef ListIterator iterator;Node* _node;ListIterator(Node* node):_node(node){}};

因为节点和迭代器我们是要访问全部成员的,所以使用了结构体,在源码里面也是这样操作的。

按照上文的理解,我们只需要一个节点,所以成员变量只有一个Node* _node,这里也要用到重命名,因为类域不一样,所以我们不能接着用list中使用的typedef,这里创建好之后,我们应该进入下一个问题,成员函数有什么?

这个问题的答案来源于,我们使用迭代器需要干什么?遍历打印的时候,我们需要判断该节点是不是尾节点,需要解引用该节点,得到里面的数值,需要迭代器++到下一个空间,也可能需要--到上一个空间,当节点里面存的是自定义类型,更麻烦,我们还需要对->进行重载,这个先不管,先一个一个函数的重载。

2.1 ==  !=

判断节点是否相等的唯一条件是,地址是否相等:

bool operator!=(const iterator& it)
{return _node != it._node;
}bool operator==(const iterator& it)
{return _node == it._node;
}

2.2 ++ -- 

为了重载更完美,重载前置和后置:

iterator operator++(int)
{Self tmp(*this);_node = _node->_next;return tmp;
}	
iterator& operator++()
{_node = _node->_next;return *this;
}iterator& operator--()
{_node = _node->_prev;return *this;
}iterator operator--(int)
{Self tmp(*this);_node = _node->_prev;return tmp;
}

返回值是引用可以减少拷贝,但是返回的是临时对象,就不能返回引用了,这个在string里面提及过,也没有什么要特别注意的。

2.3 *

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

就,so easy。

返回引用是因为遍历的时候涉及到修改,所以需要引用类型。

那么,80%的迭代器已经完成了。


3 Print_List

想要实现打印,我们的三件套,范围for,迭代器,下标访问,就失效了一个,list里面不存在下标访问。现在需要的是begin和end函数,返回的是头结点和尾结点的地址,为了和源码保持一致,这里还要实现一个const版本的,但是没什么难度:

iterator begin()
{return _head->_next;
}
iterator end()
{return _head;
}const_iterator begin() const
{return _head->_next;
}
const_iterator end() const
{return _head->_prev;
}

end节点就是哨兵位节点,即_head,现在打印的基本条件我们都满足了,那就试试吧:

template<class T>
void Print_List(list<T>& ll)
{list<int>::iterator it1 = ll.begin();while(it1 != ll.end()){cout << *it1 << " ";it1++;}cout << endl;
}

不管是用范围for也好还是迭代器,本质都是用迭代器,这里就使用迭代器就完事了。

模板也是必不可少的,因为是在类外实现的,所以我们需要类域访问限定符,这里用到的* ++ != 等操作我们都实现了,就可以完美实现打印。

测试代码:

void Test1_list()
{list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);Print_List(lt);lt.push_front(5);lt.push_front(6);lt.push_front(7);Print_List(lt);
}


4 有关自定义类型

如果,我是说如果:

struct A
{int _a1;int _a2;A(int a1 = 1,int a2 = 2):_a1(a1),_a2(a2){}
};

链表存了一个这个,我们应该如何打印呢?相信这算得上一个难度,我们先抛开这个问题,先看如如何在链表里面存进这种类型的数据:

void Test2_list()
{list<A> lt;A aa1(1, 2);//构造A aa2 = { 1,2};//隐式类型转换lt.push_back(aa1);//有名对象lt.push_back(A(2,1));//匿名对象lt.push_back({1,2});//隐式类型转换lt.push_back({9,9});
}

这和在vector里面存入一个string是一样的,有名对象和匿名对象,这个过一下就好了,我们回想C语言的一段代码:

void test_list2()
{A* ptr = &aa1;(*ptr)._a1;ptr->_a1;
}

对于一个指针,想要访问成员,需要用到->,那么我们也可以对指针进行解引用,得到该结构体,再使用.操作符进行访问,所以->实际上的操作可以理解为解引用之后再使用.操作符,那么要变身了:

T* operator->()
{return &_node->_data;
}
while (it != lt.end())
{cout << it->_a1 << ":" << it->_a2 << " ";cout << it.operator->()->_a1 << ":" << it.operator->()->_a2 << " ";cout << (*it)._a1 << ":" << (*it)._a2 << " ";it++;
}

得到了指针,就能打印,所以重载->的返回值是指针,那么为什么,打印的时候可以直接it->_a1,对于it来说,它是一个迭代器类的指针,它的成员变量是没有_a1的,这就不得不说我们的编译器了,编译器实际上,是优化了一下,真正的代码是it.operator->()->_a1,先调用了->函数,然后返回的数据类型的指针,再次调用->,这次调用的就不是函数了,是->这个操作符,这才得以打印,所以,,优化容易让人有点看不懂。


5 有关const迭代器

对于const迭代器来说,我们有一个简单粗暴的解决办法,就是重新创建一个类,原来的迭代器是ListIterator,const迭代器就叫ListConstIterator就好了:

template <class T>
struct ListConstIterator	
{typedef ListNode<T> Node;typedef ListConstIterator<T> Self;Node* _node;ListConstIterator(Node* node):_node(node){}const T& operator*(){return _node->_data;}const T* operator->(){return &_node->_data;}//返回的是迭代器Self operator++(int){Self tmp(*this);//Self tmp = _node; //错辣_node = _node->_next;return tmp;}Self& operator++(){_node = _node->next;return *this;}Self& operator--(){_node = _node->_prev;return *this;}Self operator--(int){Self tmp(*this);_node = _node->_prev;return tmp;}//迭代器里面是node 比较就是地址比较bool operator!=(const Self& it){return _node != it._node;}bool operator==(const Self& it){return _node == it._node;}
};

实际上我们只改动了两个地方,一个是*一个是->的返回值,无非是T*变成了const T*,加一个const而已。

但是仅仅是为了这两个地方,单独引入一个类太不划算了,所以这里,再次用到了模板:

template <class T,class Ref,class Ptr>
struct ListIterator
{typedef ListNode<T> Node;typedef ListIterator<T,Ref,Ptr> Self;Node* _node;ListIterator(Node* node):_node(node){}//T& operator*()Ref operator*(){return _node->_data;}//T* operator->()Ptr operator->(){return &_node->_data;}	
};

无非就是返回的指针和地址而已,那么根据参数的不同,我们返回的类型不同就ok了:

template<class T>
class list
{
public:typedef ListNode<T> Node;typedef ListIterator<T,T&,T*> iterator;typedef ListIterator<T,const T&,const T*> const_iterator;iterator begin(){return _head->_next;}iterator end()//end节点就是哨兵位{return _head;}const_iterator begin() const{return _head->_next;}const_iterator end() const{return _head->_prev;}
}

然后再再再重命名一下,就大功告成了。

测试代码:

	void PrintList(const list<int>& lt){list<int>::const_iterator it = lt.begin();while (it != lt.end()) {//*it += 10;cout << *it << ' ';it++;}cout << endl;}

6 拷贝构造 赋值 析构 Insert erase

剩下的就是收尾工作了。

insert和erase的基本没什么要注意的,已经在数据结构里面实现过了,这里直接给上代码了:

void Insert(iterator pos,const T& val)
{Node* cur = pos._node;Node* prev = cur->_prev;Node* newnode = new Node(val);newnode->_next = cur;newnode->_prev = prev;cur->_prev = newnode;prev->_next = newnode; _size++;
}iterator erase(iterator pos)
{Node* cur = pos._node;Node* prev = cur->_prev;Node* next = cur->_next;prev->_next = next;next->_prev = prev;delete cur;_size--;return iterator(next);
}

因为预防迭代器失效的问题,erase的返回值要给iterator,其他的就是正常的连接了。

实现了之后push_back和pop_back也可以复用了:

void push_back(const T& val)
{Insert(end(), val);
}void pop_back()
{erase(end()--);
}

对于拷贝构造来说,参数是引用,我们的实现方式是开一个头结点,然后为尾插,使用的是const版本的迭代器:

list(const list<T>& lt)
{empty_init();for (auto& e : lt){push_back(e);}
}

这里的empty_init就是list的构造函数:

void empty_init()
{_head = new Node;_head->_next = _head;_head->_prev = _head;_size = 0;
}
list()
{empty_init();
}

源码是这么写的,我觉得是因为不能显式的调用析构,所以需要给创造头结点的函数给单独拉出来,按照源码咯就。 

对于赋值来说,使用现代写法就可以:

list<T>& operator=(list<T> lt)
{swap(lt);return *this;
}void swap(list<T>& lt)
{std::swap(_head, lt._head);std::swap(_size, lt._size);
}

对于析构,析构即释放每个空间,实际上就是把每个节点都删除了就好,所以这里来个clear函数,专门用来删除节点:

void clear()
{iterator it = begin();while (it != end()){it = erase(it);}
}
~list()
{clear();delete _head;_head = nullptr;
}

析构函数就完美实现了。

那么list的实现,就到此为止了。


感谢阅读!

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

移动端投屏到大屏幕的操作详解

如果你懒得折腾电脑、电视或其他大屏设备上的影视软件安装及配置&#xff0c;可以选择直接在手机端上将影片投屏到电脑、电视或其他大屏设备上&#xff0c;这里给大家分享三种手机投屏的方法。 系统自带的投屏功能 不管是安卓、鸿蒙还是苹果操作系统&#xff0c;都自带了无线…

【python】OpenCV—Blob Detection(11)

学习来自OpenCV基础&#xff08;10&#xff09;使用OpenCV进行Blob检测 文章目录 1、cv2.SimpleBlobDetector_create 中文文档2、默认 parameters3、配置 parameters附录——cv2.drawKeypoints 1、cv2.SimpleBlobDetector_create 中文文档 cv2.SimpleBlobDetector_create 是 O…

关于使用南墙waf防护halo网站主页请求404报错的解决方案

文章目录 环境说明问题展示原因探究解决方法 环境说明 在1panel应用商店&#xff0c;部署南墙waf(docker版)halo(2.16.1社区版)注意部署过程中注意uuwaf必须勾选允许外部访问&#xff0c;halo可以不勾选[这里为了证明确实是南墙waf的原因&#xff0c;选择勾选] 问题展示 使…

ChatGPT-4o体验demo

OpenAI 最近推出了其最新的人工智能语言模型——GPT-4O。该模型是在原有 GPT-4 的基础上进行优化而成&#xff0c;旨在提升生成质量和响应速度。GPT-4O 采用了更加高效的架构设计&#xff0c;使其在处理复杂文本时表现出更快的速度和更高的准确性。GPT-4O 在训练过程中融入了最…

3D打印随形水路:模具水路的革命性技术

在快速发展的模具制造行业中&#xff0c;3D打印技术以其独特的优势正在引领一场技术革命。其中&#xff0c;3D打印随形水路技术&#xff0c;凭借其灵活性和定制化设计的能力&#xff0c;为模具带来了前所未有的变革。 模具3D打印随形水路技术&#xff0c;是一种利用3D打印技术制…

【RISC-V】站在巨人的肩膀上——看开源芯片、软件生态、与先进计算/人工智能/安全的结合

目录 会议议程前言开源处理器芯片的机遇与挑战&#xff0c;孙凝晖&#xff0c;中国工程院院士RISC-V原生基础软件栈&#xff0c;武延军&#xff0c;中国科学院软件研究所RISC-V推动新型架构创新&#xff0c;孟建熠&#xff0c;浙江大学 专题一&#xff1a;开源芯片开源高性能 R…

Vue3【十二】09Computed计算属性

Vue3【十二】09Computed计算属性 计算属性 获取全名 这种方式是只读的不能修改 这样定义fullName是一个计算属性&#xff0c;可读可写 案例截图 目录结构 代码 Person.vue <template><div class"person"><h1>我是 Person 组件</h1>姓&…

Spring异步任务@Async的默认线程池执行器是如何初始化的

Spring异步任务Async的默认线程池执行器&#xff0c;是从哪里来&#xff1f;是如何初始化的&#xff1f; 结论先行 异步任务Async的默认线程池执行器是通过TaskExecutionAutoConfiguration#applicationTaskExecutor自动注入的。 异步任务的线程池执行器是如何初始化的&#…

线性表和链表

一&#xff0c;线性结构 1.Array Array文档&#xff1a;可以自行阅读相关文档来了解Array class array.array(typecode[, initializer]) array.append(x)&#xff1a;添加元素到数组末尾 array.count(x)&#xff1a;计算元素出现次数 array.extend(iterable)&#xff1a;将迭代…

go语言实战--基于Vue3+gin框架的实战Cetide网项目(讲解开发过程中的各种踩坑)

最近被要求学习go语言开发&#xff0c;也就做一个项目实战巩固一下&#xff0c;也分享一下关于gin框架的实战项目 &#xff08;后续应该还是会继续学习Java&#xff0c;这一期还是做一个govue的&#xff09; 经过一段时间的开发过后&#xff0c;感觉现在的开发效率要快不少了&…

Javascript全解(基础篇)

语法与数据类型 语法 var\let\const var 声明一个变量&#xff0c;可选初始化一个值。 let 声明一个块作用域的局部变量&#xff0c;可选初始化一个值。 const 声明一个块作用域的只读常量。 用 var 或 let 语句声明的变量&#xff0c;如果没有赋初始值&#xff0c;则其值为 …

技术革命的十年:计算机、互联网、大数据、云计算与AI

近10年来&#xff0c;计算机、互联网、大数据、云计算和人工智能等技术领域发展迅速&#xff0c;带来了巨大的变革和创新。以下是各个领域的发展历史、现状、问题瓶颈、未来趋势以及可能的奇点。 计算机技术&#xff1a; 发展历史&#xff1a; 过去&#xff1a;过去十年间&am…

循环语句大揭秘:while、do-while、for、foreach你都掌握了吗?

哈喽&#xff0c;各位小伙伴们&#xff0c;你们好呀&#xff0c;我是喵手。运营社区&#xff1a;C站/掘金/腾讯云&#xff1b;欢迎大家常来逛逛 今天我要给大家分享一些自己日常学习到的一些知识点&#xff0c;并以文字的形式跟大家一起交流&#xff0c;互相学习&#xff0c;一…

Nextjs学习教程

一.手动创建项目 建议看这个中文网站文档,这个里面的案例配置都是手动的,也可以往下看我这个博客一步步操作 1.在目录下执行下面命令,初始化package.json文件 npm init -y2.安装react相关包以及next包 yarn add next react react-dom // 或者 npm install --save next react…

opera打不开网页最简单的解决办法

如果以上为解决问题&#xff0c;继续下面操作 检查网络连接&#xff1a; 确认您的电脑已连接到互联网。 检查网络连接是否稳定&#xff0c;网络速度慢或链路拥堵可能会导致网页加载失败。 修改Local State文件&#xff1a; 关闭Opera浏览器。 定位到Opera浏览器的配置…

【NI国产替代】PCIe 高速采集卡, 8 位双通道数字化仪器,采集卡最高采样率高达 5 GS/s 模拟带宽高达 500 MHz

• 8 位双通道数字化仪器 • 最高采样率高达 5 GS/s • 模拟带宽高达 500 MHz • 采用 PCIe 3.0 x 8 接口 • 基于 Xilinx Kintex UltraScale, XCKU040 • 提供硬件、FPGA、软件定制服务 高速采集卡是一款 8 位双通道数字化仪器&#xff0c;采集卡最高采样率高达 5 GS/s 模…

客户案例|Zilliz Cloud 助力点石科技转型 AI 智能服务商

福建点石科技网络科技有限公司成立于2010年&#xff0c;是国家高新技术企业&#xff0c;阿里云、蚂蚁金服等大厂海内外生态合作伙伴ISV。在餐饮、零售、酒店、旅游、商圈的行业定制化服务化上有深厚积累&#xff0c;在境内外做了大量标杆性软件项目&#xff0c;如东南亚RWS圣淘…

如何将HTTP升级成HTTPS?既简单又免费的方法!

在当今数字化时代&#xff0c;网络安全已成为用户和企业关注的焦点。HTTPS作为一种更加安全的网络通信协议&#xff0c;正逐渐取代传统的HTTP成为新的标准。对于许多网站管理员和内容创作者来说&#xff0c;如何免费升级到HTTPS是一个值得探讨的问题。本文将详细介绍一些免费的…

【动态规划-BM79 打家劫舍(二)】

题目 BM79 打家劫舍(二) 描述 你是一个经验丰富的小偷&#xff0c;准备偷沿湖的一排房间&#xff0c;每个房间都存有一定的现金&#xff0c;为了防止被发现&#xff0c;你不能偷相邻的两家&#xff0c;即&#xff0c;如果偷了第一家&#xff0c;就不能再偷第二家&#xff0c;如…

数据结构之ArrayList与顺序表(下)

找往期文章包括但不限于本期文章中不懂的知识点&#xff1a; 个人主页&#xff1a;我要学编程(ಥ_ಥ)-CSDN博客 所属专栏&#xff1a;数据结构&#xff08;Java版&#xff09; 目录 ArrayList的具体使用 118. 杨辉三角 扑克洗牌算法 接上篇&#xff1a;数据结构之ArrayLis…