【STL】list的模拟实现

这篇具有很好参考价值的文章主要介绍了【STL】list的模拟实现。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录

前言

结构解析

默认成员函数

构造函数

拷贝构造

赋值重载

析构函数

迭代器

const迭代器

数据修改

insert

erase

尾插尾删头插头删

容量查询

源码 


前言

🍉list之所以摆脱了单链表尾插麻烦,只能单向访问等缺点,正是因为其在结构上升级成了带头双向循环链表。不仅如此,list中迭代器的实现更是能拓宽我们对迭代器的认识,话不多说,马上开始今天的内容。

结构解析

🍉以前我们实现单链表的时候就只定义了节点的结构体,之后传回第一个节点就作为首个节点直接开始使用。而今天我们要封装的是一个 list,因此需要用一个类代表整个 list,之后还需要再定义一个类来表示节点。看下下图,可以了解得比较直观一些。

【STL】list的模拟实现

🍉之后我们将二者放进命名空间中,避免与库中的 list 重复。在这之中,由于结构是带头双向循环链表,所以一个节点中需要存放前后节点的指针当前节点的数据,而头结点作为链表的首位,因此 list 中只要存头结点的指针即可

namespace Alpaca
{
	template <class T>
	struct list_node         //节点的类
	{
		list_node* _next;
		list_node* _prev;
		T _data;
	};

    template <class T>
	class list           //list的类
	{
	public:
		typedef list_node<T> node; 
    	private:
		node* _head;
	};
}

🍉提到一个容器,除了它的数据结构之外,迭代器的组成也是十分重要,之前模拟实现 string 和 vector 的时候,我们都是使用原生指针作为迭代器进行使用的,这是因为二者都是在连续的空间存储的直接 ++ 指针便可以直接访问到下一个数据。但链表的各个节点并不是开辟在相邻的空间之中,而是使用节点中的指针访问下一节点的,因此继续用原生指针 ++ 并不能达到访问的目的。所以我们需要手动封装一个迭代器来满足访问数据的需求。

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 中没有其余数据,但可以确定其中至少有一个头结点,因此不论是什么类型的构造函数,都要对头结点进行初始化。为了简化代码,不妨将这个操作单独写成一个函数

void empty_init()
{
	_head = new node;        //开辟节点
	_head->_next = _head;    //维持循环关系
	_head->_prev = _head;
}

构造函数

🍉与库中的构造函数靠齐,这里我们还是实现三种构造函数。

  • 无参数构造
  • n个值构造
  • 迭代器区间构造

🍉第一个无参数构造实际上只需要初始化头结点即可,因此直接调用上面实现的函数即可。

list()
{
	empty_init();
}

🍉第二个构造则是以 n 个值来构造,因此只需要限定次数后依次插入相同数据即可。

🍉值得注意的一点是,传入的数据类型可能是自定义类型,因此不能使用浅拷贝,而必须使用深拷贝(即调用类型本身的构造函数)。

🍉因此这里使用了 push_back,具体实现在数据修改部分讲解,本质就是调用原数据类型的拷贝构造生成一个新的数据

list(int n, const T& value = T())
{
	empty_init();
	for (int i = 0; i < n; i++)
	{
		push_back(value);
	}
}

🍉使用迭代器区间进行拷贝,但是这个迭代器并不一定是 list 的迭代器,可能是 vector 的也可能是 string 的。所以要再使用一个模板来表示迭代器的类型。因此这里同样要使用深拷贝才能确保不出错。

template<class InputIterator>
list(InputIterator first, InputIterator last)
{
	empty_init();
	while (first != last)    //遍历迭代器区间
	{
		push_back(*first);
		first++;
	}
}

拷贝构造

🍉传入一个 list,需要复制一个值一样的 list 出来,但转念一想不就跟上面使用迭代器区间构造一样吗?

list(const list<T>& l)
{
	empty_init();
	const_iterator cur = l.begin();
	while (cur != l.end())
	{
		push_back(*cur);
		cur++;
	}
}

🍉咻一下,很快啊代码就写好了。 这时候我们不禁想,既然代码重合度那么高,我们能否能够复用迭代器区间构造来实现拷贝构造呢?

🍉在这之前,有一个重要的拼图我们还没有实现,那就是交换函数,实际上我们通过复用库中的交换函数交换头结点即可。

void swap(list<T>& l)
{
	std::swap(_head, l._head);
}

🍉下面就是见证奇迹的时刻,我们通过迭代器区间构造出一个临时的 list,之后将临时的 list 和当前 list 交换,即完成拷贝构造。

list(const list<T>& l)
{
    empty_init();
	list<T> tmp(l.begin(), l.end());
	swap(tmp);
}

🍉函数结束后,tmp 就会调用析构,释放原来 list 中的节点。

赋值重载

🍉之前 vector 和 string 实现之中,我们也是直接使用了这个方法,直接传值调用赋值重载,之后交换两个 list 即可。

list<T>& operator=(list<T> l)
{
	swap(l);
	return *this;
}

析构函数

🍉list 与 vector 不一样,析构 list 时需要释放链表中的所有节点,之后再销毁头结点,因此可以单独写一个 clear 函数释放节点。

🍉从头开始删除节点即可,为了避免迭代器失效,因此要接收 erase 的返回值

void clear()
{
	iterator it = begin();
    while (it != end())
	{
		it = erase(it);
	}
}

~list()
{
	clear();
	delete _head;
	_head = nullptr;
}

迭代器

🍉在上面,我们讲过我们要对迭代器进行封装来满足实际访问时的需求,迭代器内部实际上存着一个节点的指针

🍉一般都是以节点地址作为参数来构建迭代器,而当拷贝构造时,直接赋值即可。

🍉这是因为迭代器本身起一个访问的效果,与两个指向同一地址的指针的地址并不相同是同一个道理。

list_iterator(node* n = nullptr)   //默认构造
	:_node(n)
{}

list_iterator(const self& s)       //拷贝构造
	:_node(s._node)
{}

🍉同时这个迭代器的类并不需要写析构函数,因为在这个类中我们也并没有手动申请空间需要手动释放等情况,使用默认的析构即可

🍉作为一个迭代器,我们自然要放眼于其的迭代区间,begin 和 end 的范围该如何判断

【STL】list的模拟实现

🍉再看了一眼结构图,其中头结点自然是不能访问的,头结点的下一个节点才是有效节点,那什么时候才能算访问完整个 list 呢?

🍉由于带头双向循环链表中是不会出现 nullptr 的,因此在访问完所有数据后就会回到头结点

🍉因此 begin 就是头节点的下一位,而 end 就是头节点,同时这里使用匿名对象是为了贴合编译器的优化方式

iterator begin()
{
	return iterator(_head->_next);
}

iterator end()
{
	return iterator(_head);
}

🍉之后就是对运算符进行重载了,结合实际的使用,++ 就是转换成下一个节点,-- 就是转换成上一个节点。

🍉通过对节点地址的比较便可实现两个迭代器的比较

🍉其中 self 类型就是 typedef 之后的当前迭代器类型。

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

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

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

self& operator --()
{
	_node = _node->_prev;
	return *this;
}
		
bool operator != (const self& s) const
{
	return _node != s._node;
}

bool operator == (const self& s) const
{
	return _node == s._node;
}

const迭代器

🍉这时候,有人看了一眼迭代器的模板参数,问说为什么这个迭代器需要三个模板参数呢?

iterator begin()
{
	return iterator(_head->_next);
}

🍉若我们直接像上面进行重载,在我们使用 const 迭代器时就会出现权限放大的情况。

🍉那在 iterator 前再加上 const 呢?虽然不会报错,但是实际使用起来,并不能保护我们的数据不被修改

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

🍉这是由于在只有一个模板参数的情况下, iterator 的原型就是 list_iterator<T> 而加了一个 const 就代表保护类中的指针不被修改,但实际上我们要保护不被修改的对象是类中指针指向的那个数据,因此直接在 iterator 前加 const 是没有效果的。这个时候摆在我们面前就只有两个选择了,一是专门写一份const 迭代器,二则是增加一个迭代器模板参数在使用时传入一个 const T 供我们使用。

[注意]: 直接在迭代器代码中设置返回 const 那也是不可以的,因为不是所有的链表都被限定不再修改。

🍉同样的道理不仅在 & 中并且在 * 类型中的数据也要确保能被 const 保护起来,因此,在list中便可以这样定义。

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

🍉迭代器的模板为 

template <class T, class Ref, class Ptr>

🍉如此,我们便可以将上面漏掉的 * 和 -> 重载给补上了。

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

Ptr operator ->()
{
	return &(operator*());
}

🍉值得注意的是,这里对 -> 的解引用其实是经过编译器优化的,实际上应该是需要使用两个 -> 一个用于调用重载一个用于访问类中的成员。之所以进行优化就是为了贴合使用增加可读性

假设代码这样使用    l->val
转换成            &(_node->_data)->val
实际上            l->->val    

 🍉之后我们还要在 list 中加上获取 const 类型迭代器的版本。

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

const_iterator end() const
{
	return const_iterator(_head);
}

数据修改

insert

🍉用指定一个位置,在这个位置插入一个节点,这里我们就不需要向 vector 那样挪动数据了,只需要更改节点中的指针即可。

void insert(iterator pos, const T& x = T())
{
	node* cur = pos._node;          //当前位置节点
	node* prev = cur->_prev;        //上一个节点
	node* new_node = new node(x);   //新的节点
	prev->_next = new_node;         //更改指向
	new_node->_prev = prev;
	new_node->_next = cur;
	cur->_prev = new_node;
}

🍉下图中,黑色代表原来指针,红色代表之后各个节点的指向。 由于 list 中节点各自不互相影响,因此不会出现扩容的情况,即 insert 后迭代器并不会失效。

【STL】list的模拟实现

erase

🍉erase 用于删除指定位置的节点,我们通过获取当前节点前后节点,更改二者指向后再删除当前节点。

🍉由于 erase 面临迭代器失效的问题,因此在最后我们需要返回指向下一位的迭代器用于更新。

iterator erase(iterator pos)
{
	assert(pos != end());      //不删除头结点

	node* prev = pos._node->_prev;  //拿到后节点
	node* next = pos._node->_next;

	prev->_next = next;         //更改指向
	next->_prev = prev;
	delete pos._node;           //释放节点
			 
	return iterator(next);      //返回下一个节点
}

【STL】list的模拟实现

尾插尾删头插头删

🍉通过对前面 insert 和 erase 的复用,我们便可以轻松地完成 尾插尾删 和 头插头删 的实现。

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

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

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

void pop_front()
{
	erase(begin());
}

容量查询

🍉查询 list 的容量只需要数一数节点的个数,我们遍历一遍 list 就能够得到结果。

size_t size() const
{
	size_t count = 0;
	for (auto it = begin(); it != end(); it++)
	{
		count++;
	}
	return count;
}

🍉而判空就更加简单了,只要判断头结点的下一位是否还是头结点即可。

bool empty() const
{
	return _head->_next == _head;
}

源码 

🍉感兴趣的可以来这里看看源码。

代码仓库


🍉好了,今天 list模拟实现 的相关内容到这里就结束了,如果这篇文章对你有用的话还请留下你的三连加关注。文章来源地址https://www.toymoban.com/news/detail-463843.html

到了这里,关于【STL】list的模拟实现的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • C++_STL——list模拟实现

    list作为一个容器组件,有着其重要的价值,其实在底层这是一个 带头双向循环的链表数据结构 ,可以在任意位置进行插入和删除的序列式容器,并且该容器可以前后进行遍历。 那么同样作为序列式容器,list和vector相比又有哪些优缺点呢? 优点 :对于在任意位置进行插入、

    2024年02月16日
    浏览(37)
  • C++STL的list模拟实现

    要实现STL的list, 首先我们还得看一下list的源码。 我们看到这么一个东西,我们知道C++兼容C,可以用struct来创建一个类。但是我们习惯用class。 那什么时候会用struct呢? 这个类所有成员都想开放出去,比如结点的指针,它一般开放出来。所以我们用struct.。 继续看源码比较重

    2024年02月04日
    浏览(42)
  • 【C++】STL---list的模拟实现

    上次模拟实现了一个vector容器,那么我们这次来实现一个list(链表)容器,链表在实际的开发中并不常见。但是也是一种很重要的数据结构,下面给大家介绍一下链表(list) 和 vector(顺序表)的区别。 list 和 vector 一样,是一个存储容器。不同的是vector在内存中是连续存储的,而

    2024年01月22日
    浏览(53)
  • C++ ——STL容器【list】模拟实现

    代码仓库: list模拟实现 list源码 数据结构——双向链表 源码的list是双向带头循环链表,所以我们定义两个节点,一个指向下一个,一个指向前一个 list类包含一个 _head 头节点,然后为了方便查出当前有多少个节点,还能多定义一个 _size 源码的迭代器设置了三个模板参数:

    2024年02月15日
    浏览(46)
  • [ C++ ] STL---list的模拟实现

    目录 结点类的模拟实现 迭代器类的模拟实现 构造函数 前置++与后置++ 前置- -与后置 - - == 与 !=运算符重载 * 运算符重载 - 运算符重载 普通迭代器总体实现代码 list类的实现 list类的成员变量 构造函数 迭代器 insert() erase() push_front/push_back/pop_front/pop_back front/back clear() empty()

    2024年04月09日
    浏览(60)
  • C++ [STL之list模拟实现]

    本文已收录至《C++语言》专栏! 作者:ARMCSKGT list的底层与vector和string不同,实现也有所差别,特别是在迭代器的设计上,本节将为大家介绍list简单实现,并揭开list迭代器的底层! 本文介绍list部分简单接口,以list迭代器的介绍为主! list底层是一个带头双向循环链表,在节

    2024年02月09日
    浏览(39)
  • 【STL】“list“容器从使用到模拟实现

    🎉博客主页:小智_x0___0x_ 🎉欢迎关注:👍点赞🙌收藏✍️留言 🎉系列专栏:C++初阶 🎉代码仓库:小智的代码仓库 list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。 list的底层是 双向链表结构 ,双向链表中每个元素存储在

    2024年02月16日
    浏览(86)
  • 【C++STL】list的使用及其模拟实现

    list和sting、vector一样,我们可以使用cplusplus文档进行查询:list的文档介绍 【总结】 1.list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代 2.list的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通

    2023年04月19日
    浏览(72)
  • 【C++】STL——list深度剖析 及 模拟实现

    这篇文章我们来继续STL的学习,今天我们要学习的是list,也是STL中容器的一员。 和之前一样,我们还是先学习它的使用,然后再对它进行一个深度剖析和模拟实现。 1.1 list的介绍 list的文档介绍 list的底层实现其实就是我们之前数据结构学过的带头双向循环链表: 1.2 list的使

    2024年02月05日
    浏览(43)
  • STL容器 -- list的模拟实现(配详细注释)

    C++ STL(Standard Template Library,标准模板库)提供了一组通用的模板类和函数,用于实现常用的数据结构和算法。其中之一是 std::list,它实现了一个双向链表。 std::list 是一个容器,用于存储一系列的值。与数组和向量等连续存储的容器不同,std::list 使用链表作为底层数据结构

    2024年02月16日
    浏览(48)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包