首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【C++STL】以为 list 很简单?拆完它的 “骨架” 才发现,这货藏了这么多 “小心机”(附实现)

【C++STL】以为 list 很简单?拆完它的 “骨架” 才发现,这货藏了这么多 “小心机”(附实现)

作者头像
用户11960591
发布2025-12-23 15:49:52
发布2025-12-23 15:49:52
210
举报

前言:上一篇文章介绍了list的核心接口用法,本文将深入解析list的模拟实现,帮助读者理解其底层原理。

一、list的组成和结构的认识

1.1 list的基本框架

在介绍链表实现前,需要明确它与vector、string等顺序容器的区别。由于链表的物理存储空间不连续,其实现方式与顺序表存在本质差异。下面我们就来介绍一下实现链表的三个组成部分分别是:

  1. 结点的结构
  2. 迭代器相关结构
  3. 整个链表的结构

下面让我们通过源码分析来探讨 list 的实现原理:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

通过源码我们了解了链表的基本结构,那么下面我们就仿照源码里面的链表模拟实现一个!

二、链表的模拟实现

2.1 结点的结构

为什么要定义结点结构?

链表是由一个个的结点的组成,而结点是由它自身内部的数据组成。在之前我们说过链表的结点分为数据域,和指针域。 数据域存放的就是实际数据,指针域存放的就是下一个结点的地址(后继指针)和上一个结点的地址(前驱指针)。所以结点管理数据,链表管理结点。他们之间是包含关系!下面就来看看代码:

代码语言:javascript
复制
//定义结点
template<class T>
struct list_node
{
	T _data;//存放的数据
	list_node<T>* _next;//后继结点
	list_node<T>* _prev;//前驱结点


	//默认构造 对新结点初始化
	list_node(const T& x = T())
		:_data(x)
		,_prev(nullptr)
		,_next(nullptr)
	{
	}
};

结点使用struct定义是因为结点是要被链表访问的,所以要求公有。而struct的访问限制默认就是公有,所以使用sturct来定义。

2.2 迭代器相关结构

链表的迭代器实现就与vector、string的实现有所不同了,list的迭代器没有天然的优势它不像顺序表那样空间连续仅仅通过原生指针就能实现迭代器所需要的功能,链表的迭代器是一个结点的指针,没法像原生指针那样完成迭代器所具备的功能,所以我们必须封装它!

在这里插入图片描述
在这里插入图片描述

第一版本的迭代器:

代码语言:javascript
复制
	template<class T>
	struct __list_iterator
	{
		typedef list_node<T> Node;
		Node* _node;

		__list_iterator(Node* node)
			:_node(node)
		{}

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

		//不能实现,因为const __list_iterator对象才能调用这个重载
		//但是const __list_iterator对象不能调用++
		/*const T& operator*() const
		{
			return _node->_data;
		}*/

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

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

		__list_iterator<T>& operator--()
		{
			_node = _node->_prev;
			return *this;
		}

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

		bool operator!=(const __list_iterator<T>& it) const
		{
			return _node != it._node;
		}

		bool operator==(const __list_iterator<T>& it) const
		{
			return _node == it._node;
		}
	};

第一版迭代器只能支持普通迭代器调用,无法用于const对象。为什么不能直接用const修饰迭代器呢?前面的文章说过:const迭代器并不是指用const修饰迭代器本身,而是指迭代器指向的内容不可修改,迭代器本身仍然可以移动。如果直接用const修饰迭代器,会导致迭代器无法执行++操作,这样还如何进行遍历呢?

这意味着我们需要重新实现一个类似版本1的const迭代器。实际上,源码中实现了两种独立的迭代器类型。但我们的实现方式有所不同——通过增加模板参数来达到相同效果。具体实现如下:

最终版本:

代码语言:javascript
复制
//要使用该模板实现const迭代器 就要给模板加上对应的参数 来控制
//封装*Node 因为结点的指针解引用不是数据而是结点 结点的指针++不是下一个数据
//所以要封装重载 * ++ -> != 这些操作
template<class T,class Ref,class Ptr>
struct _list_iterator
{
	typedef list_node<T> Node;
	//self就是iterator<>这样能更好的维护代码 如果要添加参数只需要在模板这里该就行 不需要一个个的去改
	typedef _list_iterator<T, Ref, Ptr> Self;

	Node* _node;

	//给迭代器初始化 这里的迭代器相当于就是一个结点 所以我们管理的是一个结点
	_list_iterator(Node* node)
		:_node(node)
	{}
	
	//重载operator* 要求解引用拿到的是结点的数据
	Ref operator*()//引用返回支持修改  改成了Ref就由上层决定是返回T* 还是const T*
	{
		return _node->_data;
	}

	//operator++ 要求走到下一个结点 前置++ 
	Self& operator++()
	{
		_node = _node->_next;
		return *this;
	}

	//后置加加 这里一定是浅拷贝传值返回
	Self operator++(int)
	{
		Self tmp(_node);
		_node = _node->_next;
		return tmp;
		
	}

	//重载operator-> 返回的是数据的地址 所以要再加一个模板参数Ptr 为T*或const T* 由上层决定
	Ptr operator->()
	{
		return &_node->_data;
	}

	//前置--
	Self& operator--()
	{
		_node = _node->_prev;
		return _node;
	}

	Self operator--(int)
	{
		Self tmp(*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;
	}
};

注意:后置++和后置–为什么都是传值返回?

  • 在后置递增运算符的实现中,返回的是局部变量 tmp 的副本。由于 tmp 是一个局部对象,函数结束后其生命周期结束,因此不能返回其引用(否则会导致野引用)。此时返回的是通过拷贝构造函数或移动构造函数生成的临时对象。

2.3 整个链表的结构实现

链表由三个关键部分组成:首先,结点负责存储数据并维护内部指针;其次,迭代器负责链表的遍历、移动和修改操作;而链表本身则统筹管理所有结点,并为迭代器提供执行增删查改等基本操作的方法支持。

链表的整个大框架:

代码语言:javascript
复制
//定义链表
template<class T>
class list
{
	typedef list_node<T> Node;
public:
	typedef _list_iterator<T, T&,T*> iterator;
	typedef _list_iterator<T, const T&,const T*> const_iterator;
		
//为什么在链表中返回begin()end()因为只有链表知道头和尾
	iterator begin()
	{
		//使用迭代器来构造_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);
	}
private:
	Node* _head;
	size_t _size=0;
};

这里通过两个typedef就感悟到了,迭代器为什么要增加模板参数?这样两种类型的迭代器就共用一份模板了,下面画一张图大家一目了然:

在这里插入图片描述
在这里插入图片描述
2.3.1构造相关的接口实现

构造函数:

代码语言:javascript
复制
//空链表的处理
void empty_initial()
{
	_head = new Node;
	_head->_next = _head;
	_head->_prev = _head;
}


//list自己的默认构造
list()
{
	empty_initial();
}

//lit1(lit2) 拷贝构造
//在类内部还支持直接写list不带模板参数 但是最好带上 
list(list<T>& li)
{
	//给空链表初始化
	empty_initial();
	for (const auto& e : li)
	{
		push_back(e);
	}
}

//初始化列表初始化
list(initializer_list<T> il)
{
	empty_initial();
	for (const auto& e : il)
	{
		push_back(e);
	}
}


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

//现代写法
// lt1 = lt3
//list& operator=(list lt)
list<T>& operator=(list<T> lt)
{
	swap(lt);

	return *this;
}


//赋值重载
//list& operator=(list lt)  lit1=lit3
list<T>& operator=(const list<T>& lt)
{
	if (this != &lt)
	{
		clear();
		for (const auto& e : lt)
		{
			push_back(e);
		}
	}
	return *this;
}

上面设计empty_initial接口是为了让后面的几个接口都复用,所以封装了一个空链表初始化的接口。

析构函数:

代码语言:javascript
复制
~list()
{
	clear();
	delete _head;
	_head = nullptr;
}

void clear()
{
	//使用迭代器 不再使用老传统遍历链表一个结点一个结点的删除
	iterator it = begin();
	while(it!=end())
	{
		//这里为了防止迭代器失效 要更新it
		it=erase(it);
	}
}

析构过程采用了迭代器遍历而非传统的链表遍历方式,这得益于迭代器对节点的操作机制,其本质上与链表自身的节点操作原理是一致的。

2.3.2链表修改相关接口的实现
代码语言:javascript
复制
	iterator insert(iterator pos, const T& val)
	{
		//在pos前插入一个结点 知道prev  newnode cur结点最后连接就行
		Node* newnode = new Node(val);
		Node* cur = pos._node;
		Node* prev = cur->_prev;
			
		prev->_next = newnode;
		newnode->_next = cur;

		newnode->_prev = prev;
		cur->_prev = newnode;
		++_size;

		//返回迭代器
		return iterator(newnode);
		}

	iterator erase(iterator pos)
	{
		//删除pos前的结点 也是一样知道  prev  pos  next
		Node* cur = pos._node;
		Node* prev = cur->_prev;
		Node* next = cur->_next;

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

		delete cur;
		--_size;

		//返回下一个迭代器 避免迭代器失效
		return next;//或者直接返回next 走隐式类型转化
	}

//下面的头插尾插、头删尾删,复用上面的代码
	void push_back(const T& x)
	{
		insert(end(),x);
	}
	void push_front(const T& x)
	{
		insert(begin(), x);
	}

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

	void pop_front()
	{
		erase(begin());
	}
	size_t size() const
	{
			/*size_t n = 0;
			for (auto& e : *this)
			{
				++n;
			}
			return n;*/
		//直接返回成员变量 该成员变量在插入删除时会自动更改
		return _size;
	}

插入和删除是链表的两个重要的接口,实现了他们就可以复用实现一些其他的插入和删除接口!

三、总结

以上就是本篇文章的所有内容了,衷心感谢您的阅读!这篇文章凝聚了大量心血,如果觉得有帮助,请不吝点赞支持。有任何疑问,欢迎随时私信交流探讨。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-12-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、list的组成和结构的认识
    • 1.1 list的基本框架
  • 二、链表的模拟实现
    • 2.1 结点的结构
    • 2.2 迭代器相关结构
    • 2.3 整个链表的结构实现
      • 2.3.1构造相关的接口实现
      • 2.3.2链表修改相关接口的实现
  • 三、总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档