【c++】STL容器-list的模拟实现(迭代器由浅入深逐步完善2w字讲解)

小编个人主页详情<—请点击
小编个人gitee代码仓库<—请点击
c++系列专栏<—请点击
倘若命中无此运,孤身亦可登昆仑,送给屏幕面前的读者朋友们和小编自己!
在这里插入图片描述



前言

在学习list的模拟实现的时候一定要了解list的各个接口是什么作用,所以在学习本文前,请读者友友们先阅读【c++】STL容器-list的使用——书接上文 详情请点击<—
本文会在已经学习了list的使用的基础上进行讲解
本文由小编为大家介绍——【c++】STL容器-list的模拟实现


一、基本框架

对于list的实现,我们要实现的是一个模拟版本,小编会带领读者友友们实现list的基本接口,帮助读者友友们更好的理解list的底层,并且更深一步的掌握list

  1. 我们知道在STL容器中,都是使用模板来实现的,那么既然设计到模板,模板是声明和定义不分离的,本文list的模拟实现,小编采用声明和定义不分离的写法
  2. 并且注意由于我们在test.cpp中需要包含头文件#include<iostream>和展开std命名空间,那么我们自己模拟实现的list就会和库里的list重名,所以我们在list.h中开辟一个我们自己的命名空间,在这个命名空间中用于我们list的模拟实现
  3. 那么我们就需要两个文件,list.h用于放我们的声明和定义,并且进行测试模拟实现的正确性,test.cpp文件放main函数,包#include "list.h"的头文件,声明类域进行测试

在这里插入图片描述

二、模拟实现

注意事项:

  1. 对于绝对不可能为负数的整型值,我们使用size_t进行定义
  2. 文中所指的普通对象是不被const修饰的对象,权限是可读可写,const对象是指被const修饰的对象,权限是只读
  3. 在list.h中进行了断言需要包头文件#include <assert.h>

铺垫

在这里插入图片描述

  1. list是类模板,所以这里我们也要采用类模板的形式模拟实现list,同时list的节点应该独立出来,因为节点中存储的数据类型不确定,应该将节点定义为strcut类模板的形式,关于allocator<T>是空间配置器,内存池,用于避免重复在堆上频繁申请空间,提高效率,读者友友们暂时不需要了解,这里小编在实现的时候也会默认将其忽略
  2. 在使用类模板的时候要使用类型进行显示实例化,显示实例化之后,T就会被识别为用于显示实例化的类型,即T就为我们使用list存储的值的类型
  3. 由于list是由一个个节点链接而成并且要频繁的访问节点的前后两个指针和数据,所以这里我们将节点定义成一个strcut结构体,并且写出构造函数即可,这里的缺省值我们采用匿名对象的形式,如果用户没有显示传入参数,那么编译器默认采用调用对应类型的默认构造函数去构造出T类型的匿名对象去进行初始化,这里的T不仅可以是自定义类型同样也可以是内置类型,因为c++为了更好的适配模板,所以也对内置类型进行了全面升级,使内置类型也可以调用默认构造函数进行构造出对象
  4. 在最初的时候我们默认给节点的前后指针初始化为nullptr即可
  5. 由于链表中需要提供一个size接口用于获取当前链表中的有效节点的个数,但是链表传统方式要想获取有效节点的个数只能采用遍历的方式,这样时间复杂度为O(N),我们这里可以进行优化一下,即初始化定义一个_size作为list对象的成员变量,这样在调用size接口的时候直接返回_size即可,时间复杂度就被优化为了O(1)
  6. 同时由于list的节点是类模板的形式,所以为了便于书写使用,我们使用typedef对节点的类型进行重命名为Node
  7. list是带头双向循环链表,所以无论有没有数据节点,这里我们都要有一个头节点,所以这里我们还需要定义一个头节点的指针_head作为list对象的成员变量用于找到头节点
    在这里插入图片描述
namespace wzx
{
	template<typename T>
	struct list_node
	{
		list_node<T>* _prev;
		list_node<T>* _next;
		T _val;

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

	template<typename T>
	class list
	{
		typedef list_node<T> Node;
	public:
	
	private:
	Node* _head;
	size_t _size;
	};
}

构造函数

  1. 我们知道list的结构是一个带头双向循环链表,那么当没有数据的时候,带头双向循环链表中的结构又是什么样子呢?
  2. 由于没有数据,那么用于带头双向循环链表中就没有了用于存储数据的其它节点,那么只剩下了一个不用于存储数据的头节点,那么仅仅剩下的这一个头节点仍然为带头双向循环链表,那么怎么样处理才可以达到一个头节点就可以循环起来呢?
  3. 让头节点的的两个用于指向下一个节点指针和指向前一个节点的指针指向它自己即可
  4. 这里由于当没有数据的空初始化不仅是构造函数需要使用,同时在拷贝构造的时候仍然需要进行使用,所以这里小编将这里的步骤封装成一个函数便于调用
    在这里插入图片描述
void empty_init()
{
	_head = new Node;

	_head->_next = _head;
	_head->_prev = _head;
	_size = 0;
}

list()
{
	empty_init();
}

push_back

  1. 观察下图,尾节点恰好是头节点的中存储的前一个节点指针指向的位置,那么这里我们采用tail保存指针指向尾节点位置
  2. 按照下图链接关系链接head tail newnode即可
  3. 这里同时也可以使用insert进行实现,在实现完成insert之后,小编会统一进行替换测试
    在这里插入图片描述
void push_back(const T& val)
{
	Node* tail = _head->_prev;

	Node* newnode = new Node(val);

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

	//insert(end(), val);
}

iterator迭代器对应的 begin end(浅度讲解)

list<int>::iterator it = lt.begin();
while (it != lt.end())
{
	cout << *it <<" ";
	++it;
}
  1. 观察迭代器的本质就是模拟指针行为,使用*进行解引用拿到节点中的数据,使用++向后迭代,在string和vector的模拟实现的迭代器中,小编采用的方式都是使用指针指向一块空间,那块空间上直接存放的数据,将指针进行解引用就可以拿到数据,并且由于用于存放数据的那块空间是连续的,所以使用++就可以向后进行迭代找到下一个数据位置
  2. 而list却不可以,这其中的本质是结构不同造成的,list是由一个一个节点构成的,节点中存放着数据,我们使用指针指向节点,进行解引用拿到的是节点,而不是节点中的数据,并且由于节点的物理空间是不连续的,使用++无法进行迭代找到下一个位置的节点,而是访问到了一块未知的空间
  3. 我们思考在节点存放着我们想要拿到的数据和下一个节点的地址,但是我们使用常规指针的原生行为*和++无法拿到我们想要的,同时迭代器的本质又是模拟指针的行为,所以那么我们可以考虑使用一个struct类封装一个节点的指针,并且由于这个节点的类型是不确定的所以这个struct类应该是struct类模板,进而可以通过运算符重载*和++,进而达到我们的目的,同时使用struct是由于我们需要频繁访问类的所有成员,为了避免使用大量友元或返回成员的数据的函数,所以我们这里使用struct而不是class
  4. 迭代器是一个struct模板类,其中封装的成员变量是一个节点指针_node,我们使用这个struct模板类去重载一些基本的运算符重载进而模拟指针的行为,模板类迭代器的类型为__list_iterator<T>而不是__list_iterator,这一点读者友友要仔细区分
template<typename T>
struct __list_iterator
{
	typedef list_node<T> Node;//为了便于书写,重命名节点
	Node* _node;//定义节点的指针

	__list_iterator(Node* node)//构造函数参数接收的类型是__list_iterator<T>
		:_node(node)           //迭代器的构造函数是一个单参数的构造函数,在进行
	{}						   //调用构造时支持将类型隐式类型转换为Node*类型

	__list_iterator<T>& operator++()//前置++模拟让迭代器中的指针指向下一个节点即可
	{							//返回++之后迭代器
		_node = _node->_next;   //并且由于this指针指向的迭代器出了这个重载运算符
							    //++的函数仍然存在,所以这里返回迭代器的引用即可
		return *this;
	}

	__list_iterator<T> operator++(int)//后置++,返回++之前的迭代器,所以需要我们先
	{							   //拷贝一下this指针指向的迭代器,并且让this
		__list_iterator<T> tmp(*this);//指针指向的迭代器中的指针指向下一个位置
		_node = _node->_next;       //这里返回返回迭代器++之前的迭代器即可
									//并且由于这个tmp是一个临时的迭代器对象
		return tmp;    				//临时对象在调用完成该函数后就销毁了
	}								//所以我们需要返回迭代器的拷贝

	T& operator*()//这里直接返回迭代器中的指针指向的节点中存储的数据val即可
	{             //并且由于这个数据出了这个重载运算符*函数的范围后还在,所以
		return _node->_val;//返回引用即可
	}

	bool operator!=(const __list_iterator<T> it) const
	{                            //由于不修改迭代器中指针的值,所以这里的this指针
		return _node != it._node;//指向的内容我们采用const进行修饰,参数中的迭代器
	}                            //同样只是进行比较,不修改迭代器中指针的值,所以
};                               //我们同样也使用const修饰参数中的迭代器
								 //接下来直接比较两个迭代器中的指针是否不相等即可
  1. 同时我们还应该注意小编给这个封装了节点指针的迭代器命名为__list_iterator而不是采用iterator进行命名,其原因是在实际的库中会出现抢占名称的可能,每一个容器都有迭代器,那么你list容器用了,那其它容器呢?是不是,所以容器统一不使用iterator的名称而是采用自己独立命名的名称,这个名称命名完成之后,在容器中typedef为iterator即可,__list_iterator其类型为__list_iterator<T>,在list类模板进行调用的时候应该使用__list_iterator<T>创建这个迭代器对象,为了接口是迭代器iterator的统一性,我们使用typedef将__list_iterator命名为iterator,并且这个typedef命名要在list容器中的public访问限定符下,因为这个命名是给调用list容器的外部使用的
  2. 那么搞定完成,接下来我们编写list中的迭代器对应的begin和end函数让其返回对应的头尾节点的指针即可,由于头节点中不存放数据,头节点中指向的下一个节点才存储数据,所以迭代器对应的begin函数返回对头应该是头节点指向的下一个节点的指针,迭代器对应end函数对应的是存储的最后一个有效数据的节点的下一个节点位置,那么就为头节点,这里我们返回头结点的指针即可
    在这里插入图片描述
iterator begin()
{
	return _head->_next;
}

iterator end()
{
	return _head;
}
测试
  1. 我们使用如下代码进行测试构造函数,尾插,迭代器
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<int>::iterator it = lt.begin();
	while (it != lt.end())
	{
		cout << *it <<" ";
		++it;
	}
	cout << endl;
}

测试结果如下,正确
在这里插入图片描述

const_iterator迭代器对应的begin和end

  1. 对于const对象调用的const_iterator迭代器,不可以使用const修饰普通迭代器iterator<T>,如果这样进行修饰,那么由于性质的不同,list的迭代器中的成员变量即节点的指针就不可以进行修改,就代表迭代器无法重载operator++了,那么迭代器就无法向后迭代了,所以我们不能这样进行修改
  2. 那么我们思考性质,const迭代器的要求是不能修改数据即不能修改迭代器中的指针指向的节点中的数据,那么迭代器中有对应重载运算符*,对于普通对象返回节点的数据的引用用于获取节点的数据。返回,普通对象可以进行修改,const对象不可以进行修改,那么这里只需要控制const对象则返回const引用即可,那么如何更好的进行控制呢?
  3. 再模板参数列表中添加一个模板参数Ref即可实现功能
  4. 我们知道原本迭代器的类型为__list_iterator<T>,在加入了一个模板参数Ref之后,类型就变为了__list_iterator<T,Ref>,由于在迭代器类模板中多次使用了迭代器类型,为了便于修改控制,这里我们就使用typedef将迭代器类型__list_iterator<T,Ref>重命名为self,紧接着在使用到迭代器类型的地方,统一将其替换为self即可
template<typename T,typename Ref>
struct __list_iterator
{
	typedef list_node<T> Node;
	Node* _node;

	typedef __list_iterator<T, Ref> self;

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

	self operator++()
	{							
		_node = _node->_next;   
		
		return *this;
	}

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

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

	bool operator!=(const self& it) const
	{                            
		return _node != it._node;
	}
};
  1. 那么由于迭代器类型的改变,所以在list中我们对于迭代器的重命名也应该对应进行修改,同时将const迭代器对应的名称为const_iterator,同时再写一个由const修饰的begin和end函数,用于const对象的调用,因为编译器会根据调用对象的类型去对应的调用与其类型最为匹配的函数进行调用
typedef __list_iterator<T,T&> iterator;
typedef __list_iterator<T, const T&> const_iterator;

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

const_iterator end() const
{
	return _head;
}
  1. 接下来小编在命名空间内,list模板类外实现一个print函数,其参数是const对象,由于const对象中的数据类型可能是任意类型,所以这里同样需要使用模板,这里传引用因为我们不知道list中数据的类型是自定义类型还是内置类型,如果是自定义类型采用拷贝传参消耗大,所以我们统一使用引用传参,那么使用const对象调用const_iterator迭代器,用于打印测试
template<typename T>
void print(const list<T>& lt)
{
	list<int>::const_iterator it = lt.begin();
	while (it != lt.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}
测试
  1. 我们使用如下代码进行测试,其中lt在调用print函数的时候传参的参数列表中是const类型的对象,去调用的const迭代器进行打印
void test_list2()
{
	list<int> lt;

	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);

	print(lt);
}

测试结果如下,无误
在这里插入图片描述

迭代器中重载运算符->

  1. 我们有一个结构体A对象,其中所有的成员是public访问限定符显示,即外部可以访问这个A对象的所有成员,我们在链表中存储A对象,那么使用常规的*解引用那么拿到的就为结构体对象,并不是结构体对象中的成员变量,这种方式里如果想要进行访问结构体对象中的成员变量应该再额外使用 .成员变量 的方式进行访问,这种方式略显麻烦,那么有没有简单一点的方法
  2. 有的,list中存储的既然是结构体对象,那么结构体指针可以使用->的方式访问结构体的成员变量,那么我们也可以再迭代器模板类中重载一下->,返回取出结构体对象的地址进行返回,即返回结构体的指针的方式使用->访问结构体的成员变量
  3. 这里其实编译器做了优化,我们知道当我们使用->调用了迭代器重载运算符->返回结构体的指针之后应该再使用一个指针才能够进行访问结构体的成员变量即 lt->->_a 的方式才可以,这里直接使用 lt->_a 就可以进行访问是什么原因呢?这里其实是编译器做的优化,再你调用迭代器类模板中重载的运算符->之后会默认再给你添加一个->用于指针访问结构体对象的成员变量
struct A
{
	A(int a=0,int b=0)
		:_a(a)
		,_b(b)
	{}

	int _a;
	int _b;
};

void test_list3()
{
	list<A> lt;

	lt.push_back(A(1, 1));
	lt.push_back(A(2, 2));
	lt.push_back(A(3, 3));
	lt.push_back(A(4, 4));

	list<A>::iterator it = lt.begin(); 
	while (it != lt.end())
	{
		cout << (*it)._a << ' ' << (*it)._b << endl;
		it++;
	}
	cout << endl;
}
  1. 同样的对于普通对象和const对象的解引用之后应该返回的指针类型不同,普通对象应该返回T*,const对象应该返回const T*,这里我们同样使用模板参数Ptr进行控制,并且对对应的迭代器类型使用typedef进行修改为__list_iterator<T,Ref,Ptr>即可
template<typename T,typename Ref,typename Ptr>
struct __list_iterator
{
	typedef list_node<T> Node;
	Node* _node;

	typedef __list_iterator<T, Ref,Ptr> self;

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

	self operator++()
	{							
		_node = _node->_next;   
		
		return *this;
	}

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

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

	Ptr operator->()
	{
		return &(_node->_val);//取结构体对象的地址进行返回,即返回结构体对象的指针
	}

	bool operator!=(const self& it) const
	{                            
		return _node != it._node;
	}
};
  1. 那么由于迭代器类型改变为了__list_iterator<T,Ref,Ptr>,所以在list中我们对于迭代器的重命名也应该对应进行修改
typedef __list_iterator<T,T&,T*> iterator;
typedef __list_iterator<T, const T&,const T*> const_iterator;
测试
  1. 测试使用迭代器对象使用重载运算符->访问结构体对象的成员变量
struct A
{
	A(int a=0,int b=0)
		:_a(a)
		,_b(b)
	{}

	int _a;
	int _b;
};

void test_list3()
{
	list<A> lt;

	lt.push_back(A(1, 1));
	lt.push_back(A(2, 2));
	lt.push_back(A(3, 3));
	lt.push_back(A(4, 4));

	list<A>::iterator it = lt.begin(); 
	while (it != lt.end())
	{
		//cout << (*it)._a << ' ' << (*it)._b << endl;
		cout << it->_a << ' ' << it->_b << endl;
		it++;
	}
	cout << endl;
}

测试结果如下,正确
在这里插入图片描述

迭代器运算符重载- -和==的完善

  1. 那么接下来我们完善前置- -和后置- -和==运算符的重载即可,这里的完善参照一下前置++和后置++和!=运算符的重载即可,触类旁通很容易解决
template<typename T,typename Ref,typename Ptr>
struct __list_iterator
{
	typedef list_node<T> Node;
	typedef __list_iterator<T, Ref, Ptr> self;
	
	Node* _node;

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

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

	self operator++(int)
	{
		__list_iterator tmp(*this);
		_node = _node->_next;

		return tmp;
	}

	self& operator--()
	{
		_node = _node->_prev;

		return *this;
	}

	self operator--(int)
	{
		__list_iterator tmp(*this);
		_node = _node->_prev;

		return tmp;
	}

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

	Ptr operator->()
	{
		return &(_node->_val);
	}

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

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

迭代器拷贝构造的完善

  1. 那么有的时候我们会想要使用普通对象编译器会根据调用的类型去对应的与其类型最匹配的函数,即调用普通对象的begin和end函数,这时候我们想要使用const_iterator去接收会无法接收,即list<A>::const_iterator it = lt.begin(); 这种形式会报错,显示类型无法转换,那么由于it是一个要进行创建的const的迭代器对象,那么尽管这里使用了=,但是lt.begin()返回的是普通的迭代器对象,那么就是使用一个已经存在的对象去初始化与其相同类型的对象,即还是会被编译器优化为拷贝构造,这里我们没有显示写拷贝构造,期望编译器默认生成的拷贝构造可以帮我们完成指针的值拷贝,因为这里我们仅仅是使用迭代器中的成员变量中的节点指针去访问数据,并不需要进行释放数据,所以这里也就不存在对同一块空间释放两次的可能,所以可以仅完成浅拷贝,但是这里的两者一个是const类型的迭代器,一个是普通的迭代器,编译器默认生成的拷贝构造需要拷贝的对象和被拷贝的对象是同一类型,这里明细不是,所以这里编译器默认生成的拷贝构造不能完成我们的预期,那么就需要我们显示写拷贝构造
struct A
{
	A(int a=0,int b=0)
		:_a(a)
		,_b(b)
	{}

	int _a;
	int _b;
};

void test_list3()
{
	list<A> lt;

	lt.push_back(A(1, 1));
	lt.push_back(A(2, 2));
	lt.push_back(A(3, 3));
	lt.push_back(A(4, 4));

	list<A>::const_iterator it = lt.begin(); 
	while (it != lt.end())
	{
		//cout << (*it)._a << ' ' << (*it)._b << endl;
		cout << it->_a << ' ' << it->_b << endl;
		it++;
	}
	cout << endl;
}
  1. 我们显示写拷贝构造的目的是完成拷贝被拷贝的普通迭代器对象中的成员变量即节点的指针,所以我们不需要对被拷贝对象进行修改,所以我们使用const进行修饰即可,这里采用引用传参,虽然这里的迭代器中封装的是节点的指针可以采用传值传参也没问题,但是以后的迭代器中的成员变量可能不仅仅只是节点的指针了,所以这里养成习惯使用引用传参减小消耗,那么我们使用 .去访问对象中的成员变量即节点的指针在初始化列表完成拷贝即可
  2. 同时这里为了便捷,我们同样对普通迭代器的类型进行typedef为iterator进行使用
  3. 这样就完成了const_iterator迭代器对象进行拷贝普通迭代器对象了,所以当我们显示写了拷贝构造,那么编译器就不会再去生成默认拷贝构造函数了,而是去调用我们显示写的拷贝构造函数完成拷贝了
typedef __list_iterator<T, T&, T*> iterator;

__list_iterator(const iterator& it)
	:_node(it._node)
{}
测试
  1. 这里使用如下代码测试普通对象调用的普通迭代器被拷贝构造生成const_iterator类型的迭代器进行使用
struct A
{
	A(int a=0,int b=0)
		:_a(a)
		,_b(b)
	{}

	int _a;
	int _b;
};

void test_list3()
{
	list<A> lt;

	lt.push_back(A(1, 1));
	lt.push_back(A(2, 2));
	lt.push_back(A(3, 3));
	lt.push_back(A(4, 4));

	list<A>::const_iterator it = lt.begin(); 
	while (it != lt.end())
	{
		//cout << (*it)._a << ' ' << (*it)._b << endl;
		cout << it->_a << ' ' << it->_b << endl;
		it++;
	}
	cout << endl;
}

测试结果如下,正确
在这里插入图片描述

迭代器完整版本的源代码

template<typename T,typename Ref,typename Ptr>
struct __list_iterator
{
	typedef list_node<T> Node;
	typedef __list_iterator<T, Ref, Ptr> self;
	typedef __list_iterator<T, T&, T*> iterator;
	Node* _node;

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

	__list_iterator(const iterator& it)
		:_node(it._node)
	{}

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

	self operator++(int)
	{
		__list_iterator tmp(*this);
		_node = _node->_next;

		return tmp;
	}

	self& operator--()
	{
		_node = _node->_prev;

		return *this;
	}

	self operator--(int)
	{
		__list_iterator tmp(*this);
		_node = _node->_prev;

		return tmp;
	}

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

	Ptr operator->()
	{
		return &(_node->_val);
	}

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

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

insert

  1. 在pos迭代器位置前进行插入,那么就要找到pos前的节点位置,这里保存一下pos迭代器中的节点指针指向前一个节点的指针_prev即可找到pos前的节点位置prev
  2. 使用new去申请一个值为val的节点作为要插入的新节点newnode
  3. 按照下图的链接方式进行链接prev newnode pos即可
  4. 同时由于插入了一个节点,那么_size也应该对应加1
    在这里插入图片描述
  5. 当我们链接完成之后要返回指向新插入元素首尾的迭代器即可,观察小编这里传的是newnode其实只是一个节点的指针,但是我们的返回值却是迭代器这能行吗?
  6. 小编想说,能行,这里其实是进行了隐式类型的转换将节点的指针转换为了迭代器类型,因为迭代器类的构造函数是一个单参数的构造函数,当我们进行调用函数或传返回值或使用一个对象的时候,如果目标值类型与我们进行传入的类型不匹配,会通过隐式类型转换将类型转换为目标值类型
  7. 所以传返回值的时候会进行隐式类型的转换将指针转换为了迭代器类型
iterator insert(iterator pos, const T& val)
{
	Node* cur = pos._node;
	Node* newnode = new Node(val);
	Node* prev = cur->_prev;

	prev->_next = newnode;
	newnode->_prev = prev;
	newnode->_next = cur;
	cur->_prev = newnode;

	_size++;

	return newnode;
}

erase

  1. 初始状态无论有数据节点还是没有数据节点,这里的头节点都存在,所以头节点在erase函数中一定不能被删除,所以应该使用asseert断言一下pos不等于头节点
  2. 由于需要删除pos迭代器位置,并且需要将pos迭代器对应节点的前后节点链接起来再进行删除pos迭代器位置对应的节点,所以这里我们分别使用两个指针保存pos迭代器对应的节点的前后位置节点的指针
  3. 按照下图方式链接prev和next即可
  4. 同时删除一个节点之后list对象中的有效节点个数_size应该对应减一
    在这里插入图片描述
  5. 不要忘记使用delete释放pos迭代器对应的节点,防止内存泄露
  6. 同时由于pos迭代器对应的节点已经释放,那么pos迭代器已经失效了,所以这里为了用户便于对后续节点进行操作,所以这里我们默认返回原pos迭代器的节点的下一个节点的迭代器,观察这里小编返回的同样是下一个节点的指针next,这里同样也进行了隐式类型的转化,具体原理请见insert中的讲解隐式类型转换,这里小编就不过多赘述
iterator erase(iterator pos)
{
	assert(pos != end());

	Node* cur = pos._node;
	Node* prev = cur->_prev;
	Node* next = cur->_next;

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

	delete cur;
	cur = nullptr;

	_size--;

	return next;
}

push_back

pop_back

push_front

pop_front

  1. 这里的插入和删除操作调用insert和erase函数即可完成操作
  2. 此处begin返回的是head头节点的下一个节点的迭代器
  3. 此处end返回的是head头节点的迭代器
  4. 观察下图进行对应位置的插入和删除即可
  5. 同时这里插入的数据应该使用引用传参,避免这里的数据类型是自定义类型进行值传参的消耗大
    在这里插入图片描述
void push_back(const T& val)
{
	insert(end(), val);
}

void pop_back()
{
	erase(--end());
}

void push_front(const T& val)
{
	insert(begin(), val);
}

void pop_front()
{
	erase(begin());
}
测试
  1. 进行测试头插头删,尾插尾删
  2. 对于类模板实例化出的对象,当我们使用范围for的时候针对这里变量e的初始化一定要采用引用,因为我们不确定对象中存储的类型是自定义类型还是内置类型,如果要是自定义类型进行拷贝给变量e的消耗大,所以这里采用引用
void test_list4()
{
	list<int> lt;
	lt.push_front(3);
	lt.push_front(2);
	lt.push_front(1);
	lt.push_front(0);

	lt.pop_front();

	lt.push_back(4);
	lt.push_back(5);
	lt.push_back(6);

	lt.pop_back();

	for (auto& e : lt)
	{
		cout << e << ' ';
	}
	cout << endl;
}

测试结果如下,无误
在这里插入图片描述

拷贝构造函数

  1. 由于我们不对被拷贝对象进行修改,所以我们采用const修饰,同时拷贝构造必须是引用传参的形式,否则会造成无穷递归
  2. 针对这里的拷贝构造要拷贝出一个list对象,首先就应该确保这个对象有一个头节点,那么这时复用empty_init即可
  3. 接下来我们使用范围for去遍历被拷贝对象,依次将数据尾插到拷贝对象中即可
  4. 对于类模板实例化出的对象,当我们使用范围for的时候针对这里变量e的初始化一定要采用引用,因为我们不确定对象中存储的类型是自定义类型还是内置类型,如果要是自定义类型进行拷贝给变量e的消耗大,所以这里采用引用
void empty_init()
{
	_head = new Node;

	_head->_next = _head;
	_head->_prev = _head;
	_size = 0;
}

list(const list<T>& lt)
{
	empty_init();

	for (auto& e : lt)
	{
		push_back(e);
	}
}

swap

  1. 这里的传参要采用引用传参,因为要对lt节点内的成员变量进行交换
  2. 接下来调用库里的swap函数依次对this指针指向的对象和lt对象的成员变量头节点_head和有效节点个数_size的交换即可
void swap(list<T>& lt)
{
	std::swap(_head, lt._head);
	std::swap(_size, lt._size);
}

operator=

  1. 这里小编采用现代写法,在接收list对象参数的时候就采用值传参,即调用拷贝构造进行构造出目标对象,这个目标对象中的成员变量即为我们想要的
  2. 接下来进行调用swap交换this指针指向的对象和lt对象即可
  3. 由于要支持连续赋值,所以接下来返回已经得到了想要的的this指针指向的list对象即可
  4. 当operator=函数调用结束的时候,局部对象lt的生命周期结束,由于lt对象和this指针指向的对象已经交换,那么我们需要手动释放的this指针指向的对象的资源的过程就变成了lt对象自动调用析构函数去完成原this指针指向的对象的资源的清理工作
list<T>& operator=(list<T> lt)
{
	swap(lt);

	return *this;
}
测试
  1. 使用如下代码测试拷贝构造和赋值运算符即可
void test_list5()
{
	list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);

	list<int> lt1(lt);
	for (auto& e : lt1)
	{
		cout << e << ' ';
	}
	cout << endl;

	list<int> lt2;
	lt2 = lt1;
	for (auto& e : lt2)
	{
		cout << e << ' ';
	}
	cout << endl;
}

测试结果如下,正确
在这里插入图片描述

clear

  1. 注意这里的clear是清除有效数据的节点,并不删除清理头节点
  2. 那么我们让一个迭代器指向头节点的下一个位置
  3. 当这个迭代器cur不等于头节点的迭代器就调用erase函数删除cur,并且由于使用erase函数删除迭代器位置的节点后会返回删除位置节点下一个位置的节点的迭代器,我们使用cur再进行接收,不断循环即可我们的目的
  4. 同时清除有效数据的节点,那么此时有效节点的个数为0,对应的_size我们应该也置为0
void clear()
{
	iterator cur = begin();

	while (cur != end())
	{
		cur = erase(cur);
	}
	_size = 0;
}

析构函数

  1. 析构函数和clear的区别是析构函数需要清除所有的节点包括头节点,所以这里复用一下clear之后再调用delete删除头节点即可完成list对象中的资源清理工作
~list()
{
	clear();

	delete _head;
}

empty

  1. 当只有一个头节点的时候list对象中就没有存储数据的节点,有效节点个数为0,此时为空
    在这里插入图片描述
  2. 当没有数据节点的时候,头节点中指向的下一个节点指针指向的实际上是头节点本身,那么这里我们使用begin和end函数进行巧妙的判断即可
size_t empty()
{
	return begin() == end();
}

size

  1. 这里直接返回list对象中的成员变量_size即可, 时间复杂度为O(1),并且不需要遍历list对象的节点
size_t size()
{
	return _size;
}
测试
  1. clear的应用场景是当list对象中的数据节点我不想要继续使用了,想进行清理,清理之后重新链接新的数据节点
  2. 这里我们使用如下代码进行clear和empty和size的测试
void test_list6()
{
	list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
	
	lt.clear();
	cout << lt.empty() << endl;
	cout << lt.size() << endl <<endl;

	lt.push_back(4);
	lt.push_back(3);
	lt.push_back(2);
	lt.push_back(1);

	cout << lt.empty() << endl;
	cout << lt.size() << endl <<endl;

	for (auto& e : lt)
	{
		cout << e << ' ';
	}
	cout << endl;
}

测试结果如下,正确
在这里插入图片描述

三、list源代码

list.h

#pragma once

#include <assert.h>

namespace wzx
{
	template<typename T>
	struct list_node
	{
		list_node<T>* _prev;
		list_node<T>* _next;
		T _val;

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

	template<typename T,typename Ref,typename Ptr>
	struct __list_iterator
	{
		typedef list_node<T> Node;
		typedef __list_iterator<T, Ref, Ptr> self;
		typedef __list_iterator<T, T&, T*> iterator;
		Node* _node;

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

		__list_iterator(const iterator& it)
			:_node(it._node)
		{}

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

		self operator++(int)
		{
			__list_iterator tmp(*this);
			_node = _node->_next;

			return tmp;
		}

		self& operator--()
		{
			_node = _node->_prev;

			return *this;
		}

		self operator--(int)
		{
			__list_iterator tmp(*this);
			_node = _node->_prev;

			return tmp;
		}

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

		Ptr operator->()
		{
			return &(_node->_val);
		}

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

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

	template<typename 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 _head->_next;
		}

		iterator end()
		{
			return _head;
		}

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

		const_iterator end() const
		{
			return _head;
		}

		void empty_init()
		{
			_head = new Node;

			_head->_next = _head;
			_head->_prev = _head;
			_size = 0;
		}

		list()
		{
			empty_init();
		}

		void clear()
		{
			iterator cur = begin();

			while (cur != end())
			{
				cur = erase(cur);
			}
			_size = 0;
		}

		~list()
		{
			clear();

			delete _head;
		}

		list(const list<T>& lt)
		{
			empty_init();

			for (auto& e : lt)
			{
				push_back(e);
			}
		}

		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;
		}

		//void push_back(const T& val)
		//{
		//	Node* tail = _head->_prev;

		//	Node* newnode = new Node(val);

		//	tail->_next = newnode;
		//	newnode->_prev = tail;
		//	_head->_prev = newnode;
		//	newnode->_next = _head;
		//}


		void push_back(const T& val)
		{
			insert(end(), val);
		}

		void pop_back()
		{
			erase(--end());
		}

		void push_front(const T& val)
		{
			insert(begin(), val);
		}

		void pop_front()
		{
			erase(begin());
		}

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

			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;

			_size++;

			return newnode;
		}

		iterator erase(iterator pos)
		{
			assert(pos != end());

			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* next = cur->_next;

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

			delete cur;
			cur = nullptr;

			_size--;

			return next;
		}

		size_t empty()
		{
			return begin() == end();
		}

		size_t size()
		{
			return _size;
		}

	private:
		Node* _head;
		size_t _size;
	};

	template<typename T>
	void print(const list<T>& lt)
	{
		list<int>::const_iterator it = lt.begin();
		while (it != lt.end())
		{
			cout << *it << " ";
			++it;
		}
		cout << endl;
	}

	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<int>::iterator it = lt.begin();
		while (it != lt.end())
		{
			cout << *it <<" ";
			++it;
		}
		cout << endl;
	}

	void test_list2()
	{
		list<int> lt;

		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);

		print(lt);
	}

	struct A
	{
		A(int a=0,int b=0)
			:_a(a)
			,_b(b)
		{}

		int _a;
		int _b;
	};

	void test_list3()
	{
		list<A> lt;

		lt.push_back(A(1, 1));
		lt.push_back(A(2, 2));
		lt.push_back(A(3, 3));
		lt.push_back(A(4, 4));

		list<A>::const_iterator it = lt.begin(); 
		while (it != lt.end())
		{
			//cout << (*it)._a << ' ' << (*it)._b << endl;
			cout << it->_a << ' ' << it->_b << endl;
			it++;
		}
		cout << endl;
	}

	void test_list4()
	{
		list<int> lt;
		lt.push_front(3);
		lt.push_front(2);
		lt.push_front(1);
		lt.push_front(0);

		lt.pop_front();

		lt.push_back(4);
		lt.push_back(5);
		lt.push_back(6);

		lt.pop_back();

		for (auto& e : lt)
		{
			cout << e << ' ';
		}
		cout << endl;
	}

	void test_list5()
	{
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);

		list<int> lt1(lt);
		for (auto& e : lt1)
		{
			cout << e << ' ';
		}
		cout << endl;

		list<int> lt2;
		lt2 = lt1;
		for (auto& e : lt2)
		{
			cout << e << ' ';
		}
		cout << endl;
	}

	void test_list6()
	{
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);
		
		lt.clear();
		cout << lt.empty() << endl;
		cout << lt.size() << endl <<endl;

		lt.push_back(4);
		lt.push_back(3);
		lt.push_back(2);
		lt.push_back(1);

		cout << lt.empty() << endl;
		cout << lt.size() << endl <<endl;

		for (auto& e : lt)
		{
			cout << e << ' ';
		}
		cout << endl;
	}

}

test.cpp

#include <iostream>

using namespace std;

#include "list.h"

int main()
{
	wzx::test_list6();

	return 0;
}

总结

以上就是今天的博客内容啦,希望对读者朋友们有帮助
水滴石穿,坚持就是胜利,读者朋友们可以点个关注
点赞收藏加关注,找到小编不迷路!

评论 40
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值