【C++】STL | list (链表)详解及重要函数的实现

news2024/9/25 9:32:17

目录

前言

总代码

ListNode类框架的建立 (成员与模板)

list类的框架

普通构造 与 empty_init(适合不同类型构造函数的小函数)

list的迭代器

引子

operator++、operator--(前置与后置)

operator== 与 operator!=

operator* 与 operator->

operator*

operator->

普通迭代器 与 const迭代器的实现

begin、end(const 与 非const)

insert 插入

erase 删除

push_back、pop_back、push_front、pop_front(头删尾删、头插尾插)

拷贝构造

析构函数

赋值重载

initializer_list 构造

结语


前言

相比于vector和string,list这个迭代器可就开始上强度了

其主要的强度在于:vector和string都是开辟的一段连续的空间,所以这两个的迭代器用原生指针就可以实现

但是list不一样,list的空间是分散的,只有通过每个节点里的next指针才能找到下一个节点,还有就是实现const迭代器于普通迭代器两个版本的时候,需要在模板参数上动点手脚,这也是一个相对晦涩的地方

如果有友友想看一下list全部函数的用法的话,可以到C++较为官方的网站上去查阅,如下:

https://legacy.cplusplus.com/reference/list/list/?kw=list

总代码

如果有友友只是复习需要,只想看完整代码的话,可以直接点下面的gitee链接

当然对于看完了整篇文章的友友也可以照着这里的代码敲一篇进一步理解喔...(* ̄0 ̄)ノ

gitee - list - blog - 2024-08-07

ListNode类框架的建立 (成员与模板)

首先,库里的类是一个带头双向循环链表,我们今天的底层实现将会紧跟着库里的实现走

所以,我们的成员为:

  1. 两个指针(一个指向下一个节点_next,一个指向上一个节点_prev)
  2. 一个数据(表示节点内的数据)

而为了实现这个链表什么类型的值都可以存进去,所以我们需要写一个类模板表示那个数据类型

代码如下:

template<class T>
struct ListNode
{
	ListNode(const T& x)
		:_next(nullptr)
		,_prev(nullptr)
		,_data(x)
	{}

	ListNode<T>* _next;
	ListNode<T>* _prev;
	T _data;
};

list类的框架

我们list类里面仅需要一个参数,就是一个指向头节点的指针

由于库里的类是一个带头双向循环链表,所以我们这个指针是需要指向哨兵位的(这个在构造函数部分再解决)

另外,我们还可以给ListNode类型加一个typedef,将其改成Node方便我们后续的操作

代码如下:

template<class T>
class list
{
	typedef ListNode<T> Node;

private:
	Node* _head;
};

普通构造 与 empty_init(适合不同类型构造函数的小函数)

这个函数的作用就是——new一个哨兵位,因为我们后续会有很多种构造,比如普通的构造,拷贝构造,initializer_list构造,这些构造之前都需要创建哨兵位,而这些代码又都是重复的代码,所以就写一个小函数来放这一部分代码

代码如下:

// 初始化函数(创建哨兵位)

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

由于我们上面实现了一个empty_init小函数(作用是创建哨兵位),所以我们这个普通的构造直接使用这个小函数即可,代码如下:

list()
{
	empty_init();
}

list的迭代器

引子

我们之前的vector和string都是一段连续空间,所以原生指针就可以作为其迭代器,直接typedef一下即可

string:
typedef char* iterator;

verctor:
typedef T* iterator;

我们对其原生指针++就能找到下一个,--亦然

但是我们今天的list并不是连续的空间,所以我们可以这么玩儿:搞一个类,类里面重载operator++和operator--实现下一个和上一个,然后还有operator*,operator->等等,其成员只需要一个节点即可

同时,我们这里的节点指向的空间都是浅拷贝的,因为我要的就是指向同一块空间,这样我才能修改到,如果是另外开空间的话就成大坑了

如下:

template<class T>
struct ListIterator
{
	typedef ListNode<T> Node;
	typedef ListIterator<T> Self;

	Node* _node;

	ListIterator(Node* node)
		:_node(node)
	{}
};

我们会看到上面还typedef了一个Self,这是因为我们后续的++和--都需要返回一个iterator类型,为了方便,所以我们就多写一个typedef

operator++、operator--(前置与后置)

我们operator++的主要逻辑就是让_node指向下一个,所以我们只需要让_node做出改变,再将一个iterator类型返回即可(*this),operator--依然

//前置++与--

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

Self& operator--()
{
	_node = _node->_prev;
	return *this;
}

注意,我们这里需要的是返回一整个iterator类型,所以是先处理_node的位置再返回*this,而不是直接返回_node->_next,这样子的话返回的类型不匹配,迭代器在外面接受的时候发现类型不匹配就报错了

你可以想象一下,这个iterator是一个整体,要我们遍历到下一个的时候,我们是将这个整体里面的_node改变了,让其指向下一个之后,再返回这个整体的

同样的

前置的版本与后置的版本唯一的区别就在于参数那个地方,加一个int(没有实际意义)之后,编译器就会知道你这个是后置的(这也算是祖师爷为这块打的一个补丁吧,记住就好)

代码如下:

//后置++与--

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

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

operator== 与 operator!=

这块的代码相对简单,就不做讲解了,代码如下:

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

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

operator* 与 operator->

operator*

我们的operator*要的就是直接返回这个链表节点里面对应的值,所以我们直接返回_node里面的_data就是了,如下:

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

而operator->相对来说会更为的晦涩难懂

我们在外面使用iterator的时候,是直接 it->_data 来取到数据的,按理来说,我们在实现的内部也应该返回一个引用

但是其实编译器在这里做了一个隐藏,你看到的 it->_data 其实被隐藏了一个 ->

it->_data;
it.operator->()->_data;

如上我们可以看到,编译器在这里确实是为了美观藏了一个 ->,所以我们在返回的时候,应该返回一个指针类型的数据,也就是 _node->_data 的指针

综上,代码如下:

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

注意,这里的 ->优先级高于&,所以取到的是_data的地址

普通迭代器 与 const迭代器的实现

我们的const与非const,其实也就是operator*与operator->需要改写成const而已

至于其他的函数比如operator++,operator--,operator==,operator!=,这些都不会影响,因为const迭代器要的只是指向的内容不修改而已,++、--只是指向下一个节点而已,要加const的只是取出数据的那两个函数重载

但是这里有两个,我们难道再写一个类吗?太冗余了

那传一个const T过去吗?不行,类型不匹配(ListNode那个类的 T 不是const)

这里最好的方法就是,模板那里再加两个参数

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

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

我们可以看到,这里就只有两个函数需要用const修饰,而唯一需要改的就只有返回值,所以我们直接加两个参数:

一个用来代替T&(Ref),一个用来代替T*(Ptr)

如下:

template<class T, class Ref, class Ptr>
struct ListIterator
{
	typedef ListNode<T> Node;
	typedef ListIterator<T, Ref, Ptr> Self;

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

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

    //。。。。。。其他重载函数

};

这样子的话,我们在typedef迭代器的时候,就可以分别控制了,如下:

typedef ListIterator<T, T&, T*> iterator;
typedef ListIterator<T, const T&, const T*> const_iterator;

begin、end(const 与 非const)

我们的begin和end都需要返回一个迭代器类型的引用

begin指向的是头节点,end指向的是尾节点的下一个节点,也就是哨兵位

所以我们可以直接使用匿名对象构造一个节点后返回,如下:

iterator begin()
{
	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);
}

insert 插入

这里虽然插入并没有迭代器失效,但是库里面还是写了返回值,所以我们写的版本也写一个返回值吧

插入的参数是两个:

  1. 迭代器(代表位置)
  2. 待插入的数据

而我们只需要new一个新节点,然后将该节点的前后关系链接起来即可,图示如下:

代码如下:

//insert没有迭代器失效
iterator insert(iterator pos, const T& x)
{
	Node* cur = pos._node;
	Node* prev = cur->_prev;
	Node* newnode = new Node(x);

	newnode->_next = cur;
	newnode->_prev = prev;
	prev->_next = newnode;
	cur->_prev = newnode;

	return iterator(newnode);
}

erase 删除

需要注意的是,删除这里是有迭代器失效的,因为删除之后,这个节点就没了,而我们的迭代器还指向那块被销毁的空间,所以需要返回值,在外面接收的时候就能接收到删除位置的下一个位置的迭代器,这样子就能处理好迭代器失效的问题了

图示如下:(假设此时我要删除的是3节点)

代码如下:

//erase有迭代器失效
iterator erase(iterator pos)
{
	assert(pos != end());

	Node* cur = pos._node;
	Node* prev = cur->_prev;
	Node* next = cur->_next;

	prev->_next = next;
	next->_prev = prev;

	delete cur;

	return iterator(next);
}

push_back、pop_back、push_front、pop_front(头删尾删、头插尾插)

这里的插入和删除我们都可以复用 insert 和 erase 的逻辑

头删就是删除begin位置的节点

尾删就是删除哨兵位的上一个节点

头插就是在begin位置的前面插入新节点

尾插就是在哨兵位位置的前面插入新节点

代码如下:

void push_back(const T& x)
{
	insert(end(), x);
}

void pop_back()
{
	erase(--end());
}

void push_front(const T& x)
{
	insert(begin(), x);
}

void pop_front(const T& x)
{
	erase(begin());
}

拷贝构造

我们的拷贝构造主要分为以下两个步骤:

  1. new出哨兵位(上文中我们提到了empty_init可以直接使用该函数)
  2. 将参数传过来的list一个一个push_back

代码如下:

//拷贝构造
list(const list<T>& lt)
{
	empty_init();

	for (auto e : lt)
	{
		push_back(e);
	}
}


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

析构函数

这里的析构我们可以直接选择复用上文实现的erase

上文中提到,由于删除了之后,迭代器指向的节点被删除会发生迭代器失效,所以返回的是下一个节点的迭代器

我们这里可以直接使用迭代器访问,每遍历到一个就删除一个,然后再拿迭代器接收就行了,也不用走++,因为返回的就是下一个

删除完所有节点之后,我们还需要手动将哨兵位删除,最后再将_head指针置为空即可

代码如下:

~list()
{
	auto it = begin();
	while (it != end())
	{
		it = erase(it);
	}

	delete _head;
	_head = nullptr;
}

赋值重载

我们来思考这样一个问题,我们的整个链表,其实都只是由一个指向哨兵位的指针维护的,如果已经有一个不要的对象了,我们要获得这个对象的空间的话,我们只需要将两个对象的指针交换即可

同理,我们在这里可以让参数传一个list过来,但是我们不加&,这样编译器就会自动调用拷贝构造拷贝一份,函数结束后调用其析构销毁

而我们要快速获得这块空间的话,我们只需要将两个对象的指针交换,这样其实就间接地完成了空间的交换,也就完成了赋值重载

代码如下:

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

initializer_list 构造

initializer_list其实是一个类,这个类里面也有空间存着我们要的数据

我们需要先使用empty_list这个上文中提到的函数,将哨兵位创造出来

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

然后就只需要使用范围for将里面的数据一个一个提取出来,再依次push_back即可

代码如下:

list(initializer_list<T> il)
{
	empty_init();

	for (auto& e : il)
	{
		push_back(e);
	}
}

结语

到这里我们的list底层实现相关内容就结束啦(~ ̄▽ ̄)~

如果觉得对你有帮助的话,希望可以多多支持喔(〃 ̄︶ ̄)人( ̄︶ ̄〃)我们下一篇博客,再见!

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于主从Reactor模式的高性能服务器

服务器性能 百万并发&#xff1a;2核2G 2M Linux云服务器&#xff0c;2线程100并发请求&#xff0c;持续1000s&#xff0c;达到百万连接处理且0错误 高并发HTTP服务器项目&#xff1a;性能与功能性测试汇总-CSDN博客&#xff08;测试详细信息&#xff09; 主要通信逻辑分析 初…

专利申请后,如何支付年费?

专利申请后&#xff0c;如何支付年费&#xff1f;

Vue3项目创建及相关配置

Vue是一种用于构建用户界面的JavaScript框架。它采用了一种称为MVVM&#xff08;Model-View-ViewModel&#xff09;的架构模式。 MVVM是一种将用户界面与业务逻辑和数据分离的设计模式。它包括三个部分&#xff1a; Model&#xff08;模型&#xff09;&#xff1a;表示应用程序…

Windows安装Swoft框架

实现方式&#xff1a; 安装虚拟机&#xff0c;在虚拟机里用宝塔搭建环境后安装Swoft&#xff0c; 然后用Phpstorm SSH方式开发&#xff0c;用Apipost调用 websocket服务。 1、安装虚拟机&#xff0c;下载和安装参见 &#xff1a; https://blog.csdn.net/2401_84297265/article…

基于STM32的智能窗帘控制系统

目录 引言环境准备工作 硬件准备软件安装与配置系统设计 系统架构硬件连接代码实现 初始化代码控制代码应用场景 家居智能窗帘控制办公室窗帘自动调节常见问题及解决方案 常见问题解决方案结论 1. 引言 智能窗帘控制系统能够通过时间、光照强度或远程控制&#xff0c;实现对…

基于OBSIDIAN+Ollama+llama3.1构建个人智能助理

这里写自定义目录标题 重新定义个人智能助理&#xff1a;应用场景隐私保护与数据安全&#xff1a;构建可信赖的AI系统人工智能底座&#xff1a;搭建基于OLLAMA的大模型我的OLLAMA本地化部署配置不同模型的响应情况各大模型对计算机资源的要求&#xff08;家庭使用为主&#xff…

38.【C语言】指针(重难点)(C)

目录: 8.const 修饰指针 *修饰普通变量 *修饰指针变量 9.指针运算 *指针或-整数 *指针-指针 *指针关系运算 8.const 修饰指针 const 全称 constant adj.不变的 *修饰普通变量 #include <stdio.h> int main() {const int num 0;num 20;printf("%d\n", num)…

高并发HTTP服务器项目:性能与功能性测试汇总

负载测试 测试环境说明 云服务器&#xff1a;2核2G 2M Linux服务端和客户端环境&#xff1a;同在上述服务器中运行测试工具&#xff1a; wrk 模拟高并发请求htop检测CPU与内存使用情况 低负载压测&#xff1a;百万请求测试&#xff0c;2线程&#xff0c;100并发&#xff0c;100…

C++ 几何算法 - 求两条直线交点

一:算法介绍 1. 首先定义两条直线方程: 2. 解方程,求出x, y坐标 3. 如果x分母的行列式等于0, 说明两条直线平行或方向相反 4. 如果x,y分母的行列式都等于0,说明两条线重叠 二:代码实现: #include <cmath> #include <iostream>class Point2D { public:doubl…

Dubbo框架实现RPC远程调用包括nacos的配置和初始化

项目背景介绍 这个技术我是直接在项目中运用并且学习的&#xff0c;所以我写笔记最优先的角度就是从项目背景出发 继上一次API网关完成了这个实现用户调用一次接口之后让接口次数增多的操作之后&#xff0c;又迎来了新的问题。 就是我们在调用接口的时候需要对用户进行校验&…

关于地址的级联选择器

相信大家在项目中经常要做关于地址的级联选择器&#xff1a;效果如下图 在常见的组件库&#xff0c;例如element-ui/element-plus等中就有关于级联选择器的使用&#xff0c;但该组件并没有封装好的关于中国省市区甚至更详细的关于地址的级联选择器&#xff0c;因此我尝试在网络…

Excel中的数据筛选利器:COUNTIF函数深度解析与应用实战

文章目录 前言一、COUNTIF基础应用二、常见用法1.基本条件计数2.使用通配符进行模糊匹配3.引用单元格中的值作为条件4.多条件计数&#xff08;使用COUNTIFS&#xff09;5.重复值查找与去重6.结合其他函数使用 前言 Excel中的COUNTIF函数是一个极为强大且灵活的工具&#xff0c;…

【MongoDB 】MongoDB 介绍及应用,设计到4个案例

MongoDB 介绍概述 基础概念 MongoDB 是非关系型数据库&#xff0c;也就是nosql&#xff0c;存储json数据格式会非常灵活&#xff0c;要比数据库mysql/MariaDB更好&#xff0c;同时也能为mysql/MariaDB分摊一部分的流量压力。 对于经常读写的数据他会存入内存&#xff0c;如此…

STL中的list以及简单实现

STL的list的底层结构其实就是带头双向循环双向链表 带头双向循环双向链表又简单又好用&#xff0c;效率又高&#xff0c;所以其结构是完美的&#xff08;对于链表而言&#xff09;&#xff1a; 其中一个原因&#xff1a;有哨兵位的头节点&#xff0c;又循环&#xff0c;找尾很…

前端day7-css选择器

<!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>css</title><!-- 内嵌式CSS --><sty…

Python 为Excel单元格设置填充\背景色 (纯色、渐变、图案)

在使用Excel进行数据处理和分析时&#xff0c;对特定单元格进行背景颜色填充不仅能够提升工作表的视觉吸引力&#xff0c;还能帮助用户快速识别和区分不同类别的数据&#xff0c;增强数据的可读性和理解性。 本文将通过以下三个示例详细介绍如何使用Python在Excel中设置不同的单…

远程连接本地虚拟机失败问题汇总

前言 因为我的 Ubuntu 虚拟机是新装的&#xff0c;并且应该装的是比较纯净的版本&#xff08;纯净是指很多工具都尚未安装&#xff09;&#xff0c;然后在使用远程连接工具 XShell 连接时出现了很多问题&#xff0c;这些都是我之前没遇到过的&#xff08;因为之前主要使用云服…

javax.validation.constraints.NotEmpty 报错

1、问题 javax.validation.constraints.NotEmpty报错2、原因 validation-api版本较低问题 3、解决 升级版本 javax.validation:validation-api 由1.1.0.Final升级至 2.0.1.Final <dependency><groupId>javax.validation</groupId><artifactId>vali…

机房托管服务器说明

机房托管服务器是指将企业或个人的服务器放置到专业数据中心(IDC机房)进行管理和维护&#xff0c;由数据中心提供稳定、安全的运行环境以及网络连接等基础设施支持。rak小编为您整理发布机房托管服务器说明详细内容。 通过托管服务器到专业机房&#xff0c;企业能够享受到高性能…

【Redis 进阶】集群(重点理解流程和原理)

一、基本概念 前面学习的哨兵模式&#xff0c;提高了系统的可用性。但是真正用来存储数据的还是 master 和 slave 节点&#xff0c;所有的数据都需要存储在单个 master 和 slave 节点中。如果数据量很大&#xff0c;接近超出了 master / slave 所在机器的物理内存&#xff0c…