【C++进阶】STL容器--list底层剖析(迭代器封装)

目录

前言

list的结构与框架

list迭代器

list的插入和删除

insert

erase

list析构函数和拷贝构造

析构函数

 拷贝构造

赋值重载

 迭代器拷贝构造、析构函数实现问题

 const迭代器

 思考

总结


前言

        前边我们了解了list的一些使用及其注意事项,今天我们进一步深入学习一下list容器;本文的主要内容是list底层数据结构剖析以及list的模拟实现与封装;

在这里插入图片描述

list的结构与框架

list是一个带头双向循环链表,对list的模拟实现的重难点在于迭代器的实现,以及const迭代器的实现;

list实现整体需要封装三个部分:list_node(节点)、list_iterator(迭代器)、list(链表)

首先我们需要先定义一个节点类:

template<class T>
struct list_node
{
	T _data;
	list_node<T>* _next;
	list_node<T>* _prev;


	list_node(const T& x = T())
		:_data(x)
		,_next(nullptr)
		,_prev(nullptr)
	{}
};

         定义时的基本结构和C语言的很像,节点中存储两个指针,一个指向下一个节点,一个指向前一个节点,以及T类型的值(便于存储各种类型的数据);

         其次就是 list_node 的构造函数,我们在list中new节点时需要调用 list_node 的构造函数(new只要对于内置类型:开空间+调用构造函数)

list的基本结构:

class list
{
	typedef list_node<T> Node;
public:
	void empty_init() // 初始化开一个头节点,同时也为了方便拷贝构造与构造函数复用
	{
		_head = new Node;
		_head->_next = _head;
		_head->_prev = _head;
		_size = 0;
	}

	list()
	{
		empty_init();
	}
private:
	Node* _head;
	size_t _size;//记录链表长度
};

 list结构初始化需要开一个头节点,并且指针都指向自己;

接下来我们实现一下push_back(尾插),这样我们就可以先上手测试list基本结构是否完善;

 具体操作如下,可以根据图先动手尝试写一下:

 具体代码如下:

void push_back(const T& x)
{
	Node* newnode = new Node(x);
    Node* tail = head->next;
    
    // 插入节点
    tail->next = newnode;
    newnode->prev = tail;

    newnode->_next = _head;
    _head->prev = newnode;

    _size++;
}

 能够插入数据后,接下来就是遍历list,最基本的也就是迭代器遍历,前边我们使用迭代器的方法:

list<int>::iterator it = lt.begin();
while (it != lt.end())
{
	cout << *it << ' ';
	it++;
}

vector是顺序结构,它有天然的迭代器,那list是链表怎么++进行遍历?list的 “ ++ ” 本质其实就是封装+运算符重载,我们需要重载“ ++ ”,每次调用“ ++ ”时迭代器都移动到下一个节点;

         从上述迭代器的使用方法中可以看出,要想实现最基本的遍历迭代器起码要重载三部分:

!=、*(解引用)、++;

list迭代器

         要想实现list的遍历,我们首先需要实现list的迭代器,为什么要实现迭代器?

        list迭代器的实现最能体现的就是封装思想,封装屏蔽底层的差异和实现细节;其目的是为了和其他容器的遍历修改的方式保持一致;

template <class T>
struct __list_iterator
{
	typedef list_node<T> Node;
	typedef __list_iterator<T,Ref,Ptr> self;

	Node* _node;
	//构造函数
	__list_iterator(Node* node)
		:_node(node)
	{}
};

 我们使用list的节点指针来初始化构造迭代器对象,以上便是迭代器的基本框架,接下来就是迭代器的基本功能实现;

// 前置
self& operator++()
{
	_node = _node->_next;
	return *this;
}


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

//后置
self operator++(int)
{
	self tmp(*this);
	_node = _node->_next;
	return tmp;
}

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

T& operator*()//解引用返回节点的数据即可
{
	return _node->_data;
}

T* operator->()//通过指针访问成员,常用于自定义类型
{
	return &_node->_data;
}

//迭代器比较,比较的是指针是否相等,如果指针相等,则它们指向同一节点
bool operator!=(const self& s)
{
	return s._node != _node;
}

这些功能实现以后,我们还不能直接使用迭代器,我们需要把迭代器包装到我们实现的list当中;

在list里边实现begin( )、end( );

class list
{
	
public:

    typedef list_node<T> Node;
    typedef __list_iterator<T> iterator;

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

	list()
	{
		empty_init();
	}

    iterator begin();

    iterator end();
private:
	Node* _head;
	size_t _size;
};

 在实现begin( )、end( );之前,我们需要明确它们指向的位置是哪里?

iterator begin()
{
	return _head->_next;
    //返回时会自动调用iterator构造函数
}

iterator end()
{
	return _head;
}

list的插入和删除

         我们在list的基础上包装了一次iterator,那么接下来我们就在有迭代器的基础上实现insert和erase操作;

insert

根据传进来的迭代器的位置进行插入操作:

 

iterator insert(iterator pos, const T& x)
{
	Node* newnode = new Node(x);
	Node* cur = pos._node;
	Node* prev = cur->_prev;

	// 插入节点
	newnode->_next = cur;
	cur->_prev = newnode;

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

	_size++;
	return iterator(newnode);
}

 小tips

         双向循环链表在插入链接时很容易出现节点丢失的情况,为了尽可能的避免,这里推荐创建一个变量来记录当前节点的前一个位置;

erase

iterator erase(iterator pos)
{
	Node* cur = pos._node;
	Node* prev = cur->_prev;
	Node* next = cur->_next;

	delete cur;

	prev->_next = next;
	next->_prev = prev;
	
	--_size;
	return iterator(next);
}

注意:

        erase之后迭代器会失效,因为迭代器指向的节点已经被删除释放,为了避免非法访问,这里我们应该返回下一个节点;insert根据客观性来讲,应该返回新插入节点;

 有了插入和删除,在尾插、尾删、头插、头删的接口中都可以复用插入删除操作:


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

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

void pop_back(const T& x)
{
	erase(--end());
}

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

list析构函数和拷贝构造

有了迭代器析构函数和拷贝构造就会非常简单,我们可以复用前边实现的功能

析构函数

void clear()
{
	iterator it = begin();
	while (it != end())
	{
		it = erase(it);//注意删除之后需要更新it(迭代器),erase自动返回下一个节点位置
	}
}

//析构函数
~list()
{
	clear();
	delete _head;//注意释放头节点
	_head = nullptr;
}

 拷贝构造

list(list<T>& lt)
//这里应该是list(const list<T>& lt),但这样写需要const迭代器,const迭代器下面会进行实现
//所有这里先不用const修饰
{
	empty_init();
	for (auto e : lt)
	{
		push_back(e);
	}
}

赋值重载

这里说传统写法,在复用前边功能实现也是非常简单

list<int>& operator=(list<int>& lt)
{
	if (this != &lt)
	{
		clear();
		for (auto e : lt)
		{
			push_back(e);
		}
	}
	return *this;
}

现代写法:

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

 迭代器拷贝构造、析构函数实现问题

普通迭代器基本已经实现完毕,那我们来思考一下,迭代器要不要实现析构函数和拷贝构造?

        切记,迭代器不需要实现析构函数,如果实现析构函数释放空间就会把list节点释放;把节点指针给迭代器只是为了上迭代器能够访问list;

        迭代器的拷贝构造和赋值重载也不需要实现,迭代器存在的目的只是为了模拟指针去访问,如果自己实现进行深拷贝那还怎么访问list节点;

 const迭代器

         在实现之前我们需要先捋清楚const迭代器const修饰的是什么?

        

 const迭代器修饰的是iterator指向的对象,对象不能修改;

在普通迭代器前边加上const修饰(const iterator),它修饰的是iterator迭代器,迭代器要能够修改,因为我们要使用 it++向后遍历,而我们需要的是内容不能被修改;

所以要想实现内容不能修改,我们需要重新实现一个类,不能单纯的使用const修饰普通迭代器;

 我们先使用简单的方法,再封装一个和iterator相似的类,命名为const_iterator;

再封装一个类它的内容和iterator的代码很类似,这样代码复用率很低,这里我们选择使用模板来实现一个类模板,可供普通迭代器和const迭代器使用;

 这里我们可以给iterator多添加两个模板参数:


template <class T, class Ref, class Ptr >
// Ref引用   Ptr指针
struct __list_iterator
{
	typedef list_node<T> Node;
	typedef __list_iterator<T,Ref,Ptr> self;

	Node* _node;
}

 为什么要添加?

在iterator类中支持修改的接口就俩个:

  • operator*()
  • operator->()

 区别就在于它们的返回值,普通迭代器返回的是T&、T*;

const_iterator返回的是const T&、const T*;

        它们是完全不同的类型,如何做到一个函数可以返回两种类型的参数,唯有模板参数,所以这里我们添加两个参数,一个用来返回指针类型(T* 和 const T*),一个用来返回引用(T& 和  constT&) ;

这样我们只需修改两个接口:

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

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

修改之后我们就可以把它包装在自己实现的list中:

class list
{
	
public:

    typedef list_node<T> Node;
    typedef __list_iterator<T> iterator;
    typedef __list_iterator<T, const T&, const T*> const_iterator;

	//...
    iterator begin();
    iterator end();

    const_iterator begin() const;// 内容不需要修改只需对函数进行修饰,修改返回类型
    //因为返回的list节点指针可以构造iterator,也可以构造const_iterator

    const_iterator end() const;  // 成员函数后加const修饰的是成员函数,
    // 成员函数不可以修改对象的成员变量
private:
	Node* _head;
	size_t _size;
};

 我们写一个函数来测试一下:

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

	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;
}

 list的模拟实现很好是体现了封装思想,也让我们很好的感受到泛型编程的魅力;

 思考

         这个测试接口只可以输出 list<int> 类型;那么问题来了,如何修改让它可以适用于任何类型?又如何让它能够遍历任何容器(vector、list ... 都可以使用)?

如何修改让它可以适用于list的任何类型?其实很简单,添加一个模板参数即可;

template<typename T>
void print_list(const list<T>& lt)
{
	// 使用class时,list<T>未实例化的类模板,编译器不能识别进行实例化
	// 前面加一个typename就是告诉编译器,这里是一个类型,等list<T>实例化后
	// 再去类里面去取并实例化出迭代器对象
    // 这也就是class和typename的区别
	typename list<T>::const_iterator it = lt.begin();
	while (it != lt.end())
	{
		//*it = 10;
		cout << *it << " ";
		++it;
	}
	cout << endl;

	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;
}

 如何让它能够遍历任何容器(vector、list ... 都可以使用),这里只需要再抽象一层,T可以是任何数据类型,vecror<string>、list<int>都是数据类型,由于每个容器的迭代器使用方式都基本一致,所以我们可以不指定遍历容器的迭代器,相同的使用方式就可以遍历其他的容器:

template<typename Con>
void print_container(const Con& con)
{
	typename T::const_iterator it = con.begin();
	while (it != con.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

 完整代码:list模拟实现

内含反向迭代器的实现,目前阶段可以忽视


总结

        list模拟实现的目的就是为了更深刻的感受封装,以及泛型编程, list的模拟实现很好是体现了封装思想,也让我们很好的感受到泛型编程的魅力;以上便是本文的全部内容,希望对你有帮助,感谢阅读!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

132 Linux 系统编程9 ,IO操作,lseek 函数,truncate函数,查看文件的表示形式

一 lseek 函数 函数说明&#xff1a;此函数用于文件偏移 Linux中可使用系统函数lseek来修改文件偏移量(读写位置) 每个打开的文件都记录着当前读写位置&#xff0c;打开文件时读写位置是0&#xff0c;表示文件开头&#xff0c;通常读写多少个字节就会将读写位置往后移多少个字…

PixPin:一键搞定截图、长截图、贴图、GIF

名人说&#xff1a;莫道桑榆晚&#xff0c;为霞尚满天。——刘禹锡&#xff08;刘梦得&#xff0c;诗豪&#xff09; 创作者&#xff1a;Code_流苏(CSDN)&#xff08;一个喜欢古诗词和编程的Coder&#x1f60a;&#xff09; 目录 一、什么是PixPin&#xff1f;①PixPin②核心功…

C语言每日一题(61)盛最多水的容器

题目链接 力扣 11 盛最多水的容器 题目描述 给定一个长度为 n 的整数数组 height 。有 n 条垂线&#xff0c;第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。 找出其中的两条线&#xff0c;使得它们与 x 轴共同构成的容器可以容纳最多的水。 返回容器可以储存的最大水…

flet 读取本地音频文件的信息,歌名,歌手,歌曲长度,封面

请先安装 pip install flet, tinytag 组件 tinytag 是用来读取音频文件的信息的 测试用最好找一个有封面的音频的文件, 我是windows电脑,打开预览模式,选中文件时候能够右边显示图片, 如下,我电脑上某个音频文件的封面 import flet as ft from tinytag import TinyTag import…

全方位了解CRM系统:功能、种类和费用详细解说

尽管CRM已经横空出世20余年&#xff0c;但在国内普及率依旧不足20%。很多企业管理者、业务负责人对CRM是什么&#xff1f;CRM的作用、类型和价格等概念一头雾水。希望通过这篇文章深入浅出的讲解让大家对CRM管理软件从陌生到熟知。 一、CRM是什么&#xff1f; 什么是CRM&…

nginx高级配置详解

目录 一、网页的状态页 1、状态页的基本配置 2、搭配验证模块使用 3、结合白名单使用 二、nginx 第三方模块 1、echo模块 1.1 编译安装echo模块 1.2 配置echo模块 三、nginx变量 1、内置变量 2、自定义变量 四、自定义图标 五、自定义访问日志 1、自定义日志格式…

Informer:高效长序列时间序列预测模型(更新中)

文章行文思路&#xff1a; 目录 一、背景&#xff1a;1.时间序列介绍&#xff1a;2.LSTF介绍&#xff1a;3.Transformer与Informer的关系&#xff1a; 二、Transformer&#xff1a;1.Transformer简介&#xff1a;2.Transformer整体架构&#xff1a;3.模型输入&#xff1a;3.1第…

SSRF靶场实战

SSRF&#xff08;curl&#xff09; SSRF&#xff08;file_get_content&#xff09;

源代码管理——码云Gitee

目录 Git安装 Gitee配置SSH 源代码管理常规操作 1.idea配置git 2.常规操作 Git安装 安装Git是进行源代码管理的基本步骤之一。以下是在本地安装Git的通用步骤&#xff0c;适用于Windows系统&#xff1a; 下载Git安装程序: 访问Git官网的下载页面&#xff1a;Git官网下载地…

day16_ListSet课后练习题 - 参考答案

文章目录 day16_课后练习题第1题第2题第3题第4题第5题第6题第7题第8题 day16_课后练习题 第1题 案例&#xff1a; ​ 1、用一个String[]数组存点数 ​ 2、用一个String[]数组存花色 ​ 3、用一个String[]数组存大王、小王 ​ 4、用上面的数组&#xff0c;生成一副扑克牌 …

keepalived+HAProxy+MySQL双主实验

keepalivedHAProxyMySQL双主实验 环境准备 node1(HAProxy1):192.168.184.10 node2(HAProxy2):192.168.184.20 node3(MySQL1):192.168.184.30 node4(MySQL2):192.168.184.40 虚拟IP vip&#xff1a;192.168.184.100MySQL部署 在node3执行以下脚本&#xff1a; #!/bin/bash sy…

智能枪弹柜管理系统-智能枪弹管理系统DW-S306

随着社会的发展和治安形势的日益严峻&#xff0c;对于枪弹的管理变得尤为重要。传统的手工记录和存放方式已经无法满足现代化、高效化、安全化的需求。因此&#xff0c;智能枪弹柜管理系统应运而 生。 在建设万兆主干、千兆终端的监控专网的基础上&#xff0c;弹药库安全技术…

Web性能优化-详细讲解与实用方法-MDN文档学习笔记

Web性能优化 查看更多学习笔记&#xff1a;GitHub&#xff1a;LoveEmiliaForever MDN中文官网 性能优良的网站能够提高访问者留存和用户满意度&#xff0c;减少客户端和服务器之间传输的数据量可降低各方的成本 不同的业务目标和用户需求需要不同的性能度量&#xff0c;要提高…

LangChain Agent v0.2.0简明教程 (上)

快速入门指南 – LangChain中文网 langchain源码剖析系列课程 九天玩转Langchain! 1. LangChain是什么2. LangChain Expression Language (LCEL)Runnable 接口3. Model I/O3.1 Prompt Templates3.2 Language Model3.3 Output ParsersUse case(Q&A with RAG)1. LangChain…

我的128创作纪念日

目录 学习成长机遇个人数据一览榜单认可日常2024憧憬和规划创作纪念日总结 学习成长机遇 账号创建已经快9年了&#xff0c;以前一直在个人网站和简书上写文章&#xff0c;在CSDN真正写文竟然在2023年10月20&#xff0c;至今才128天&#xff0c;不过获得的数据还算可以&#xff…

力扣细节题:翻转二叉树

细节一&#xff1a;递归采用前序递归 细节二&#xff1a;采用交换节点而不是交换数据因为左右树交换的同时左右树的所有子节点都要交换 细节三&#xff1a;采用外置函数因为return如果在本函数内操作会存在必须返回空指针的问题 /*** Definition for a binary tree node.* s…

【MySQL面试复习】什么是聚簇索引(聚集索引)和非聚簇索引(二级索引)/什么是回表?

系列文章目录 在MySQL中&#xff0c;如何定位慢查询&#xff1f; 发现了某个SQL语句执行很慢&#xff0c;如何进行分析&#xff1f; 了解过索引吗&#xff1f;(索引的底层原理)/B 树和B树的区别是什么&#xff1f; 系列文章目录什么是聚簇索引&#xff08;聚集索引&#xff09…

堆排序、快速排序和归并排序

堆排序、快速排序和归并排序是所有排序中最重要的三个排序&#xff0c;也是难度最大的三个排序&#xff1b;所以本文单独拿这三个排序来讲解 目录 一、堆排序 1.建堆 2.堆排序 二、快速排序 1.思想解析 2.Hoare版找基准 3.挖坑法找基准 4.快速排序的优化 5.快速排序非…

程序媛的mac修炼手册-- 2024如何彻底卸载Python

啊&#xff0c;前段时间因为想尝试chatgpt的API&#xff0c;需要先创建一个python虚拟环境来安装OpenAI Python library. 结果&#xff0c;不出意外的出意外了&#xff0c;安装好OpenAI Python library后&#xff0c;因为身份认证问题&#xff0c;根本就没有获取API key的权限…

opencv基础 python与c++

question: pip install -i https://pypi.tuna.tsinghua.edu.cn/simple matplotlib Opencv 一、读取图片 (1).imshow Mat imread(const string& filename, intflags1 );flags: enum { /* 8bit, color or not */CV_LOAD_IMAGE_UNCHANGED -1, /* 8bit, gray */CV_LOAD_I…