C++标准模板库(STL)——list的模拟实现

C++标准模板库(STL)------list的模拟实现

在C++ 标准模板库(STL)的容器中,list 是一个极具代表性的 "双向循环链表" 实现,在任意位置的插入 /删除操作场景下有着无可替代的效率优势 。你是否好奇过list的底层原理呢?本篇文章将从底层节点结构、迭代器的封装,到容器类的完整实现,一步步讲解list的设计逻辑

文章目录

1、 list双向链表的节点

list的底层是带头双向循环链表 ,链表的所有数据都储存在一个个独立的节点中,节点通过指针串联形成完整的链表。
list由一个哨兵头节点和若干有效数据节点组成,哨兵头节点不储存数据
其他节点储存一个数值(_data),以及指向前后元素的指针(_prev _next)
,因此我们要先定义节点的结构体list_node

cpp 复制代码
//创建LI命名空间,让自己模拟实现的list与std::list隔离
namespace LI
{
	//创建类模板,接收不同类型的数据(内置类型、自定义类型)
	//用struct结构体,因为struct默认成员是公有的,方便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)
		{}
	};
}
  1. 模板参数T,支持任意数据类型(内置类型如 int,自定义类型如 string)
  2. 双向指针_next和_prev,保证了list双向遍历的能力,为后续迭代器的++、- -提供基础
  3. 默认构造的参数x = T(),支持无参构造。如果T是内置类型,T()是"零值",比如int()是0,double()是0.0,char()是'\0'。如果T是自定义类型,T()会调用默认构造函数创建对象

2、 list迭代器的实现

在类(或结构体)中, . 和 -> 本质都是访问成员的运算符

对象.成员左边是类 / 结构体的实例对象(或引用)直接访问其成员

指针->成员左边是指向类 / 结构体的指针,通过指针间接访问目标对象的成员

指针->成员 本质是 (*指针).成员 的简化写法,两者完全等价,但 -> 更简洁(尤其嵌套场景)

  1. list的迭代器不是单纯的原生指针,而是节点指针的封装 。为了统一迭代器的使用,我们将 "链表节点指针" 包装成 "像指针一样用" 的对象 ------ 支持*取值、->访问成员、++/--遍历
  2. 为什么需要迭代器封装?
    链表的底层并不是连续的空间,而是一个个分散的节点不能直接通过指针++跳转到下一个节点。需要通过迭代器对象,将节点指针的操作(_node->next)封装为统一的operator++
  3. 为什么要使用三个模板参数?
    模板参数 template<class T, class Ref, class Ptr>,目的是 一份代码复用,同时实现普通迭代器和 const 迭代器,不用写两份重复代码

如果我们要实现 list 的普通迭代器和 const 迭代器,不用模板复用的话,需要写两份几乎一样的代码,非常的冗余

cpp 复制代码
//普通版本
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需求是不能改变迭代器指向的东西,而不是迭代器本身不能改变
	/*const T& operator*() const
	{
		return _node->_data;
	}*/

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

	T* operator->()
	{
		return &_node->_data; // 返回数据的普通指针,支持修改成员
	}

	__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版本
template<class T>
struct __list_const_iterator
{
	typedef list_node<T> Node;
	Node* _node;

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

	//返回const int& ,不能改值
	const T& operator*() const
	{
		return _node->_data;
	}

	const T* operator->() const
	{
		return &_node->_data; // 返回数据的const指针,只读成员
	}

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

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

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

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

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

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

可以看到上面两份代码几乎一模一样,我们可以运用模板类,通过设置模板参数,区分普通迭代器和const迭代器,由编译器自动生成两种的函数,完成各自的功能

通用迭代器设计(兼容普通迭代器和 const 迭代器)

通过 Ref 和 Ptr 两个模板参数,我们假设用Ref代替T&/const T&,用Ptr代替T*/const T*我们只需要写一份

_list_iterator,再通过不同的参数实例化,就能同时得到两种迭代器

为了方便理解,先展示具体类型的实例化

cpp 复制代码
//普通迭代器
_list_iterator<int, int&, int*>;

//const迭代器
_list_iterator<int, const int&, const int*>;

现在我们来逐步拆解迭代器代码,接下来讲的东西会很绕,让我们再来理一遍逻辑。

  1. 首先要明确迭代器本质是包装了节点指针(_node)的对象 ,不管是解引用(*)、箭头访问(->),还是前后移动(++/- -),所有操作的底层都是在操控这个节点指针------我们之所以要封装这个对象,就是为了屏蔽链表节点的底层细节(比如不用手动写 _node->_next),提供一套统一、简单的遍历访问接口。
  2. 其次要区分"迭代器类"和"实例化的迭代器"
    我们现在创建的迭代器只是一个类,并没有实例化,类相当于建房图纸一样规划有多少个房间,房间大小功能等,所以我们构建迭代器类的时候,也只是在规划它要实现怎样的功能,只有当我们在list容器中实例化它(比如通过 typedef 确定普通/const迭代器的具体类型),这份"图纸"才会变成"能住人的房子",迭代器的读写权限、返回值类型等才会有具体意义

理清这两点,再看后续的模板参数、运算符重载,就不会陷入细节迷失方向

cpp 复制代码
//当前三个模板参数只是规划,只是一个图纸,等到后面实例化迭代器时
//才会替换成具体类型,真正区分处普通迭代器和const迭代器

//定义一个模板类----迭代器类
template<class T,class Ref,class Ptr>
struct _list_iterator
{
	//如果觉得这些类太陌生,可以想想我们熟悉的vector,它也是一个类
	//重命名节点结构体,不用每次都写 list_node<T>,直接写 Node
	typedef list_node<T> Node;
	//// 重命名自身类型,不用每次写 _list_iterator<T, Ref, Ptr>,直接写 Self
	typedef _list_iterator<T, Ref, Ptr> Self;



	//成员变量:节点指针
	//迭代器通过这个指针,找到对应的链表节点,进而访问节点里面的数据(_data,_prev,_next)
	Node* _node;



	//构造函数,接收节点指针,初始化迭代器
	//创建迭代器时,必须绑定一个具体的节点,(如iterator(_head->_next))
	_list_iterator(Node*node)
		:_node(node)
	{ }



	//Ref决定了返回值是普通引用(int&),还是const引用(const int&)
	//解引用操作运算符重载,返回数据的引用
	Ref operator*() 
	{
		return _node->_data;
	}

	//Ptr决定了返回值是普通地址(T*),还是const地址(const T*) 
	//箭头操作,返回数据的地址,访问结构体/类成员
	//简化结构体 / 类成员的访问,不用写 (*it).name,直接写 it->name
	Ptr operator->()
	{
		return &_node->_data;//返回_data 地址
	}

	//前置++,迭代器向后移动
	Self& operator++()
	{
		_node = _node->_next;//跳转到下一个节点
		return *this; //返回自身引用

	}

	//后置++,迭代器向后移动
	Self operator++(int)
	{
		Self tmp(*this);//保存当前迭代器的状态
		_node = _node->_next;
		return tmp;//返回移动前的临时对象,值返回
	}

	//前置--,迭代器向前移动
	Self& operator--()
	{
		_node = _node->_prev;//跳转到前一个节点
		return *this;
	}

	//后置--,迭代器向前移动
	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;  //比较两个迭代器的_node 是否指向同一个节点
	}
};

用Self,Self&做返回值的函数,允许所有迭代器使用

用Ref和Ptr,做返回值的函数,根据实例化的结果自动生成对应的有无const修饰的函数
没理解也没关系,我们只需要记住迭代器现在可以像指针一样++/- -/*/->/使用就好了

3、 list类的实现

list 类封装了链表的核心操作(初始化、拷贝构造、赋值、插入、删除等),内部持有哨兵节点指针_head和节点个数_size

哨兵节点(_head)是一个 "不存储有效数据" 的节点,它让空链表和非空链表的操作逻辑统一(比如push_back都是 "在哨兵前插入",pop_back都是 "删除哨兵前一个节点"),非常方便简洁

1)list类的私有成员

cpp 复制代码
private:
	Node* _head;//指向哨兵节点的指针,链表的核心控制指针
size_t _size = 0;//链表中有效元素的个数,不包含哨兵节点
  1. _head:是整个双向循环链表的 "总开关",通过它可以找到所有有效节点(_head->_next 是第一个有效节点,_head->_prev 是最后一个有效节点)
  2. _size:记录有效元素个数

2)迭代器类型的定义(在list类中)

通过模板参数实例化,分别得到普通迭代器和 const 迭代器

之前的 _list_iterator<T, Ref, Ptr> 是 "通用模板"(图纸),三个参数都是占位符。现在通过 typedef

给模板传了 "具体参数",直接生成两个 "专用迭代器类型"

cpp 复制代码
template<class T>
class list
{
public:

	typedef list_node<T> Node;
	//重命名变成我们熟知的普通迭代器和const迭代器

	//普通迭代器:Ref=T&,Ptr=T*
	typedef _list_iterator<T, T&, T*>iterator; 

	// const迭代器:Ref=const T&,Ptr=const T*
	typedef _list_iterator<T, const T&, const T*> const_iterator;


};

迭代器核心接口(begin/end) list 是双向循环链表,通常用 "哨兵节点"(_head)简化边界处理

• begin():指向第一个有效节点(_head->_next)。

• end():指向哨兵节点(_head),作为尾后迭代器(不存储数据)

cpp 复制代码
iterator begin()
{
	return iterator(_head->_next); //用第一有效节点初始化迭代器
}

iterator end()
{
	return iterator(_head); //用哨兵节点初始化迭代器
}

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

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

3)初始化与构造函数

cpp 复制代码
public:
	//空链表初始化
	void empty_init()
	{
		_head = new Node; //创建哨兵节点(默认构造,数据为T())
		_head->_next = _head;//后继指向自身
		_head->_prev = _head;//前驱指向自身
	}

	//无参构造:直接调用empty_init创建空链表
	list()
	{
		empty_init();
	}

	//拷贝构造(lt2(lt1))深拷贝传入链表的所有节点
	list(const list<T>& lt)
	{
		empty_init();//先初始化自身为空头链表
		//遍历lt,将每一个元素插入到当前链表尾部
		for (const auto& e : lt)
		{
			push_back(e);
		}
		_size = lt._size;//同步原链表的大小
	}


	//使用场景list<int> l = {1,2,3,4};
	//初始化列表构造(C++11)
	list(initializer_list<T> il)
	{
		empty_init();
		// 遍历初始化列表中的所有元素,逐个尾插到当前链表
		for (const auto& e : il)
		{
			push_back(e);
		}

4) 赋值运算符重载

采用拷贝交换法,简洁高效且天然支持自赋值

cpp 复制代码
//交换函数:交换两个链表的哨兵节点和大小
void swap(list<T>& lt)
{
	std::swap(_head, lt._head);
	std::swap(_size, lt._size);
}
	
//赋值运算符(lt1=lt3)
list<T>& operator=(list<T> lt)//传值参数,自动拷贝一份lt3
{
	// 交换当前对象和lt,并且函数返回lt自动析构
	swap(lt);
	return *this;
}

为什么交换两个链表的哨兵节点和大小呢?

因为 list 的所有有效节点都通过 _head 关联,交换 _head 就相当于交换了整个链表的所有节点(不用逐个交换节点,O (1) 时间复杂度,极高效)

5)析构函数

先清空所有有效节点,再删除哨兵节点

cpp 复制代码
//析构函数
~list()
{
	clear();//清空有效节点
	delete _head;//删除哨兵节点
	_head = nullptr;//避免野指针
}

//清空所有有效节点,保留哨兵节点
void clear()
{
	iterator it = begin();
	while (it != end())
	{
		it = erase(it);//用 erase 的返回值更新 it,避免迭代器失效
	}
	_size = 0;
}

6)插入与删除操作

insert

在迭代器pos指向的节点前插入新节点

cpp 复制代码
iterator insert(iterator pos, const T& val)
{

	//确保pos指向的节点有效
	assert(pos._node != nullptr);

	//取出pos封装的节点指针
	Node* cur = pos._node;
	//创建新节点
	Node* newnode = new Node(val);
	//找到cur的前驱节点
	Node* prev = cur->_prev;

	// 调整指针关系:prev -> newnode -> cur
	prev->_next = newnode;
	newnode->_next = cur;
	cur->_prev = newnode;
	newnode->_prev = prev;

	++_size;//节点+1
	//返回指向新节点的迭代器
	return iterator(newnode);
}
erase

删除迭代器pos指向的节点,返回下一个节点的迭代器,避免迭代器失效

cpp 复制代码
//erase
iterator erase(iterator pos)
{
	//不能删除哨兵节点
	assert(pos != end());

	//取出 pos 指向的节点
	Node* cur = pos._node;

	// 保存前后节点,避免删除后找不到
	Node* prev = cur->_prev;
	Node* next = cur->_next;

	// 调整指针关系:prev -> next,跳过cur
	prev->_next = next;
	next->_prev = prev;
	delete cur;//释放cur节点内存

	--_size;//节点个数-1
	return iterator(next);//返回下一个节点的迭代器
}

7)其他常用接口

push_back/push_front
pop_back/pop_front
empty()/size()

基于 insert 和 erase 封装常用操作

cpp 复制代码
//尾插:复用 insert,在哨兵节点前插入
void push_back(const T& x)
{
	insert(end(), x);
}

//头插:复用 insert,在第一个有效节点前插入
void push_front(const T& x)
{
	insert(begin(), x);
}

//尾删(--end()指向最后一个有效节点)
void pop_back()
{
	erase(--end());
}

//头删,复用 erase,删除第一个有效节点(begin())
void pop_front()
{
	erase(begin());
}

// 判断链表是否为空(有效节点个数为 0)
bool empty() const
{
	return _size == 0;
}

// 返回节点个数
size_t size() const
{
	return _size;
}

4、测试

其实应该写一段代码,就检测一段代码的正确性,为了方便阅读,我将测试用例都写在一起了

cpp 复制代码
#include"list.h"

//初始化列表构造,拷贝构造,赋值运算符、尾插尾删,头插头删
void test1()
{
	LI::list<int> l1 = { 1,2,3,4,5 };
	for (auto e : l1)
	{
		cout << e << " ";
	}
	cout<<"size:"<<l1.size() << endl;//输出:1 2 3 4 5 size:5
	
	LI::list<int> l2(l1);
	l2.push_back(8);
	l2.pop_front();
	for (auto e : l2) cout << e << " ";
	cout << "size:" << l2.size() << endl;//输出:2 3 4 5 8 size:5
	
	LI::list<int> l3;
	l3 = l1;
	l3.push_front(8);
	l3.pop_back();
	for(auto e:l3) cout << e << " "; //输出8 1 2 3 4
	cout << endl;
	
}

//const迭代器遍历
void test2()
{
	const LI::list<int> l4 = { 1,2,3,4,5 };
	for (LI::list<int>::const_iterator it = l4.begin();it!=l4.end();++it)
	{
		cout << *it << " ";//输出:1 2 3 4 5
	}
	cout << endl;
}

//插入,删除,清空
void test3()
{
	LI::list<int> l5;
	l5.push_back(1);
	l5.push_back(3);
	auto it = l5.begin();
	++it;
	l5.insert(it, 2);//// 在1和3之间插入2
	for (auto it : l5)
	{
		cout << it << " ";//输出1 2 3
	}
	cout << endl;
	l5.erase(l5.begin()++);
	for (auto it : l5)
	{
		cout << it << " ";//输出2 3
	}
	cout << endl;


	l5.clear();
	//输出清空后empty: yes
	cout << "清空后empty: " << (l5.empty() ? "yes" : "no") << endl; 

}
int main()
{
	//test1();
	//test2();
	test3();
}

5、完整代码

list.h

cpp 复制代码
#pragma once
#include<iostream>
using namespace std;
#include<assert.h>

//创建LI命名空间,让自己模拟实现的list与std::list隔离
namespace LI
{
	//创建类模板,接收不同类型的数据(内置类型、自定义类型)
	//用struct结构体,因为struct默认成员是公有的,方便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)
		{
		}
	};


	//当前三个模板参数只是规划,只是一个图纸,等到后面实例化迭代器时
	//才会替换成具体类型,真正区分处普通迭代器和const迭代器

	//定义一个模板类----迭代器类
	template<class T, class Ref, class Ptr>
	struct _list_iterator
	{
		//如果觉得这些类太陌生,可以想想我们熟悉的vector,它也是一个类
		//重命名节点结构体,不用每次都写 list_node<T>,直接写 Node
		typedef list_node<T> Node;
		//// 重命名自身类型,不用每次写 _list_iterator<T, Ref, Ptr>,直接写 Self
		typedef _list_iterator<T, Ref, Ptr> Self;



		//成员变量:节点指针
		//迭代器通过这个指针,找到对应的链表节点,进而访问节点里面的数据(_data,_prev,_next)
		Node* _node;



		//构造函数,接收节点指针,初始化迭代器
		//创建迭代器时,必须绑定一个具体的节点,(如iterator(_head->_next))
		_list_iterator(Node* node)
			:_node(node)
		{
		}



		//Ref决定了返回值是普通引用(int&),还是const引用(const int&)
		//解引用操作,返回数据的引用
		Ref operator*() const
		{
			return _node->_data;
		}

		//Ptr决定了返回值是普通地址(T*),还是const地址(const T*) 
		//箭头操作,返回数据的地址,访问结构体/类成员
		//简化结构体 / 类成员的访问,不用写 (*it).name,直接写 it->name
		Ptr operator->() const
		{
			return &_node->_data;//返回_data 地址
		}

		//前置++,迭代器向后移动
		Self& operator++()
		{
			_node = _node->_next;//跳转到下一个节点
			return *this; //返回自身引用

		}

		//后置++,迭代器向后移动
		Self operator++(int)
		{
			Self tmp(*this);//保存当前迭代器的状态
			_node = _node->_next;
			return tmp;//返回移动前的临时对象,值返回
		}

		//前置--,迭代器向前移动
		Self& operator--()
		{
			_node = _node->_prev;//跳转到前一个节点
			return *this;
		}

		//后置--,迭代器向前移动
		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;  //比较两个迭代器的_node 是否指向同一个节点
		}
	};

	template<class T>
	class list
	{
	public:

		typedef list_node<T> Node;
		//重命名变成我们熟知的普通迭代器和const迭代器

		//普通迭代器:Ref=T&,Ptr=T*
		typedef _list_iterator<T, T&, T*>iterator;

		// const迭代器:Ref=const T&,Ptr=const T*
		typedef _list_iterator<T, const T&, const T*> const_iterator;



		iterator begin()
		{
			return iterator(_head->_next); //用第一有效节点初始化迭代器
		}

		iterator end()
		{
			return iterator(_head); //用哨兵节点初始化迭代器
		}

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

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




	private:
		Node* _head;//指向哨兵节点的指针,链表的核心控制指针
		size_t _size = 0;//链表中有效元素的个数,不包含哨兵节点


	public:
		//空链表初始化
		void empty_init()
		{
			_head = new Node; //创建哨兵节点(默认构造,数据为T())
			_head->_next = _head;//后继指向自身
			_head->_prev = _head;//前驱指向自身
		}

		//无参构造:直接调用empty_init创建空链表
		list()
		{
			empty_init();
		}

		//拷贝构造(lt2(lt1))深拷贝传入链表的所有节点
		list(const list<T>& lt)
		{
			empty_init();//先初始化自身为空头链表
			//遍历lt,将每一个元素插入到当前链表尾部
			for (const auto& e : lt)
			{
				push_back(e);
			}
			_size = lt._size;//同步原链表的大小
		}


		//使用场景list<int> l = {1,2,3,4};
		//初始化列表构造(C++11)
		list(initializer_list<T> il)
		{
			empty_init();
			// 遍历初始化列表中的所有元素,逐个尾插到当前链表
			for (const auto& e : il)
			{
				push_back(e);
			}

			//直接复用初始化列表的大小
			_size = il.size();
		}

		//交换函数:交换两个链表的哨兵节点和大小
		void swap(list<T>& lt)
		{
			std::swap(_head, lt._head);
			std::swap(_size, lt._size);
		}

		//赋值运算符(lt1=lt3)
		list<T>& operator=(list<T> lt)//传值参数,自动拷贝一份lt3
		{
			// 交换当前对象和lt,并且函数返回lt自动析构
			swap(lt);
			return *this;
		}


		//析构函数
		~list()
		{
			clear();//清空有效节点
			delete _head;//删除哨兵节点
			_head = nullptr;//避免野指针
		}

		//清空所有有效节点,保留哨兵节点
		void clear()
		{
			iterator it = begin();
			while (it != end())
			{
				it = erase(it);//用 erase 的返回值更新 it,避免迭代器失效
			}
			_size = 0;
		}

		iterator insert(iterator pos, const T& val)
		{

			//确保pos指向的节点有效
			assert(pos._node != nullptr);

			//取出pos封装的节点指针
			Node* cur = pos._node;
			//创建新节点
			Node* newnode = new Node(val);
			//找到cur的前驱节点
			Node* prev = cur->_prev;

			// 调整指针关系:prev -> newnode -> cur
			prev->_next = newnode;
			newnode->_next = cur;
			cur->_prev = newnode;
			newnode->_prev = prev;

			++_size;//节点+1
			//返回指向新节点的迭代器
			return iterator(newnode);
		}



		//erase
		iterator erase(iterator pos)
		{
			//不能删除哨兵节点
			assert(pos != end());

			//取出 pos 指向的节点
			Node* cur = pos._node;

			// 保存前后节点,避免删除后找不到
			Node* prev = cur->_prev;
			Node* next = cur->_next;

			// 调整指针关系:prev -> next,跳过cur
			prev->_next = next;
			next->_prev = prev;
			delete cur;//释放cur节点内存

			--_size;//节点个数-1
			return iterator(next);//返回下一个节点的迭代器
		}

		//尾插:复用 insert,在哨兵节点前插入
		void push_back(const T& x)
		{
			insert(end(), x);
		}

		//头插:复用 insert,在第一个有效节点前插入
		void push_front(const T& x)
		{
			insert(begin(), x);
		}

		//尾删(--end()指向最后一个有效节点)
		void pop_back()
		{
			erase(--end());
		}

		//头删,复用 erase,删除第一个有效节点(begin())
		void pop_front()
		{
			erase(begin());
		}

		// 判断链表是否为空(有效节点个数为 0)
		bool empty() const
		{
			return _size == 0;
		}

		// 返回节点个数
		size_t size() const
		{
			return _size;
		}
	};




}
相关推荐
Justinyh3 小时前
1、CUDA 编程基础
c++·人工智能
猿来是你_L3 小时前
C# Dictionary 转换成 List
windows·c#·list
悟能不能悟3 小时前
java List怎么转换为Vector
java·windows·list
white-persist3 小时前
差异功能定位解析:C语言与C++(区别在哪里?)
java·c语言·开发语言·网络·c++·安全·信息可视化
ShineWinsu4 小时前
对于数据结构:链式二叉树的超详细保姆级解析—中
数据结构·c++·算法·面试·二叉树·校招·递归
liu****4 小时前
20.传输层协议TCP
服务器·网络·数据结构·c++·网络协议·tcp/ip·udp
沐怡旸4 小时前
【穿越Effective C++】条款20:宁以pass-by-reference-to-const替换pass-by-value——参数传递的效率与语义
c++·面试
八个程序员4 小时前
c++音乐——《两只老虎》
c++·游戏
..空空的人5 小时前
C++基于websocket的多用户网页五子棋 ---- 模块介绍1
开发语言·c++·websocket