list模拟实现(简单版)【C++】

目录

前言

[1. list的私有成员](#1. list的私有成员)

[2. 构造函数](#2. 构造函数)

[2.1 list构造函数](#2.1 list构造函数)

[3. list遍历](#3. list遍历)

[3.1 push_back](#3.1 push_back)

[3.2 ListIterator模拟实现](#3.2 ListIterator模拟实现)

[3.2.1 成员变量_node](#3.2.1 成员变量_node)

[3.2.2 it++ 和 ++it](#3.2.2 it++ 和 ++it)

[3.2.3 it-- 和 --it](#3.2.3 it-- 和 --it)

[3.2.4 *it](#3.2.4 *it)

[3.2.5 operator== 和 operator!=](#3.2.5 operator== 和 operator!=)

[3.2.6 operator->](#3.2.6 operator->)

[3.3 begin 和 end](#3.3 begin 和 end)

[3.4 遍历测试](#3.4 遍历测试)

[4. list 的增加和删除](#4. list 的增加和删除)

[4.1 empty 和 size](#4.1 empty 和 size)

[4.2 insert 和 erase](#4.2 insert 和 erase)

[4.2.1 insert](#4.2.1 insert)

[4.2.2 erase](#4.2.2 erase)

[4.3 push_back(plus)、push_front 和 pop_back、pop_front](#4.3 push_back(plus)、push_front 和 pop_back、pop_front)

[4.4 测试代码](#4.4 测试代码)

[5. const 类型迭代器](#5. const 类型迭代器)

[6. list析构函数 和 拷贝构造](#6. list析构函数 和 拷贝构造)

[6.1 析构函数](#6.1 析构函数)

[6.2 拷贝构造](#6.2 拷贝构造)

[6.3 operator=](#6.3 operator=)


前言

Q: 什么是list?

A: 参考标准库里面的解释std::list

list就是一个序列容器,支持常数级别的时间复杂度的插入和删除。list底层的数据结构是带头双向循环链表,这样每个数据元素就可以在内存中是非相邻的。

如果对带头双向循环链表不熟悉的话,请猛戳这里带头双向循环链表

接下来 list的模拟实现的简单版。

文件准备:

在 vs 2022中创建头文件和测试文件

然后在list.h文件中创建my_list 命名空间,在my_list来模拟实现list类,在test.cpp文件中创建主函数来调用测试接口。

因为list底层的数据结构是带头结点的双向循环链表,这里需要一个节点的数据结构。

cpp 复制代码
#pragma once
#include<iostream>
#include<assert.h>

using namespace std;

namespace my_list
{
    //双向链表结构体节点模板
	template<typename T>
	struct ListNode 
	{
		ListNode<T>* _next;
		ListNode<T>* _prev;
		T _data;

        //节点的构造函数 用来初始化节点数据
		ListNode(const T& x = T())
			:_next(nullptr)
			,_prev(nullptr)
			,_data(x)
		{}

	};
    
    //类似双向链表类模板
	template<class T>
	class list 
	{
		typedef ListNode<T> Node;
	public:

	private:
		Node* _head;
	};

	void test_list1()
	{

	}
}

为什么使用 struct ?

因为里面的数据需要公开的,因为list本质是带头双向循环链表,不公开list就没法使用。

值得注意的是,节点的构造函数中的 T() 表示类型 T默认构造值。

当调用不提供参数时,默认使用类型T的默认值。

主要分为内置类型和自定义类型

  • 内置类型

    • int()0

    • double()0.0

    • bool()false

    • char()'\0'

    • 指针类型 → nullptr

  • 自定义类型

    • 调用该类型的默认构造函数

    • 如果没有默认构造函数,编译会报错

1. list的私有成员

因为list本质是带头双向循环链表,所以需要一个节点指针指向头节点,还需要一个计数器_size来统计节点个数。

cpp 复制代码
	//list 类中的私有成员
    private:
		Node* _head;	//指向头结点
		size_t _size;	//记录结点个数

2. 构造函数

2.1 list构造函数

在不考虑内存池的情况下的list构造函数,首先创建一个节点,然后头节点的_next指向自己,最后头结点的_prev也指向自己。

cpp 复制代码
list() 
{
    _head = new Node;               // 1. 创建一个头节点(哨兵节点)
    _head->_next = _head;           // 2. 头节点的 next 指向自己
    _head->_prev = _head;           // 3. 头节点的 prev 也指向自己
    _size = 0;                      // 4. 方便计算节点个数
}

3. list遍历

3.1 push_back

在实现遍历之前,首先保证list里面有元素,所以先实现一个尾插元素。这里与链表的尾插相似。

cpp 复制代码
		//插入一个数据
		//尾插数据
		void push_back(const T& x)
		{
			//传统写法
			//先申请一个节点
			Node* newnode = new Node(x);
			Node* tail = _head->_prev;	//指向最后一个元素节点

			tail->_next = newnode;
			newnode->_prev = tail;

			newnode->_next = _head;
			_head->_prev = newnode;
            _size++;
		}

使用带头节点的优势就是:当只有一个头节点(哨兵卫)的情况 和 其他情况 进行插入节点都是一样的。

3.2 ListIterator模拟实现

list的遍历需要使用迭代器去遍历。使用迭代器的意义就是不管底层是什么,都可以进行访问。

Q:这里的原生指针可以充当迭代器吗?

A:不可以

因为list和顺序表不一样,在顺序表中,原生指针是天然的迭代器(前提是T*指向的物理空间是连续的);

而list中的原生指针Node*指向的物理空间是不连续的(因为节点是new出来的,不能保证每一个节点的地址都是连续的)。

既然list的原生指针不可以的话,所以封装一个类用自定义类型去重载运算符因为C++中的类和运算符重载可以去控制其行为。

迭代器用什么构造? 节点的指针就可以的,只不过是用类进行封装。

所以这要再命名空间my_list中额外写一个类进行控制。

3.2.1 成员变量_node

这里的使用_node来指向链表中的节点,这里要写成公有类,方便外部使用迭代器去调用。

cpp 复制代码
	template<class T>
	struct ListIterator 
	{
		//自定义类型封装指针,去控制其行为

		typedef ListNode<T> Node;	//模板类重命名为Node

		typedef ListIterator<T> Self;	//模板类重命名为Self

		Node* _node;	// 指向当前迭代器所代表的链表节点的指针
        
        //构造函数
		ListIterator(Node* node) 
			:_node(node)

	};

3.2.2 it++ 和 ++it

因为原生指针不可以充当迭代器,所以这里使用专门封装的类中的运算符重载来进行控制。

3.2.2.1 it++

it++, 这里需要考虑的是先使用后++,返回的是之前的节点,先保存原来的节点,再进行++。

cpp 复制代码
		//后置++,使用(int)来区分前置和后置
		Self operator++(int) 
		{
			//后置++ 返回之前的值
			Self tmp(*this);	//这里调用了拷贝构造,指针内置类型的浅拷贝,因为希望指向同一个空间
								//迭代器也不需要写析构,因为节点不属于迭代器,节点属于链表,所以这里不需要析构
			_node = _node->_next;

			return tmp;
		}

注意:

Q :为什么后置++ 返回T 而不是 T& (或者 为什么后置++是返回临时变量) ?

A :这里返回临时变量,因为后置++ 返回的是自增前的旧值,而旧值是一个临时对象(不能返回局部变量的引用)。前置++ 返回的是自增后的对象本身

3.2.2.2 ++it

++it, 前置++,返回++以后的节点,_node 的下一个节点。

cpp 复制代码
		//重载前置++
		Self& operator++() 
		{
			_node = _node->_next;
			//前置++,返回++以后的值
			return *this;
		}

注意:

Q :前置++为什么返回引用?

A : 因为前置++ 返回的是自增后的对象本身(*this),而 *this 的生命周期,不会立即销毁。

值得注意的是,这里使用 operator(int)++ 来进行区分 前置与后置。

3.2.3 it-- 和 --it

这里自减与上述自增类似。

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

3.2.4 *it

这里需要注意的是,解引用来获取data,不能传值返回,传值返回的是data的拷贝,因为*it 有读和写的功能,传引用返回就可以进行读写data。

cpp 复制代码
		T& operator*() 
		{
			return _node->_data;
		}

3.2.5 operator== 和 operator!=

这里只需比较节点的指针就可以,两个迭代器如果它们里面的指针是相同的,它们就是相等的,不相同就是不相等的。

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

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

3.2.6 operator->

这里为了提高可读性,数据访问时,由it.operator->()->_a1 直接变为 it->_a1。

cpp 复制代码
	//在C++11中支持多参数构造的隐式类型转换
	struct A 
	{
		int _a1;
		int _a2;

		A(int a1 = 1,int a2 = 1)
			:_a1(a1)
			,_a2(a2)
		{}
	};
	void test_list3() 
	{
	
		list<A> lt;
		A aa1(2, 2);
		A aa2 = {3,3};
		lt.push_back(aa1);
		lt.push_back(A(2,2));
		lt.push_back({3,3});	//C++11多参数的隐式类型转换
		list<A>::iterator it = lt.begin();

		cout << it->_a1 << endl;
		cout << it.operator->()->_a1 << endl;
	}

3.3 begin 和 end

使用begin ,因为想使用_head->next 去构造节点,因为_head是私有的,所以使用公有的begin,返回第一个元素的迭代器。

begin返回头结点的下一个节点即可。

普通写法

cpp 复制代码
		iterator begin()
		{
			//1 普通版
			iterator it = _head->_next;
			return it;
		}

匿名对象写法

cpp 复制代码
		iterator begin() 
		{
			//2 匿名对象版
			return iterator(_head->_next);
		}

单参数构造写法

cpp 复制代码
		iterator begin() 
		{
			//3 单参数构造函数支持隐式类型转换
			//这里的迭代器就是单参数构造函数
			return _head->_next;
			//构造函数
			//ListIterator(Node * node)
			//	:_node(node)
			//{}
		}

end返回头结点即可(因为list本质是带头节点的双向循环链表)。

cpp 复制代码
		iterator end() 
		{
            //其他写法同begin类似
			return _head;
		}

3.4 遍历测试

这里可以在命名空间my_list中进行测试。

cpp 复制代码
	void test_list1()
	{
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);
		lt.push_back(5);

		//list的遍历需要使用迭代器
		list<int>::iterator it = lt.begin();
		while (it != lt.end()) 
		{
			cout << *it << " ";
			++it;
		}
		cout << endl;
	}

输出结果 :

4. list 的增加和删除

4.1 empty 和 size

在实现list的增加和删除这里需要先实现,判空和计算节点个数的接口。

cpp 复制代码
		bool empty() const
		{
			return _size == 0;
		}

		size_t size() const
		{
			return _size;
		}

4.2 insert 和 erase

4.2.1 insert

这里要实现一个 在pos节点之前插入一个值为val的函数。

先用cur指向pos节点,再创建一个值为val的节点,再用prev指向cur的前一个节点。

cpp 复制代码
		void insert(iterator pos , const T& val) 
		{
			Node* cur = pos._node;		//cur指向pos位置的节点
			Node* newnode = new Node(val);
			Node* prev = cur->_prev;

			//在cur前面插入一个节点
			// prev newnode cur
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;
			++_size;
		}

4.2.2 erase

删除pos位置的节点,链表只需修改指针域即可。

cpp 复制代码
		iterator erase(iterator pos) 
		{
			//避免空
			assert(!empty());
			//删除pos位置的节点
			//prev cur next
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* next = cur->_next;

			prev->_next = next;
			next->_prev = prev;
			delete cur;
			--_size;

			//pos 失效 是 释放了pos位置的空间
			//为了避免失效,要返回一下节点的迭代器
			return iterator(next);
		}

值得注意的是,erase返回类型是iterator,避免迭代器失效,要返回下一个节点的迭代器。

4.3 push_back(plus)、push_front 和 pop_back、pop_front

这里借助上方实现的insert 和 erase 来进行实现。

cpp 复制代码
		//push_back 现代写法
		void push_back(const T& x)
		{
			insert(end(),x);
		}
		//头插
		void push_front(const T& x) 
		{
			insert(begin(),x);
		}
		//尾删
		void pop_back() 
		{
			erase(--end());	//注意这里不能end()-1 ,因为这里是使用运算符重载来实现的
		}
		//头删
		void pop_front() 
		{
			erase(begin());
		}

值得注意的是,pop_back去调用end()时,不能使用end-1 ,运算符重载只实现了operator--()。

4.4 测试代码

在test.cpp中进行调用,test_list2() 函数在my_list命名空间中进行实现。

cpp 复制代码
	void test_list2() 
	{
		list<int> lt;
		lt.push_back(2);
		lt.push_front(1);
		lt.push_back(3);
		lt.push_back(4);
		lt.push_back(5);

		//整体遍历
		list<int>::iterator it = lt.begin();
		while (it != lt.end()) 
		{
			cout << *it << " ";
			++it;
		}
		cout << endl;

		//头删
		lt.erase(lt.begin());

		it = lt.begin();
		while (it != lt.end())
		{
			cout << *it << " ";
			++it;
		}
		cout << endl;

		//头删
		lt.pop_front();
		//尾删
		lt.pop_back();
		for (auto e : lt) 
		{
			cout << e << " ";
		}
		cout << endl;
		
		cout << "元素个数:"<<lt.size() << endl;
	}

5. const 类型迭代器

我们知道:

cpp 复制代码
		const int* ptr;	// const 在*的左边,修饰的是指针指向的数据不能被修改
		int* const ptr = nullptr;	//const 在*右边,修饰的是指针不能被修改

我们知道权限可以缩小,但是不能放大。这里要实现的是迭代器指向的内容不能被修改。

具体实现:

方式一: 单独实现一个ListConstIterator去封装里面的迭代器指向的内容不能被修改。

cpp 复制代码
	template<class T>
	struct ListConstIterator
	{
		//自定义类型封装指针,去控制其行为
		typedef ListNode<T> Node;	//模板类重命名为Node

		typedef ListConstIterator<T> Self;	//模板类重名为Self

		Node* _node;	// 指向当前迭代器所代表的链表节点

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

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

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

		//通过运算符重载控制其行为
		//重载前置++
		Self& operator++()
		{
			_node = _node->_next;
			//前置++,返回++以后的值
			return *this;
		}

		//后置++,使用(int)来区分前置和后置
		Self operator++(int)
		{
			//后置++ 返回之前的值
			Self tmp(*this);	
			_node = _node->_next;

			return tmp;
		}

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

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

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

	};

方式二:

发现方式一的写法有一些代码冗余,因为里面具体只是一些函数的返回值类型与Iterator类不同,所以可以考虑使用模板参数来控制。

本质:写一个模板类,然后编译器实例化生成两个类。

cpp 复制代码
	template<class T,class Ref,class Ptr>
	struct ListIterator 
	{
		//自定义类型封装指针,去控制其行为
		typedef ListNode<T> Node;	//模板类重命名为Node

		typedef ListIterator<T,Ref,Ptr> Self;	//模板类重名为Self

		Node* _node;	// 指向当前迭代器所代表的链表节点

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

		//* 
		Ref operator*() 
		{
			return _node->_data;
		}

		//it->
		Ptr operator->()
		{
			return &_node->_data;
		}

		//通过运算符重载控制其行为
		//重载前置++
		Self& operator++() 
		{
			_node = _node->_next;
			//前置++,返回++以后的值
			return *this;
		}

		//后置++,使用(int)来区分前置和后置
		Self operator++(int) 
		{
			//后置++ 返回之前的值
			Self tmp(*this);
			_node = _node->_next;

			return tmp;
		}

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

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

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

	};

6. list析构函数 和 拷贝构造

6.1 析构函数

先实现一个clear,借助迭代器和erase删除所有数据(不含头节点)。

cpp 复制代码
		void clear() 
		{
			//借助迭代器和erase
			iterator it = begin();
			while (it != end()) 
			{
				it = erase(it);	//前提是erase处理了迭代器失效问题
			}
		}

再去调用clear实现析构。

cpp 复制代码
		~list() 
		{
			clear();
			delete _head;
			_head = nullptr;
		}

6.2 拷贝构造

值得注意的是,list的拷贝构造需要手动去实现,因为:

当不写拷贝构造,list会使用默认的拷贝构造(浅拷贝--指向同一块空间),但是 对同一块空间进行析构两次是错误的,因为第一次析构后对象就已经不在了,第二次可能导致释放了不该释放的内存!

先初始化一个头节点,再复用push_back,把lt的节点拷贝进去。

cpp 复制代码
		void empty_init()
		{
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;
			_size = 0;
		}

		//list 构造函数
		list()
		{
			empty_init();
		}

		//lt1(lt2)
		list(const list<T>& lt)
		{
			//先初始化一个头节点
			//再复用push_back,把lt的节点拷贝进去
			empty_init();
			for (auto& e : lt) 
			{
				push_back(e);
			}
		}

需要析构,一般需要自己写深拷贝。

6.3 operator=

cpp 复制代码
		void swap(list<T>& lt) 
		{
			//借助标准库函数中的swap来实现
			std::swap(_head,lt._head);
			std::swap(_size,lt._size);
		}
		//lt2 = lt1
		list<T>& operator=(list<T> lt) 
		{
			swap(lt);
			return *this;
		}
相关推荐
DoomGT2 小时前
UE5 - C++项目基础
c++·ue5·ue4·虚幻·虚幻引擎·unreal engine
Yupureki2 小时前
从零开始的C++学习生活 1:命名空间,缺省函数,函数重载,引用,内联函数
c语言·开发语言·c++·学习·visual studio
青草地溪水旁3 小时前
设计模式(C++)详解——策略模式(2)
c++·设计模式·策略模式
鄃鳕3 小时前
高并发日志项目中,C++IO的使用
开发语言·c++
小墨宝3 小时前
web前端学习 langchain
前端·学习·langchain
点云侠3 小时前
PCL 生成缺角立方体点云
开发语言·c++·人工智能·算法·计算机视觉
9毫米的幻想3 小时前
【Linux系统】—— 程序地址空间
java·linux·c语言·jvm·c++·学习
71-33 小时前
C语言——循环的嵌套小练习
c语言·笔记·学习·其他
MediaTea3 小时前
Python 库手册:keyword 关键字查询
开发语言·python