【c++丨STL】list模拟实现(附源码)

🌟🌟作者主页:ephemerals__****
🌟🌟所属专栏:C++STL

目录

前言

一、节点、迭代器以及函数声明

二、list功能实现

[1. 节点](#1. 节点)

[2. 迭代器](#2. 迭代器)

迭代器的默认构造

operator*

operator->

前置++和--

后置++和--

operator!=

[3. 容器](#3. 容器)

交换两个容器的内容

迭代器接口

空链表初始化

构造函数

拷贝构造函数

赋值重载

析构函数

容量接口size

插入和删除

insert和erase

push_back、push_front、pop_back、pop_front

清空链表

三、程序全部代码

总结


前言

通过之前对list的学习,我们已经基本掌握了其底层结构以及常用接口。今天我们在此基础上,尝试模拟实现list。

与vector、string不同,由于list的底层是一个双向带头循环链表 ,所以它的实现上要更加复杂。vector和string的迭代器可以是原生指针,但是list的迭代器需要用一个类模板来实现,并且数据之间都是以节点的指针相连接,我们还需要定义其节点结构。当然,与vector一样,在接下来模拟实现list的过程中,我们也将采用类模板进行定义,并不会将声明和定义分离。

建议大家在掌握了双向带头循环链表 以及vector、string的实现之后,再来阅读本文,否则其中部分内容可能较难理解。

一、节点、迭代器以及函数声明

首先是链表节点、迭代器和我们要实现接口的声明:

cpp 复制代码
#include <iostream>
#include <cassert>
using namespace std;

//节点
template<class T>
struct list_node
{
	T _data;//数据域
	list_node<T>* _next;//指向后一个节点
	list_node<T>* _prev;//指向前一个节点

	//节点的默认构造
	list_node(const T& x = T());
};

//迭代器
template<class T, class Ref, class Ptr>
struct list_iterator
{
	typedef list_node<T> Node;
	typedef list_iterator<T, Ref, Ptr> Self;

	Node* ptr;//封装节点的指针

	//迭代器构造
	list_iterator(Node* node);

	//解引用
	Ref operator*();
	//结构成员间接访问
	Ref operator->();
	//前置++
	&Self operator++();
	//前置--
	&Self operator--();
	//后置++
	Self operator++(int);
	//后置--
	Self operator--(int);
	//不等于
	bool operator!=(const Self& it);
};

//容器
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;

	//迭代器接口
	iterator begin();
	iterator end();
	const_iterator begin() const;
	const_iterator end() const;

	//空链表的初始化
	void empty_init();

	//无参构造
	List();
	//n个val值构造
	List(size_t n, const T& val = T());
	//拷贝构造
	List(const List<T>& l);
	//赋值重载
	List<T>& operator=(List<T> l);
	//析构
	~List();

	//插入和删除
	iterator insert(iterator pos, const T& x);
	iterator erase(iterator pos);
	void push_back(const T& x);
	void push_front(const T& x);
	void pop_back();
	void pop_front();

	//清空链表
	void clear();

	//交换两个链表
	void swap(List<T> l);
private:
	Node* _head;//头节点的指针
	size_t _size;//节点个数
};

//交换两个链表--非成员函数版
template <class T>
void swap(List<T>& l1, List<T>& l2);

接下来,我们开始逐一实现这三个部分。

二、list功能实现

1. 节点

和传统的双向带头循环链表相同,节点当中有一个数据域用于存放数据 ;两个指针分别指向前驱节点后继节点。 我们需要实现节点的默认构造函数,便于容器构造节点。

代码实现:

cpp 复制代码
//节点
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)
	{}
};

可以看到,这里的节点我们也定义成了一个类模板,参数T表示的是传入的数据类型。对于构造函数,如果我们没有显式传参,则数据x就会调用其默认构造函数初始化。

2. 迭代器

迭代器也是一个类模板,我们模拟实现的迭代器具有三个模板参数数据类型TT的引用类型Ref (普通引用或const引用)、T的指针类型Ptr (普通指针或const指针)。 为什么要这样设计呢?其实这样做的目的是为了提高代码复用率。如果我们创建了一个list普通对象 和一个const对象 ,那么使用它们的迭代器时,相对应就会有普通迭代器const迭代器 。这样我们在实现迭代器时,就要将同样的接口写两遍。而当我们定义了三个模板参数后,容器内部就可以通过传参来定义对应的普通迭代器ef和const迭代器,减少了代码冗余。

cpp 复制代码
//list容器内部定义迭代器
typedef list_iterator<T, T&, T*> iterator;//普通迭代器
typedef list_iterator<T, const T&, const T*> const_iterator;//const迭代器

其次,为了简化代码,我们typedef一下节点类型以及迭代器本身类型:

cpp 复制代码
typedef list_node<T> Node;//节点
typedef list_iterator<T, Ref, Ptr> Self;//迭代器本身

除此之外,由于迭代器是将指针进行封装,所以我们要将其成员变量设置为一个指向链表节点的指针

cpp 复制代码
Node* _ptr;//封装节点的指针

接下来,我们开始实现迭代器的操作函数。

迭代器的默认构造

cpp 复制代码
//迭代器构造
list_iterator(Node* node)
    :_ptr(node)
{}

对于构造函数,我们将传入的指针赋值给成员指针即可。

operator*

cpp 复制代码
//解引用
Ref operator*()
{
	return _ptr->_data;
}

解引用用于访问指针所指对象的数据,所以函数返回迭代器指向节点的数据域。

operator->

cpp 复制代码
//结构成员间接访问
Ref operator->()
{
	return &_ptr->_data;
}

正常来讲,该函数的作用应该是用于访问指针所指向数据的成员 ,但是它并没有参数,而且返回值却是数据地址 。这是为什么呢?这其实和c++的语法规定有关。当我们使用该运算符重载时,要访问到数据的成员,原本应该这样写:迭代器 ->-> 成员。但是连续两个"->"看起来并不美观,并且难以理解,所以c++语法在这里省略了一个"->" ,仅支持 迭代器->成员迭代器.operator->()->成员的写法。所以我们在实现该重载时,要返回数据的地址,编译器就会默认根据这个地址找到指定成员(成员名写在->后即可,不传参)

前置++和--

由于链表底层结构并非连续,所以不能直接使指针++/--来改变指向,而应该使用类似链表遍历的方式。实现如下:

cpp 复制代码
//前置++
Self& operator++()
{
	_ptr = _ptr->next;
	return *this;
}
//前置--
Self& operator--()
{
	_ptr = _ptr->prev;
	return *this;
}

后置++和--

后置++/--的实现方法与前置++/--类似。这里创建一个临时迭代器来保存未移动时的指向。

cpp 复制代码
//后置++
Self operator++(int)
{
	Self tmp(*this);
	_ptr = _ptr->next;
	return tmp;
}
//后置--
Self operator--(int)
{
	Self tmp(*this);
	_ptr = _ptr->prev;
	return tmp;
}

operator!=

cpp 复制代码
//不等于
bool operator!=(const Self& it)
{
	return _ptr != it._ptr;
}

这里判断它们的成员指针是否相等即可。

3. 容器

为了简化代码,还是先typedef一下节点类型:

cpp 复制代码
typedef list_node<T> Node;

接下来,我们开始实现list容器的常用接口。

交换两个容器的内容

还是老套路,我们仅需交换它们的成员变量,就可以完成容器的交换。代码实现:

cpp 复制代码
//交换两个链表
void swap(List<T> l)
{
	std::swap(_head, l.head);//交换头节点指针
	std::swap(_size, l._size);//交换size
}
cpp 复制代码
//交换两个链表--非成员函数版
template <class T>
void swap(List<T>& l1, List<T>& l2)
{
	l1.swap(l2);//直接调用成员函数版
}

迭代器接口

首先是普通迭代器和const迭代器的定义:

cpp 复制代码
typedef list_iterator<T, T&, T*> iterator;
typedef list_iterator<T, const T&, const T*> const_iterator;

迭代器接口实现:

cpp 复制代码
//迭代器接口
iterator begin()
{
	return iterator(_head->next);
}
iterator end()
{
	return _head;
}
const_iterator begin() const
{
	return iterator(_head->next);
}
const_iterator end() const
{
	return _head;
}

这里要注意一下它们的返回值:begin的返回值是指向首元素的迭代器,由于我们的链表头部有一个哨兵节点,所以首元素是哨兵节点的下一个节点 ;end的返回值是指向尾元素的"下一个元素"的迭代器,比较抽象,这里刚好可以定义为哨兵节点

空链表初始化

该函数用于创建哨兵节点并且初始化链表的各项属性,便于后续构造函数调用。

cpp 复制代码
//空链表的初始化
void empty_init()
{
	_head = new Node();
	_head->next = _head->prev = _head;
	_size = 0;
}

构造函数

这里我们实现一下无参构造n个val值构造

cpp 复制代码
//无参构造
List()
{
	empty_init();
}
cpp 复制代码
//n个val值构造
List(size_t n, const T& val = T())
{
	empty_init();
	for (size_t i = 0; i < n; i++)
	{
		push_back(val);//循环尾插
	}
}

拷贝构造函数

拷贝构造函数的逻辑与n个val值构造相同,遍历源链表内容,循环尾插给当前链表即可。

cpp 复制代码
//拷贝构造
List(const List<T>& l)
{
	empty_init();
	for (auto& e : l)
	{
		push_back(e);
	}
}

赋值重载

这里我们仍然使用新式写法,构造新对象然后交换,完成赋值操作。 代码如下:

cpp 复制代码
//赋值重载
List<T>& operator=(List<T> l)
{
	swap(l);
	return *this;//支持连续赋值
}

析构函数

对于析构函数,我们首先释放掉所有的节点空间,然后删除哨兵节点。

cpp 复制代码
//析构
~List()
{
	clear();//调用clear函数清空链表
	delete _head;
	_head = nullptr;
}

容量接口size

容量接口size用于获取链表数据个数。我们在函数当中返回成员变量_size的值。

cpp 复制代码
//容量接口
size_t size()
{
	return _size;
}

插入和删除

和之前实现vector时的逻辑相同,我们首先实现inserterase函数,这两个函数分别用于在任意位置进行插入和删除操作。然后,其他的插入和删除函数可以直接调用它们,能确保代码的高效性和复用性。

insert和erase

insert的逻辑和我们c语言实现双向链表时大体相同(在pos位置之前插入),注意插入元素后,pos指向的不再是原本需要插入的位置,迭代器失效 ,所以函数要返回新插入的节点的迭代器

cpp 复制代码
//插入
iterator insert(iterator pos, const T& x)
{
	Node* cur = pos._ptr;
	Node* newnode = new Node(x);
	newnode->_next = cur;
	newnode->_prev = cur->_prev;
	cur->_prev->_next = newnode;
	cur->_prev = newnode;
	_size++;
	return iterator(newnode);
}

erase负责删除pos指向的节点。注意删除节点之后,该节点的迭代器会失效 ,所以函数返回指向被删除节点的后一个节点的迭代器

cpp 复制代码
iterator erase(iterator pos)
{
	assert(pos != end());//避免删除哨兵节点
	Node* del = pos._ptr;
	Node* next = del->_next;
	Node* prev = del->_prev;
	next->_prev = del->_prev;
	prev->_next = del->_next;
	delete del;
	_size--;
	return iterator(next);
}
push_back、push_front、pop_back、pop_front

尾插、头插、尾删、头删 直接调用insert和erase函数即可。注意尾删节点时,end指向的前一个位置才是尾节点

cpp 复制代码
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());
}

清空链表

清空链表的操作类似于我们c语言实现的销毁链表,这里循环调用erase函数来完成数据的清除。注意不要删除哨兵节点

cpp 复制代码
//清空链表
void clear()
{
	auto it = begin();//从首元素开始,一个个删除
	while (it != end())
	{
		it = erase(it);
	}
}

三、程序全部代码

关于list模拟实现的全部代码如下:

cpp 复制代码
#include <iostream>
#include <cassert>
using namespace std;

//节点
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)
	{}
};

//迭代器
template<class T, class Ref, class Ptr>
struct list_iterator
{
	typedef list_node<T> Node;
	typedef list_iterator<T, Ref, Ptr> Self;

	Node* _ptr;//封装节点的指针

	//迭代器构造
	list_iterator(Node* node)
		:_ptr(node)
	{}

	//解引用
	Ref operator*()
	{
		return _ptr->_data;
	}
	//结构成员间接访问
	Ref operator->()
	{
		return &_ptr->_data;
	}
	//前置++
	Self& operator++()
	{
		_ptr = _ptr->next;
		return *this;
	}
	//前置--
	Self& operator--()
	{
		_ptr = _ptr->prev;
		return *this;
	}
	//后置++
	Self operator++(int)
	{
		Self tmp(*this);
		_ptr = _ptr->next;
		return tmp;
	}
	//后置--
	Self operator--(int)
	{
		Self tmp(*this);
		_ptr = _ptr->prev;
		return tmp;
	}
	//不等于
	bool operator!=(const Self& it)
	{
		return _ptr != it._ptr;
	}
};

//容器
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;

	//迭代器接口
	iterator begin()
	{
		return iterator(_head->next);
	}
	iterator end()
	{
		return _head;
	}
	const_iterator begin() const
	{
		return iterator(_head->next);
	}
	const_iterator end() const
	{
		return _head;
	}

	//空链表的初始化
	void empty_init()
	{
		_head = new Node();
		_head->next = _head->prev = _head;
		_size = 0;
	}

	//无参构造
	List()
	{
		empty_init();
	}
	//n个val值构造
	List(size_t n, const T& val = T())
	{
		empty_init();
		for (size_t i = 0; i < n; i++)
		{
			push_back(val);//循环尾插
		}
	}
	//拷贝构造
	List(const List<T>& l)
	{
		empty_init();
		for (auto& e : l)
		{
			push_back(e);
		}
	}
	//赋值重载
	List<T>& operator=(List<T> l)
	{
		swap(l);
		return *this;//支持连续赋值
	}
	//析构
	~List()
	{
		clear();//调用clear函数清空链表
		delete _head;
		_head = nullptr;
	}

	//容量接口
	size_t size()
	{
		return _size;
	}

	//插入和删除
	iterator insert(iterator pos, const T& x)
	{
		Node* cur = pos._ptr;
		Node* newnode = new Node(x);
		newnode->_next = cur;
		newnode->_prev = cur->_prev;
		cur->_prev->_next = newnode;
		cur->_prev = newnode;
		_size++;
		return iterator(newnode);
	}
	iterator erase(iterator pos)
	{
		assert(pos != end());//避免删除哨兵节点
		Node* del = pos._ptr;
		Node* next = del->_next;
		Node* prev = del->_prev;
		next->_prev = del->_prev;
		prev->_next = del->_next;
		delete del;
		_size--;
		return iterator(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());
	}

	//清空链表
	void clear()
	{
		auto it = begin();//从首元素开始,一个个删除
		while (it != end())
		{
			it = erase(it);
		}
	}

	//交换两个链表
	void swap(List<T> l)
	{
		std::swap(_head, l.head);//交换头节点指针
		std::swap(_size, l._size);//交换size
	}
private:
	Node* _head;//头节点的指针
	size_t _size;//节点个数
};

//交换两个链表--非成员函数版
template <class T>
void swap(List<T>& l1, List<T>& l2)
{
	l1.swap(l2);//直接调用成员函数版
}

总结

本篇文章,我们在掌握list使用方法及其底层原理的基础上,模拟实现出了list容器。由于底层数据内存的非连续性,它的迭代器实现与vector、string有较大差异。之后博主会和大家一起开始stack和queue的学习。如果你觉得博主讲的还不错,就请留下一个小小的赞在走哦,感谢大家的支持❤❤❤

相关推荐
stm 学习ing3 分钟前
FPGA 第十讲 避免latch的产生
c语言·开发语言·单片机·嵌入式硬件·fpga开发·fpga
湫ccc1 小时前
《Python基础》之字符串格式化输出
开发语言·python
mqiqe2 小时前
Python MySQL通过Binlog 获取变更记录 恢复数据
开发语言·python·mysql
AttackingLin2 小时前
2024强网杯--babyheap house of apple2解法
linux·开发语言·python
Ysjt | 深2 小时前
C++多线程编程入门教程(优质版)
java·开发语言·jvm·c++
码农飞飞2 小时前
深入理解Rust的模式匹配
开发语言·后端·rust·模式匹配·解构·结构体和枚举
一个小坑货2 小时前
Rust 的简介
开发语言·后端·rust