【C++】List的模拟实现

目录

一、基础结构:

二、迭代器的运算符重载:

1、解引用、访问重载:

2、自增自减重载:

3、等于、不等于重载:

4、迭代器使用:

三、List的构造函数:

1、无参构造:

2、初始化构造:

3、赋值运算符重载、swap函数:

四:修改操作:

[1、insert 与 erase:](#1、insert 与 erase:)

2、其他插入删除操作:

[3、clear 与 析构函数:](#3、clear 与 析构函数:)

五、完整代码:


一、基础结构:

在进行链表的模拟实现时,我们可以分为三个板块:

一:定义节点:

存储对应类型(内置类型/自定义类型)的值、指向前一个节点的指针(prev)和指向后一个节点的指针(next)。

二:迭代器封装:

因为节点的物理地址不连续,所以无法直接用原生指针解引用,得要利用重载去控制迭代器的行为以获取 / 改变 链表中节点的值。

三:链表功能: 实现链表的基础功能。

cpp 复制代码
namespace yue
{
	// 第一板块: 定义节点:
	template<class T>
	struct ListNode // 因为成员变量都需要公有,所以可以直接用struct
	{
		ListNode<T>* _next;
		ListNode<T>* _prev;
		T _val;

		// 构造函数
		ListNode(const T& x = T())
			:_next(nullptr)
			,_prev(nullptr)
			,_vel(x)
		{}
	};

	// 第二板块: 迭代器封装:
	template<class T, class Ref, class Ptr> // Ref(引用类型:T&), Ptr(指针类型:T*)
	struct ListIterator
	{
		typedef ListNode<T> Node;
		typedef ListIterator<T, Ref, Ptr> Self; // Self 对应 iterator
		
		Node _ndoe; // 成员变量
		ListIterator(Node* node)
			:_node(node)
		{}
        // 迭代器无需写析构,因为节点是下面list 链表的        

		// 成员函数...
	};

	// 第三板块: 定义链表:
	template<class T>
	class list
	{
		typedef ListNode<T> Node;
	public:
		// 定义普通迭代器与const迭代器:
		typedef ListIterator<T, T&, T*> iterator;
		typedef ListIterator<T, const T&, const T*> const_iterator;

		// 构造与析构函数...

		// 迭代器...
		
		// 其他成员函数,例如插入删除等...
		
        size_t size(){
			return _size;
		}
		bool empty(){
			return _size == 0;
		}
	private:
		Node* _head; // 定义哨兵位头节点
		size_t _size; // 返回链表中有效数据的元素个数
	};
}

二、迭代器的运算符重载:

前面说过:因为节点的物理地址不连续,所以无法直接用原生指针解引用去解引用获取 / 改变 节点中的值,但是我们可以通过重载运算符来控制迭代器的行为,以此达到我们的目的。

在上面迭代器的基础结构中用的是模板是因为我们要实现普通版本和const版本两个不同版本的迭代器,普通版本是能对数据进行读写,const版本是对数据进行只读,但是链表的迭代器需要我们去重载运算符去控制,而这两个版本代码大体上是相似的,所以我们可以直接引入模板,按照所需传入对应类型参数,就能去调用对应的函数。(例如:传入const就调用const函数)

cpp 复制代码
// 在设置封装迭代器的时候使用模板: 
template<class T, class Ref, class Ptr>
struct ListIterator
{
    // ...
};

// 普通对象: 传入的是T& 和 T*
typedef ListIterator<T, T&, T*> iterator;

// const对象: 传入的是const T& 和 const T*
typedef ListIterator<T, const T&, const T*> const_iterator;

1、解引用、访问重载:

解引用 * 重载:

**重载作用:**通过解引用迭代器直接访问其当前指向的元素值:

cpp 复制代码
// 不能传值返回,传值返回返回的是拷贝,但是我们需要它能读能写
// Ref: 普通对象就是T&,const对象就是 const T&
Ref operator*()
{
	return _node->_val;
}

成员访问 -> 重载:

**重载作用:**通过迭代器访问其当前指向的元素的成员。

cpp 复制代码
Ptr operator->()
{
	return &_node->_val;
}

使用场景:

在那些需要通过迭代器直接访问元素成员的场景中,例如:list存储自定义对象时,每个对象都有其自身的方法或属性。当你需要遍历这个链表并对其中的每个对象调用方法或访问其属性。

例如:

cpp 复制代码
// 创建一个学生类
class Student
{
public:
	string _name;
	int _height;

	Student(string name, int height)
		:_name(name)
		,_height(height)
	{}

	void Print()
	{
		cout << "Name: " << _name << "Height: " << _height << endl;
	}
};

void Test()
{
	yue::list<Student> l1;
	l1.push_back(Student("zs", 175));
	l1.push_back(Student("ls", 185));
	for (yue::list<Student>::iterator it = l1.begin(); it != l1.end(); it++)
	{
		it->Print();  // 通过成员访问 -> 来直接调用学生类对象中的Print函数
	}
}

2、自增自减重载:

前置++--加引用的原因:提高效率,直接修改原始迭代器对象,而无需创建新的迭代器副本。

**后置++--不加引用的原因:**因为需要返回递增前的值,所以不能直接返回迭代器的引用,需要创建一个迭代器的临时副本,该副本表示递增前的状态,并返回这个副本。

前置++:

cpp 复制代码
Self& operator++()
{
	_node = _node->_next;
	return *this;
}

后置++:

cpp 复制代码
Self operator++(int)
{
	Self tmp(*this); // 直接构建一个临时副本,表示递增前的状态
	_node = _node->_next;
	return tmp;
}

前置--:

cpp 复制代码
Self& operator--()
{
	_node = _node->_prev;
	return *this;
}

后置--:

cpp 复制代码
Self operator--(int)
{
	Self tmp(*this);
	_node = _node->_prev;
	return tmp;
}

3、等于、不等于重载:

就是比较两个迭代器相不相等,常用于控制结束条件,例如:it = begin(); it != end(); it++;

==重载:

cpp 复制代码
bool operator!=(const Self& i)
{
	// 直接比较节点的指针相不相等即可
	return _node != i._node;
}

!=重载:

cpp 复制代码
bool operator==(const Self& i)
{
	return _node == i._node;
}

4、迭代器使用:

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

iterator begin()
{
	return _head->_next
	// iterator it(_head->_next); return it;
	// 也可以这样写: return iterator(_head->_next);
	// 其实就是匿名对象
}
iterator end()
{
	return _head;
}

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

三、List的构造函数:

1、无参构造:

无参的构造函数实现的功能就是创造一个哨兵位头节点,然后两个指针都指向自己,因此我们可以****用一个函数empty_init()去封装这个功能,以便于后面进行复用。

cpp 复制代码
// 用empty_init()函数去实现创建一个哨兵位头节点的功能
void empty_init()
{
	_head = new Node;
	_head->_next = _head;
	_head->_prev = _head;

	_size = 0;
}
list()
{
	empty_init(); // 直接复用empty_init()函数
}

2、初始化构造:

例如:list l2(l1),我们可以创建一个哨兵位头节点,然后再利用迭代器读取l1的数据尾插到l2中。

cpp 复制代码
list(const list<T>& lt)
{
	// 搞一个哨兵位头节点
	empty_init();

	for (auto& e : lt)
	{
		push_back(e); // 尾插的实现在后面: 修改操作里面
	}
}

3、赋值运算符重载、swap函数:

例如:list l2 = l1,还是老套路,自己写一个swap函数(功能为交换两个链表哨兵位节点的指向以及大小),然后传值调用,会生成一份l1的拷贝,然后再用手写的swap交换l2与l1的拷贝即可达到目的。

cpp 复制代码
// swap函数: 
void swap(list<T>& lt) 
{
	std::swap(_head, lt._head);
	std::swap(_size, lt._size);
}

// 赋值运算符重载
list<T>& operator=(list<T> lt) // 直接传值,会生成一份拷贝
{
	swap(lt);
	return *this;
}

四:修改操作:

1、insert 与 erase:

insert:创建一个节点插入,然后更改节点相互间的指向即可。

cpp 复制代码
void insert(iterator pos, const T& val)
{
	Node* cur = pos._node;
	Node* prev = cur->_prev;
	
	Node* newnode = new Node(val);

	// prev newnode cur  更改节点相互之间的指向
	prev->_next = newnode;
	newnode->_prev = prev;
	newnode->_next = cur;
	cur->_prev = newnode;

	_size++;
}

erase:更改删除节点前后节点的指向即可,需要注意的是删除节点后节点的迭代器会失效,为了避免失效,需要返回下一个节点的迭代器。

cpp 复制代码
// 解决迭代器失效问题: 要返回当前节点的下一个节点的迭代器
iterator erase(iterator pos)
{
	Node* cur = pos._node;

	// 拿到前一个和后一个
	Node* prev = cur->_prev;
	Node* next = cur->_next;

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

	delete cur;
	cur = nullptr;
	_size--;

	return iterator(next);
}

2、其他插入删除操作:

在实现了insert 与 erase函数之后,头插尾插、头删尾删就直接对其进行复用即可。

cpp 复制代码
// 尾插: 就是在end位置插入,end位置指向最后一个有效数据的下一个位置。
void push_back(const T& x)
{
	insert(end(), x);
}

// 头插: 就是在begin位置插入,begin指向第一个有效数据。
void push_front(const T& x)
{
	insert(begin(), x);
}

// 尾删: 删除end的前一个位置的数据
void pop_back()
{
	erase(--end());
}

// 头删: 删除begin位置的数据
void pop_front()
{
	erase(begin());
}

3、clear 与 析构函数:

clear与析构函数的区别就是链表clear之后头节点会被保留,因为后面还可能会用到这个链表,而析构函数是对整个链表进行销毁。而clear函数会清除哨兵位头节点以外的节点,那么析构函数就只需要做删除哨兵位节点的操作即可。

cpp 复制代码
void clear() // 不清除头节点
{
	iterator it = begin(); // iterator 在内部就不需要指定类域了
	while (it != end())
	{
		it = erase(it);// it在erese之后就失效了,返回的是下一个位置的迭代器
	}
}


~list()
{
	clear(); // 复用clear函数,剩下的工作就只需要清除哨兵位头节点即可
	delete _head;
	_head = nullptr;
}

五、完整代码:

cpp 复制代码
namespace yue
{
	// 定义节点
	template<class T>
	struct ListNode
	{
		ListNode<T>* _next;
		ListNode<T>* _prev;
		T _val;

		ListNode(const T& x = T())
			:_next(nullptr)
			, _prev(nullptr)
			, _val(x)
		{
		}
	};

	// 迭代器封装
	template<class T, class Ref, class Ptr>
	struct ListIterator
	{
		typedef ListNode<T> Node;
		typedef ListIterator<T, Ref, Ptr> Self; // iterator

		Node* _node;

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

		// T& operator*()
		Ref operator*()
		{
			return _node->_val;
		}
		// T* operator->()
		Ptr operator->()
		{
			return &_node->_val;
		}

		Self& operator++()
		{
			_node = _node->_next;
			return *this;
		}
		// 后置++
		Self operator++(int)
		{
			iterator tmp(*this);
			_node = _node->_next;
			return tmp;
		}

		Self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}
		// 后置--
		Self operator--(int)
		{
			iterator tmp(*this);
			_node = _node->_prev;
			return tmp;
		}

		bool operator!=(const Self& i)
		{
			return _node != i._node;
		}
		bool operator==(const Self& i)
		{
			return _node == i._node;
		}
	};

	// 定义链表
	template<class T>
	class list
	{
		typedef ListNode<T> Node;
	public:
		typedef ListIterator<T, T&, T*> iterator;
		typedef ListIterator<T, const T&, const T*> const_iterator;

		void empty_init()
		{
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;

			_size = 0;
		}
		list()
		{
			empty_init();
		}

		// lt2(lt1)
		list(const list<T>& lt)
		{
			// 搞一个哨兵位头节点
			empty_init();
			for (auto& e : lt)
			{
				push_back(e);
			}
		}
		// 赋值: lt2 = lt1
		list<T>& operator=(list<T> lt) 
		{
			swap(lt);
			return *this;
		}
		void swap(list<T>& lt)
		{
			std::swap(_head, lt._head);
			std::swap(_size, lt._size);
		}

		void clear() // 不清除头节点
		{
			iterator it = begin();
			while (it != end())
			{
				it = erase(it);// it在erese之后就失效了,返回的是下一个位置的迭代器
			}
		}

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

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

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

		// 头插尾插 / 头删尾删
		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 insert(iterator pos, const T& val)
		{
			Node* cur = pos._node;
			Node* newnode = new Node(val);
			Node* prev = cur->_prev;

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

		}
		// 解决迭代器失效问题: 要返回当前节点的下一个节点的迭代器
		iterator erase(iterator pos)
		{
			Node* cur = pos._node;
			// 拿到前一个和后一个
			Node* prev = cur->_prev;
			Node* next = cur->_next;

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

			delete cur;
			cur = nullptr;
            _size--;

			return iterator(next);
		}
		size_t size()
		{
			return _size;
		}
		bool empty()
		{
			return _size == 0;
		}

	private:
		Node* _head; // 带头双向循环
		size_t _size;
	};

}
相关推荐
唐诺4 小时前
几种广泛使用的 C++ 编译器
c++·编译器
冷眼看人间恩怨5 小时前
【Qt笔记】QDockWidget控件详解
c++·笔记·qt·qdockwidget
红龙创客5 小时前
某狐畅游24校招-C++开发岗笔试(单选题)
开发语言·c++
Lenyiin5 小时前
第146场双周赛:统计符合条件长度为3的子数组数目、统计异或值为给定值的路径数目、判断网格图能否被切割成块、唯一中间众数子序列 Ⅰ
c++·算法·leetcode·周赛·lenyiin
yuanbenshidiaos6 小时前
c++---------数据类型
java·jvm·c++
十年一梦实验室7 小时前
【C++】sophus : sim_details.hpp 实现了矩阵函数 W、其导数,以及其逆 (十七)
开发语言·c++·线性代数·矩阵
taoyong0017 小时前
代码随想录算法训练营第十一天-239.滑动窗口最大值
c++·算法
这是我587 小时前
C++打小怪游戏
c++·其他·游戏·visual studio·小怪·大型·怪物
fpcc7 小时前
跟我学c++中级篇——C++中的缓存利用
c++·缓存
呆萌很8 小时前
C++ 集合 list 使用
c++