【 C++ 】list的模拟实现

本文详细介绍了C++中如何模拟实现一个双向循环链表容器类list,包括结点类、迭代器类和list类的接口实现。内容涵盖了结点的构造、迭代器的运算符重载以及list的插入、删除、迭代器相关函数等核心功能。通过源码分析,展示了list类如何提供类似vector的操作体验。


目录

1、list三个基本函数类

2、结点类接口实现

        基本框架

        构造函数

3、迭代器类接口实现

        3.1、正向迭代器

                基本框架

                默认成员函数

                构造函数

                * 运算符重载

                ->运算符重载

                ++运算符重载

                --运算符重载

                !=运算符重载

                ==运算符重载

        3.2、反向迭代器

4、list类接口实现

        基本框架

        4.1、默认成员函数

                构造函数

                析构函数

                拷贝构造函数

                赋值运算符重载函数

        4.2、迭代器相关函数

                begin和end

                rbegin和rend

        4.3、增加的相关函数

                insert

                push_back尾插

                push_front头插

        4.4、删除的相关函数

                erase

                pop_back尾删

                pop_front头删

        4.5、其它函数

                clear清除

                empty_init空初始化

                swap交换

                size求数据个数

5、源码链接


1、list三个基本函数类

前面list的学习中我们得知,list其实就是一个带头双向循环链表:

现在要模拟实现list,要实现下列三个类:

  1. 模拟实现结点类
  2. 模拟实现迭代器的类
  3. 模拟list主要功能的类

这三个类的实现层层递进,就好比如我整个核心list的类的模拟实现其基本功能(增加删除……)要建立在迭代器类和结点类均已实现好的情况下才得以完成。


2、结点类接口实现

因为list的本质为带头双向循环链表,所以其每个结点都要确保有下列成员:

  1. 前驱指针
  2. 后继指针
  3. data值存放数据

而结点类的内部只需要实现一个构造函数即可。

基本框架

//1、结点类的模拟实现
template<class T>
struct list_node//因为成员函数都是公有,就不设计成class了,直接struct
{
	list_node<T>* _next;//后继指针
	list_node<T>* _prev;//前驱指针
	T _data;//记录数据
};

构造函数

构造函数这里是用来对其成员变量的一个初始化。

//构造函数
list_node(const T& val = T())//给一个缺省值T()
	:_next(nullptr)
	, _prev(nullptr)
	, _data(val)
{}

3、迭代器类接口实现

这里强调下,因为list的特殊性,其本质是带头双向循环链表,对于链表,我们已然得知其内存空间是不连续的,是通过结点的指针顺次链接,我们不能像先前的string和vector一样直接解引用去访问其数据,结点的指针解引用还是结点,结点指针++还是结点指针,归根结底在于list物理空间不连续。而string和vector的物理空间是连续的,所以这俩不需要实现迭代器类,可以直接使用。

为了能让list像vector一样去解引用,++访问到下一个数据,我们需要单独写一个迭代器类的接口实现,在其内部进行封装补齐相应的功能,而这就要借助运算符重载来完成

  • 注意:

迭代器又分为正向迭代器和反向迭代器

3.1、正向迭代器

基本框架

//模拟实现迭代器类
template<class T, class Ref, class Ptr>
struct __list_iterator//因为成员函数都是公有,就不设计成class了,直接struct
{
	typedef list_node<T> Node;
	typedef __list_iterator<T, Ref, Ptr> self;//只要用自己的类型,就对其typedef成self,方便后续使用
	Node* _node;
};
  • 注意:

这里我迭代器类的模板参数里面包含了3个参数:

template<class T, class Ref, class Ptr>

而后文list类的模拟实现中,我对迭代器进行了两种typedef:

typedef __list_iterator<T, T&, T*> iterator;//普通迭代器
typedef __list_iterator<T, const T&, const T*> const_iterator;//const迭代器

根据这里的对应关系:Ref对应的是&引用类型,Ptr对应的是*指针类型,这里如果我是普通对象传过来的迭代器,生成对应的普通迭代器,如果是const对象传递过来的迭代器,会生成对应的const迭代器。

这样做的原因在于避免单独写一个支持不能修改迭代器指向结点数据的类而造成的复用性差。

默认成员函数

这里的默认成员函数我们只需要写构造函数。

  • 析构函数 -- 结点不属于迭代器,不需要迭代器释放
  • 拷贝构造 -- 默认浅拷贝即可
  • 赋值重载 -- 默认浅拷贝即可

构造函数

这里我们通过结点的指针即可完成构造。通过结点指针构造一个迭代器。

//构造函数
__list_iterator(Node* node)//通过结点指针构造一个迭代器
	:_node(node)
{}

* 运算符重载

*解引用的目的是为了获取结点里的_data数据,因此我们直接return返回结点指向的_data即可。

//*运算符重载
Ref operator*()//结点出了作用域还在,用引用返回
{
	return _node->_data;//返回结点指向的数据
}

->运算符重载

假设出现这样的场景,我链表存储的不是内置类型,而是自定义类型,如下:

struct AA
{
	AA(int a1 = 0, int a2 = 0)
		:_a1(a1)
		, _a2(a2)
	{}
	int _a1;
	int _a2;
};
void test()
{
	cpp::list<AA> lt;
	lt.push_back(AA(1, 1));
	lt.push_back(AA(2, 2));
	lt.push_back(AA(3, 3));
	lt.push_back(AA(4, 4));
}

对于内置类型和自定义类型成员的指针,其访问方式都是不同的:

int*  *it
AA*   (*it). 或者 it->

而这里我们应该重载一个->运算符。以便于访问自定义类型成员的指针的数据。

//->运算符重载
Ptr operator->()
{
    return &(operator*());//返回结点指针所指结点的数据的地址
    //或者return &_node->_data;
}

实现了->运算符重载后,我们执行it->_a1,编译器将其转换成it.operator->(),此时获得的是结点位置的地址即AA*,而理应还有一个箭头->才能获取数据,也就是这样:it.operator->()->_a1,也就是说第一个->是运算符重载的调用,第二个->才是原生指针的解引用

  • 总结:编译器为了可读性进行优化处理,如果不优化应该是it->->_a1,优化以后,省略了一个箭头->。

++运算符重载

++运算符分为前置++和后置++

  • 前置++

迭代器++的返回值还是迭代器,这里的++是为了让结点指向下一个结点的指针,注意前置++是要返回自增后的结点指针。

//前置++
self& operator++()//迭代器++的返回值还是迭代器
{
	_node = _node->_next;//直接让自己指向下一个结点即可实现++
	return *this;//返回自增后的结点指针
}
  • 后置++

为了区分前置++,后置++通常要加上一个参数以便区别。此外,后置++是返回自增前的结点指针。

//后置++
self operator++(int)//加参数以便于区分前置++
{
	self tmp(*this);//拷贝构造tmp
	_node = _node->_next;//直接让自己指向下一个结点即可实现++
	return tmp;//注意返回tmp,才是后置++
}

--运算符重载

--运算符也分前置--和后置--

  • 前置--

前置--是让此结点指向上一个结点,最后返回自减后的结点指针即可。

//前置--
self& operator--()
{
	_node = _node->_prev;//让_node指向上一个结点即可实现--
	return *this;
}
  • 后置--

注意传参以区分前置--,最后返回的是自减前的结点指针即可。

//后置--
self operator--(int)//记得传缺省值以区分前置--
{
	self tmp(*this);//拷贝构造tmp
	_node = _node->_prev;
	return tmp;
}

!=运算符重载

这里比较是否不等,是两个迭代器的比较,直接返回两个结点的位置是否不同即可。

//!=运算符重载
bool operator!=(const self& it)
{
	return _node != it._node;//返回两个结点指针的位置是否不同即可
}

==运算符重载

这里直接返回俩结点指针是否相同即可。

//==运算符重载
bool operator==(const self& it)
{
	return _node == it._node;//返回俩结点指针是否相同
}

3.2、反向迭代器

反向迭代器是一个适配器模式(后文会将适配器)。相较于正向迭代器,反向迭代器有下面三种主要变化:

  • 反向迭代器的++执行的操作是正向迭代器里的--,
  • 反向迭代器里的--执行的操作是正向迭代器里的++
  • 反向迭代器的*解引用和->操作指向的是前一个数据

总代码如下:

namespace cpp
{
	template<class Iterator, class Ref, class Ptr>
	struct Reverse_iterator
	{
		Iterator _it;
		typedef Reverse_iterator<Iterator, Ref, Ptr> Self;
		
		//构造函数
		Reverse_iterator(Iterator it)
			:_it(it)
		{}
		//*运算符重载
		Ref operator*()
		{
			Iterator tmp = _it;
			//返回上一个数据
			return *(--tmp);
		}
		//->运算符重载
		Ptr operator->()
		{
			//复用operator*,返回上一个数据
			return &(operator*());
		}
		//++运算符重载
		Self& operator++()
		{
			--_it;
			return *this;
		}
		//--运算符重载
		Self& operator--()
		{
			++_it;
			return *this;
		}
		//!=运算符重载
		bool operator!=(const Self& s)
		{
			return _it != s._it;//返回两个结点指针的位置是否不同即可
		}
		//==运算符重载
		bool operator==(const Self& s)
		{
			return _it == s._it;//返回俩结点指针是否相同
		}
	};
}

4、list类接口实现

此接口的核心任务是为了模拟实现list类的一些核心功能,好比如插入删除,迭代器等等。

基本框架

在list类中的唯一成员变量即自定义类型的变量,由先前的结点类构成的头结点:

/*3、模拟实现list的功能类*/
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;//const迭代器

	//反向迭代器适配支持
	typedef Reverse_iterator<iterator, T&, T*>reverse_iterator;
	typedef Reverse_iterator<const_iterator, const T&, const T*> const_reverse_iterator;//const反向迭代器
private:
	Node* _head;
};

4.1、默认成员函数

构造函数

  • 无参构造:

此目的是为了对哨兵位的头结点_head进行初始化:

//构造函数
list()
{
	_head = new Node();//申请一个头结点
	_head->_next = _head;//头结点的下一个结点指向自己构成循环
	_head->_prev = _head;//头结点的上一个结点指向自己构成循环
}
  • 传迭代器区间构造:

先初始化,再利用循环对迭代器区间的元素挨个尾插即可。

//传迭代器区间构造
template <class InputIterator>
list(InputIterator first, InputIterator last)
{
	empty_init();
	while (first != last)
	{
		push_back(*first);
		first++;
	}
}
  • initializer_list:

我们可以像vector一样用一个 {} 指定元素然后对一个 list 对象进行初始化。它的底层原理是用了一个叫 initializer_list 的类来实现的。注意同样我们也需要先进行empty_init空初始化。

list(initializer_list<T> lt)
{
	empty_init();
	for (auto& e : lt)
		push_back(e);
}

使用效果如下:

xzy::list<int> lt = { 10, 20, 30, 40, 50 };

析构函数

这里可以先复用clear函数把除了头结点的所有结点给删除掉,最后delete头结点即可。

//析构函数
~list()
{
	clear();
	delete _head;//删去哨兵位头结点
	_head = nullptr;
}

拷贝构造函数

假设要用lt1拷贝构造lt2。

  • 传统写法:

首先复用empty_init对头结点进行初始化,接着遍历lt1的元素,在遍历的过程中尾插将lt1的元素尾插到lt2上即可。这里直接利用push_back自动开辟空间完成深拷贝。

//传统写法
list(const list<T>& lt)
{
	//先初始化lt2
	empty_init();
	//遍历lt1,把lt1的元素push_back到lt2里头
	for (auto e : lt)
	{
		push_back(e);//自动开辟新空间,完成深拷贝
	}	
}
  • 现代写法:

这里先初始化lt2自己,再把lt1引用传参传给lt,传lt的迭代器区间构造tmp,复用swap交换头结点指针即可完成深拷贝的现代写法。

//现代写法
list(const list<T>& lt)
{
	//初始化自己
	empty_init();
	list<T>tmp(lt.begin(), lt.end());//借用迭代器区间去构造tmp
	//交换头结点指针即可完成深拷贝现代写法
	swap(tmp);
}

赋值运算符重载函数

  • 假设要把lt1赋值给lt2。

这里直接给出现代写法。注意这里传值传参把lt1传给lt自定义类型传值传参调用拷贝构造,拷贝构造完成的是深拷贝生成了lt,复用swap函数交换lt1与lt的头结点指针指向即可,最后返回*this。

//赋值运算符重载(现代写法)
list<T>& operator=(list<T> lt)//套用传值传参去拷贝构造完成深拷贝
{
	swap(lt);
	return *this;
}

4.2、迭代器相关函数

begin和end

  • begin的作用是返回第一个位置的结点的迭代器,而第一个结点就是哨兵位头结点的下一个结点,因此,直接return返回_head的_next即可。
  • end的作用就是返回最后一个有效数据的下一个位置的迭代器,而这里对于list指的就是头结点_head的位置。

begin和end均分为普通对象调用和const对象调用,因此要写两个版本。

  • 普通对象调用版
//begin
iterator begin()//begin返回的就是第一个有效数据,即头结点的下一个结点
{
	return iterator(_head->_next);//构造了一个匿名对象,通过调用构造函数利用头结点指向的第一个结点作为参数,来返回头结点
	//return _head->_next;  也可以这样写
}
//end
iterator end()
{
	return iterator(_head);//end返回的是最后一个结点的下一个结点,就是头结点_head
	//return _head;  也可以这样写
}
  • const对象调用版
//begin
const_iterator begin() const
{
	return const_iterator(_head->_next);
	//return _head->_next; 
}
//end
const_iterator end() const
{
	return const_iterator(_head);
	//return _head;  也可以这样写
}

rbegin和rend

rbegin就是正向迭代器里end()的位置,rend就是正向迭代器里begin()的位置。

rbegin和rend同样分为普通对象调用和const对象调用:

  • 普通对象调用:
//rbegin()
reverse_iterator rbegin()
{
	return reverse_iterator(end());
}
//rend
reverse_iterator rend()
{
	return reverse_iterator(begin());
}
  • const对象调用:
//const反向迭代器
const_reverse_iterator rbegin() const
{
	return const_reverse_iterator(end());
}
const_reverse_iterator rend() const
{
	return const_reverse_iterator(begin());
}

4.3、增加的相关函数

insert

实现insert首先创建一个新的结点存储插入的值,接着取出插入位置pos的结点为cur,记录cur的上一个结点位置prev,先链接prev和newnode,再链接newnode和cur即可。最后记得要返回新插入元素的迭代器位置。

//insert,插入pos位置之前
iterator insert(iterator pos, const T& x)
{
	Node* newnode = new Node(x);//创建新的结点
	Node* cur = pos._node; //迭代器pos处的结点指针
	Node* prev = cur->_prev;
	//prev newnode cur
	//链接prev和newnode
	prev->_next = newnode;
	newnode->_prev = prev;
	//链接newnode和cur
	newnode->_next = cur;
	cur->_prev = newnode;
	//返回新插入元素的迭代器位置
	return iterator(newnode);
}
  • 补充:list的insert不存在野指针和意义变了的迭代器失效问题。

push_back尾插

  • 法一:

首先,创建一个新结点用来存储尾插的值,接着找到尾结点。将尾结点和新结点前后链接构成循环,再将头结点和新结点前后链接构成循环即可。

//尾插
void push_back(const T& x)
{
	Node* tail = _head->_prev;//找尾
	Node* newnode = new Node(x);//创建一个新的结点
	//_head  tail  newnode
	//使tail和newnode构成循环
	tail->_next = newnode;
	newnode->_prev = tail;
	//使newnode和头结点_head构成循环
	newnode->_next = _head;
	_head->_prev = newnode;

}
  • 法二:

这里也可以复用insert函数,当insert中的pos位置为哨兵位头结点的位置时,实现的就是尾插,因为insert插入是在pos位置前插入,而pos位哨兵位头结点时,在其前一个位置(尾部)插入就是实现了尾插。

//尾插
void push_back(const T& x)
{
	//法二:复用insert
	insert(end(), x);
}

push_front头插

直接复用insert函数,当pos位置为begin()时,获得的pos就是第一个有效结点数据,即可满足头插。

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

4.4、删除的相关函数

erase

erase删除的是pos位置的结点,首先取出pos位置的结点为cur,记录cur上一个结点的位置为prev,再记录cur下一个结点的位置为next,链接prev和next,最后delete释放cur的结点指针即可。最后记得返回删除元素后一个元素的迭代器位置。

//erase
iterator erase(iterator pos)
{
	assert(pos != end());
	Node* cur = pos._node;
	Node* prev = cur->_prev;
	Node* next = cur->_next;
	//prev cur next
	//链接prev和next
	prev->_next = next;
	next->_prev = prev;
	//delete要删去的结点
	delete cur;
	//返回删除元素后一个元素的迭代器位置
	//return next;
	return iterator(next);
}

pop_back尾删

直接复用erase即可,当pos位置为--end()时,pos就是最后一个结点的位置,实现的就是尾删。

//尾删
void pop_back()
{
	erase(--end());
}

pop_front头删

直接复用erase即可,当pos位置为begin()时,pos就是第一个有效数据,实现的就是头删。

//头删
void pop_front()
{
	erase(begin());
}

4.5、其它函数

clear清除

clear的作用是清除除了头结点外的所有结点,这里可以复用erase并通过遍历的方式一个一个删除。

//clear清除
void clear()
{
	//复用erase
	iterator it = begin();
	while (it != end())
	{
		it = erase(it);//用it接收删除后的下一个结点的位置
	}
}

empty_init空初始化

此函数的作用把哨兵位的头结点开出来,再对齐初始化。该函数是库里的。

//空初始化  对头结点进行初始化
void empty_init()
{
	_head = new Node();
	_head->_next = _head;
	_head->_prev = _head;
}

swap交换

对于链表的swap,直接交换头结点指针的指向即可完成。直接复用库函数的swap即可。

//swap交换函数
void swap(list<T>& lt)
{
	std::swap(_head, lt._head);//交换头指针
}

size求数据个数

正常要求得链表list中数据的个数,我们需要遍历链表,但为了方便,我们可以在list类里额外定义一个_size成员变量,当我需要插入数据时只需要对insert函数接口的_size++,因为我push_back和push_front函数全都复用了insert,删除数据时只需要对erase函数接口的_size--,因为我pop_back和pop_front函数全都复用了erase,并提供一个size()接口直接返回_size即可。

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;
    
//...

	size_t size() const
	{
		return _size;
	}

	void insert(iterator pos, const T& x)
	{
		//...
		++_size;
	}
	iterator erase(iterator pos)
	{
		//...
		--_size;
		return iterator(nextNode);
	}
	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());
	}
private:
	Node* _head;
	size_t _size = 0;
};

5、源码链接

链接直达:list的模拟实现完整版

<think>首先,用户的问题是关于C++ list数据结构的模拟实现方法。用户意图是了解如何使用C++模拟实现list数据结构,这基于系统指令中的引用部分。 系统指令要求: - 所有行内数学表达式使用$...$格式。 - 独立公式使用$$...$$格式并单独成段。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 回答中引用的段落末尾自然地添加引用标识。 - 回答结构清晰,帮助用户逐步解决问题。 参考的引用: - 引用[1]:提到了list模拟实现的框架,包括节点结构体模板、迭代器结构体模板、list结构体模板和测试函数。 - 引用[2]:详细描述了节点结构体的实现,包括数据域和指针域。 - 引用[3]:解释了__list_iterator结构体,用于封装迭代器,包括指针和运算符重载。 - 引用[4]:讨论了迭代器的解引用操作符重载。 我需要构建一个清晰的回答,逐步解释如何模拟实现C++list数据结构。listC++标准库中是一个双向链表。 回答结构: 1. **介绍**:简要说明list是什么,以及模拟实现的目的。 2. **核心组成部分**: - 节点结构体(ListNode):存储数据和前后指针。 - 迭代器类(iterator):封装节点指针,提供迭代功能。 - list类(list):管理链表,包括头尾指针和大小。 3. **详细实现**: - 节点结构体的定义。 - 迭代器类的定义,包括运算符重载(如++、--、*、->等)。 - list类的定义,包括构造函数、析构函数、插入、删除等操作。 4. **代码示例**:提供关键部分的代码片段,使用代码块格式。 5. **总结**:简要回顾实现要点。 6. **相关问题**:生成3-5个相关问题。 使用中文回答。 在引用部分末尾添加引用标识:[^1]、[^2]等。 生成相关问题: - 基于用户可能的兴趣,如性能分析、优化、应用场景等。 现在,组织内容: 1. **介绍**: - C++中的list是一个双向链表容器。模拟实现可以帮助理解其内部机制。 - 模拟实现主要包括三个部分:节点结构体、迭代器类和list类本身。 2. **节点结构体(ListNode)**: - 每个节点包含三个部分:数据域(_data)、指向前一个节点的指针(_prev)、指向后一个节点的指针(_next)。 - 使用模板以支持泛型。 - 示例代码: ```cpp template<class T> struct ListNode { ListNode<T>* _prev; ListNode<T>* _next; T _data; ListNode(const T& data = T()) : _prev(nullptr), _next(nullptr), _data(data) {} }; ``` 引用[2]描述了类似的实现。 3. **迭代器类(iterator)**: - 迭代器用于遍历链表,封装节点指针。 - 重载运算符:++(前置和后置)、--(前置和后置)、*(解引用)、->(成员访问)、==、!=等。 - 示例代码: ```cpp template<class T> struct __list_iterator { ListNode<T>* _node; __list_iterator(ListNode<T>* node) : _node(node) {} // 前置++ __list_iterator& operator++() { _node = _node->_next; return *this; } // 后置++ __list_iterator operator++(int) { __list_iterator tmp = *this; _node = _node->_next; return tmp; } // 前置-- __list_iterator& operator--() { _node = _node->_prev; return *this; } // 后置-- __list_iterator operator--(int) { __list_iterator tmp = *this; _node = _node->_prev; return tmp; } T& operator*() { return _node->_data; } T* operator->() { return &_node->_data; } bool operator!=(const __list_iterator& other) const { return _node != other._node; } bool operator==(const __list_iterator& other) const { return _node == other._node; } }; ``` 引用[3]和[4]提供了相关信息。 4. **list类(list)**: - 包含头节点指针(通常是一个哨兵节点,用于简化操作)。 - 成员函数:构造函数、析构函数、push_back、push_front、insert、erase、size等。 - 示例代码: ```cpp template<class T> class list { public: typedef __list_iterator<T> iterator; list() { _head = new ListNode<T>(); _head->_next = _head; _head->_prev = _head; _size = 0; } ~list() { clear(); delete _head; } iterator begin() { return iterator(_head->_next); } iterator end() { return iterator(_head); } void push_back(const T& value) { insert(end(), value); } void push_front(const T& value) { insert(begin(), value); } iterator insert(iterator pos, const T& value) { ListNode<T>* newNode = new ListNode<T>(value); ListNode<T>* cur = pos._node; ListNode<T>* prev = cur->_prev; prev->_next = newNode; newNode->_prev = prev; newNode->_next = cur; cur->_prev = newNode; _size++; return iterator(newNode); } iterator erase(iterator pos) { if (pos == end()) return pos; ListNode<T>* cur = pos._node; ListNode<T>* prev = cur->_prev; ListNode<T>* next = cur->_next; prev->_next = next; next->_prev = prev; delete cur; _size--; return iterator(next); } size_t size() const { return _size; } void clear() { iterator it = begin(); while (it != end()) { it = erase(it); } } private: ListNode<T>* _head; // 哨兵节点 size_t _size; }; ``` 5. **总结**: - 模拟实现list涉及节点、迭代器和list类。 - 关键点是使用哨兵节点简化边界条件处理。 6. **相关问题**: - 1. 如何优化list实现的性能? - 2. list与vector在C++中的主要区别是什么? - 3. 在实际项目中,list有哪些常见应用场景? - 4. 如何处理list中的异常安全? - 5. 迭代器的失效机制在list中是如何工作的? 确保在回答中引用相关引用: - 当提到框架时,引用[1]。 - 当描述节点结构时,引用[2]。 - 当讨论迭代器时,引用[3]和[4]。 最终回答结构: - 开头介绍。 - 详细部分分步骤。 - 代码块。 - 总结。 - 相关问题。</think>### C++ list数据结构模拟实现方法 在C++中,`std::list`是一个双向链表容器,其模拟实现涉及三个核心部分:**节点结构体**、**迭代器类**和**list类**本身。下面我将逐步解释实现方法,确保内容清晰易懂。实现基于模板以支持泛型编程,并使用哨兵节点简化边界处理。关键代码使用C++语法,并遵循标准库的设计思想。 #### 1. **节点结构体(ListNode)** 节点是链表的基本单元,存储数据、前驱和后继指针。使用结构体模板定义: ```cpp template<class T> struct ListNode { ListNode<T>* _prev; // 指向前驱节点 ListNode<T>* _next; // 指向后继节点 T _data; // 存储数据 // 构造函数:初始化指针为nullptr,数据域使用默认值 ListNode(const T& data = T()) : _prev(nullptr), _next(nullptr), _data(data) {} }; ``` - **作用**:每个节点维护双向链接,支持高效插入和删除。 - **细节**:构造函数使用`T()`提供默认初始化,确保兼容各种数据类型[^2]。 #### 2. **迭代器类(__list_iterator)** 迭代器封装节点指针,提供类似指针的接口,支持遍历操作。重载运算符是关键: ```cpp template<class T> struct __list_iterator { ListNode<T>* _node; // 指向当前节点 // 构造函数:基于节点指针初始化 __list_iterator(ListNode<T>* node = nullptr) : _node(node) {} // 前置++:移动到下一个节点 __list_iterator& operator++() { _node = _node->_next; return *this; } // 后置++:返回当前迭代器后移动 __list_iterator operator++(int) { __list_iterator tmp = *this; // 拷贝当前状态 _node = _node->_next; return tmp; // 返回移动前的迭代器 } // 前置--:移动到前一个节点 __list_iterator& operator--() { _node = _node->_prev; return *this; } // 后置--:返回当前迭代器后移动 __list_iterator operator--(int) { __list_iterator tmp = *this; _node = _node->_prev; return tmp; } // 解引用:返回节点数据的引用 T& operator*() { return _node->_data; } // 箭头操作符:返回节点数据的指针 T* operator->() { return &_node->_data; } // 比较操作符:检查迭代器是否相等 bool operator==(const __list_iterator& other) const { return _node == other._node; } bool operator!=(const __list_iterator& other) const { return _node != other._node; } }; ``` - **作用**:实现类似标准迭代器的行为,包括`++`、`--`、`*`和`->`等操作。 - **细节**:后置`++`和`--`通过拷贝构造保留原状态,确保符合STL规范[^3][^4]。 #### 3. **list类(list)** list类管理整个链表,包括哨兵节点、大小维护和基本操作(如插入、删除)。使用哨兵节点(头节点)简化逻辑: ```cpp template<class T> class list { public: typedef __list_iterator<T> iterator; // 定义迭代器类型 // 构造函数:初始化哨兵节点(自环) list() { _head = new ListNode<T>(); // 创建哨兵节点 _head->_next = _head; // 初始化为自环 _head->_prev = _head; _size = 0; // 大小初始为0 } // 析构函数:释放所有节点 ~list() { clear(); // 清空链表 delete _head; // 删除哨兵节点 } // 迭代器接口 iterator begin() { return iterator(_head->_next); // 首元素是哨兵的下一个 } iterator end() { return iterator(_head); // 尾后位置是哨兵自身 } // 插入元素到指定位置前 iterator insert(iterator pos, const T& value) { ListNode<T>* newNode = new ListNode<T>(value); // 创建新节点 ListNode<T>* cur = pos._node; // 当前位置节点 ListNode<T>* prev = cur->_prev; // 前驱节点 // 链接新节点 prev->_next = newNode; newNode->_prev = prev; newNode->_next = cur; cur->_prev = newNode; _size++; // 更新大小 return iterator(newNode); // 返回新节点的迭代器 } // 删除指定位置元素 iterator erase(iterator pos) { if (pos == end()) return pos; // 不能删除哨兵 ListNode<T>* cur = pos._node; ListNode<T>* prev = cur->_prev; ListNode<T>* next = cur->_next; // 绕过当前节点 prev->_next = next; next->_prev = prev; delete cur; // 释放节点 _size--; // 更新大小 return iterator(next); // 返回下一个位置的迭代器 } // 添加元素到尾部 void push_back(const T& value) { insert(end(), value); } // 添加元素到头部 void push_front(const T& value) { insert(begin(), value); } // 清空链表 void clear() { iterator it = begin(); while (it != end()) { it = erase(it); // 逐个删除 } } // 获取链表大小 size_t size() const { return _size; } private: ListNode<T>* _head; // 哨兵节点(不存储数据) size_t _size; // 链表大小 }; ``` - **作用**:提供链表的核心功能,包括构造、析构、插入、删除和遍历。 - **细节**: - 哨兵节点`_head`形成自环,`begin()`指向第一个数据节点,`end()`指向哨兵,简化边界处理。 - `insert`和`erase`操作时间复杂度为$O(1)$,符合链表特性。 - `clear`和析构函数确保无内存泄漏[^1]。 #### 4. **测试示例** 简单测试代码: ```cpp #include <iostream> int main() { list<int> myList; myList.push_back(10); myList.push_front(20); auto it = myList.begin(); std::cout << *it << std::endl; // 输出20 it = myList.erase(it); std::cout << *it << std::endl; // 输出10 return 0; } ``` #### 总结 模拟实现C++ list的关键是: - **节点结构体**:管理数据和指针。 - **迭代器类**:封装遍历逻辑,通过运算符重载提供STL兼容接口。 - **list类**:使用哨兵节点简化操作,实现高效插入、删除和大小维护。 这种实现帮助深入理解链表底层机制,但实际开发中推荐使用标准库`std::list`以获得优化和安全性[^1][^3]。
评论 33
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

三分苦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值