详解c++---list模拟实现

news2025/1/11 21:04:28

目录标题

  • list的准备工作
  • 构造函数
  • push_back
  • list迭代器
  • begin
  • end
  • insert
  • erase
  • clear
  • const迭代器
  • list迭代器区间构造
  • swap
  • 现代拷贝构造函数
  • 现代赋值重载
  • size
  • empty
  • ->重载

list的准备工作

首先我们知道list是一个带头双向链表,他的数据就会存储在一个一个的节点里面,这些节点通过指针相互连接起来所以在实现list之前我们得创建一个结构体,该结构体专门用来描述节点,每当list插入一个数据的时候我们都得通过这个结构体创建一个节点用来描述这个数据的内容以及对应的位置关系,所以该结构体就得含有两个指针和一个数据类型,并且为了让list能够容乃各种各样的数据,我们这里还得给该节点添加上一个模板以面得各种类型:那么这里的代码如下:

	template <class T>
	struct list_node
	{
		list_node* _next;
		list_node* _prev;
		T _data;
	};

当我们把节点创建出来的时候如果还能顺便让他容纳一些数据就好了,所以我们这里再写一个构造函数那么这里的代码如下:

	template <class T>
	struct list_node
	{
		list_node* _next;
		list_node* _prev;
		T _data;
		list_node(const T& x= T())
			:_next(nullptr)
			, _prev(nullptr)
			, _data(x)
		{}
	};

节点的结构体完成之后,我们再来完成list的准备工作,首先list要容乃各种各样的数据,所以该类也得是一个模板类,并且list是带头双向链表,所以在类里面就只有一个成员变量指向头节点就行,那么这里的代码如下:

	template<class T>
	class list
	{
		typedef list_node<T> Node;
	public:

	private:
		Node* _head;
	};

好看到这里我们的准备工作就完成了。

构造函数

因为在执行构造函数之前该对象是没有任何数据的,所以我们构造函数要干的事就是创建一个头节点出来,并让对象中的指针指向头节点,最后将头节点的两个指针都指向自己就行,那么这里的代码就如下:

		list()
		{
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;
		}

push_back

在list里面尾插数据最重要的是节点之间的连接,那么我们这里先创建一个新节点出来,然后再创建一个指针变量用来记录最后一个节点的置,以方便更好的将节点插入到list里面,那么这里的代码如下:

		push_back(const T& x)
		{
			Node* newnode = new Node(x);
			Node* prev = _head->_prev;
		}

接下来就是将prev的_next指向newnode,将newnode的_prev指向prev,再让newnode的_next指向_head,让_head的_prevt指向newnode这样我们就完成了节点的插入,完整的代码如下:

		push_back(const T& x)
		{
			Node* newnode = new Node(x);
			Node* prev = _head->_prev;
			prev->_next = newnode;
			newnode->_prev = prev;
			_head->_prev = newnode;
			newnode->_next = _head;
		}

list迭代器

string和vector的迭代器都是通过对指针进行重命名操作来实现的,我们说迭代器是一个像指针的东西原因是通过对其解引用能够获取对应的数据,对其++或者–能够使得迭代器指向后一个或者前一个数据,这两点跟指针非常的相似,但是迭代器是指针吗?很显然不全是,list迭代器如果是指针的话,我们对齐解引用能够拿到里面存储的内容吗?很显然拿不到,我们对其++或者–的话能够将其指向下一个元素或者上一个元素吗?很显然list中的数据不是连续存储的,所以拿不到,那么这里我们就可以知道一点就是list迭代器不是通过指针重命名实现的,但是我们却必须通过解引用来获取对应的数据,必须通过++和–来改变迭代器的指向,那这不就矛盾了嘛,按照我们的理解只有指针是通过++ --来改变指向位置的,只有指针是可以通过解引用来获取数据的,但是这里的迭代器不是指针啊,它却依然能够这么做,那么这就说明了一种可能,这里的解引用并不是真正的解引用而是运算符重载,这里的++和–也是运算符重载。我们平时在使用迭代器的时候是先创建出来一个迭代器变量,然后对这个迭代器变量进行++或者–,所以对*和++ --的运算符重载就不能在list类中实现而是得单独再写个类,再该类中进行运算符的重载,那么我们就叫这个类为:_list_iterator,既然这个类是专门用来处理迭代器的话,那么这个类中就还得含有一个list_node类型的指针,用于在该类中获取list对象中的数据。首先我们这里肯定得完善一下该类型的构造函数:

	template<class T>
	struct _list_iterator
	{
		typedef list_node<T> node;
		node* _pnode;
		_list_iterator(node* p)
			:_pnode(p)
		{}
	};

然后就是对*的运算符重载,这个运算符重载完成的事情就是返回_pnode指针指向的list对象中的数据,那么这里的代码就很简单直接return _pnode->date就行,那么这里的代码如下:

		T& operator*()
		{
			return _pnode->_data;
		}

接下来就是前置和后置++,那么这里的++是让该迭代器对象指向下一个空间并返回迭代器对象,所以在该函数里面我们直接让_pnode的值等于_pnode中的next值然后返回*this就可以了,那么这里的代码如下:

		_list_iterator<class T>& operator++()
		{
			_pnode = _pnode->_next;
			return *this;
		}

这个是前置++,对于后置++我们得做出一些改变,我们得先通过拷贝构造创建出一个临时对象tmp出来,然后再让_pnode的值等于_pnode中的next值,最后将tmp的值进行返回,那么这就是后置++的运算符重载,其代码如下:

		_list_iterator<class T> operator++(int)
		{
			_list_iterator tmp(*this);
			_pnode = _pnode->_next;
			return tmp;
		}

这里的前置后置++是这么实现的,所以对于前置和后置的–实现的原理也是大致相同,将这里的next改成prev就行,其代码如下:

		_list_iterator<class T>& operator--()
		{
			_pnode = _pnode->_prev;
			return *this;
		}
		_list_iterator<class T> operator--(int)
		{
			_list_iterator tmp(*this);
			_pnode = _pnode->_prev;
			return tmp;
		}

该类中最后一个函数就是对!=实现重载以用来表示迭代器的遍历结束,那么这个函数得有个参数就是迭代器类型的引用,当参数中的迭代器中的_pnode和本对象中的_pnode相等的时候就表明这里的迭代已经结束了,所以我们这里的代码就如下:

		bool operator!=(const _list_iterator& it)
		{
			return _pnode != it._pnode;
		}

那么到这里迭代器的封装就差不多实现完成了,但是要想使用这里的迭代器我们还得实现下面这两个函数。

begin

这个函数在list类中实现,用来以返回当前对象中的第一个位置,因为该函数的返回值要被迭代器对象所用,所以该函数的返回类型得是_list_iterator那么这里为了方便书写我们在list类里对这个名字进行重命名改成iterator,然后在函数体里面就得创建一个iterator对象tmp,并将其值赋值为_pnode的next最后将这个对象返回就行,那么这里的代码如下:

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

当然我们这里可以对其进行一下简化使用匿名对象:

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

end

begin函数是返回第一个元素的位置,end函数是返回最后一个元素的后一个位置,而我们这里的带头双向链表,所以最后一个元素的下一个位置就是头节点,那么这里的代码就如下:

		iterator end()
		{
			return iterator(_head);
		}

那么看到这里我们的普通迭代器就实现完了,我们可以用下面的代码来测试一下上面写的代码:

#include"list.h"
void test1()
{
	ycf::list<int> it1;
	it1.push_back(1);
	it1.push_back(2);
	it1.push_back(3);
	it1.push_back(4);
	it1.push_back(5);
	it1.push_back(6);
	ycf::list<int>::iterator itr = it1.begin();
	while (itr != it1.end())
	{
		cout << *itr << " ";
		++itr;
	}
}
int main()
{
	test1();
	return 0;
}

将这段代码运行一下就可以发现这里的实现是正确的。
在这里插入图片描述
大家看到这里有没有感觉到迭代器的价值,不管什么样的对象不管对象中含有什么类型的数据我们的迭代器都可以以统一的方法来访问这里的数据,对迭代器解引用来获取数据对迭代器加加或者减减来将迭代器指向其他的数据,那么这么看的话迭代器的出现是不是就降低了我们使用该类型的成本,能够让我们更快的接收这个类型,其次迭代器还对底层的实现进行了封装,使其不暴露底层的实现,这样我们在使用它的时候是不是也就更加的方便了,那么这就是迭代器的意义。

insert

该函数的作用就是在我们指定的位置上插入数据,那么这个函数在实现的过程中我们就可以先创建两个指针变量prev和cur,prev指向该位置的前一个,cur指向当前位置,然后再创建一个节点出来,这里的代码如下:

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

那么接下来要干的事情就是将该节点插入到链表里面,首先将prev的_next指向newnode,然后将newnode的_prev指向prev,然后再将newnode的next指向cur,最后将cur的_prev指向newnode,那么这里的代码就如下:

		iterator insert(iterator pos, const T& x)
		{
			Node* newnode = new Node(x);
			Node* prev = pos._pnode->_prev;
			Node* cur = pos._pnode;
			prev->_next = newnode;
			newnode->_prev = prev;
			cur->_prev = newnode;
			newnode->_next = cur;
			return iterator(newnode);
		}

我们来看看下面的代码:

void test2()
{
	ycf::list<int> it1;
	it1.push_back(1);
	it1.push_back(2);
	it1.push_back(3);
	it1.push_back(4);
	ycf::list<int>::iterator itr = it1.begin();
	it1.insert(++itr, 30);
	itr = it1.begin();
	while (itr != it1.end())
	{
		cout << *itr << " ";
		++itr;
	}
}

这段代码的运行结果如下:
在这里插入图片描述
那么这就说明我们这里的代码写的大致是正确的。

erase

这个函数就是删除指定位置的元素,这个函数的使用重点就是做好前后节点的连接,那么这里我们就创建两个指针一个是prev一个是next,分别表示该位置的前一个节点和后一个节点,然后将这两个节点连接起来,再用delete函数将对应位置的元素删除,最后为了防止删除该位置而出现迭代器失效的问题,我们这里将该位置的下一个位置作为返回值进行返回,那么这里的代码就如下:

		iterator erase(iterator pos)
		{
			assert(pos != end());
			Node* prev = pos._pnode->_prev;
			Node* next = pos._pnode->_next;
			prev->_next = next;
			next->_prev = prev;
			delete pos._pnode;
			return iterator(next);
		}

我们来看看下面的测试代码

void test3()
{
	ycf::list<int> it1;
	it1.push_back(1);
	it1.push_back(2);
	it1.push_back(3);
	it1.push_back(4);
	ycf::list<int>::iterator itr = ++it1.begin();
	it1.erase(++itr);
	itr = it1.begin();
	while (itr != it1.end())
	{
		cout << *itr << " ";
		++itr;
	}
}

这段代码的运行结果如下:
在这里插入图片描述

clear

将erase函数实现之后我们这里的clear函数就非常好的实现,首先erase函数返回的是指定位置的下一个元素,那么我们这里就可以创建一个迭代器并将其指向对象的开头,然后我们再创建一个while循环,在循环的里面通过erase函数删除数据,并用这个函数的返回值来跟新迭代器的内容,那么下面就是这个clear函数的代码:

		void clear()
		{
			iterator it1 = begin();
			while (it1 != end())
			{
				it1=erase(it1);
			}
		}

const迭代器

如何来实现const对象?有些小伙伴说:这简单我们直接在普通迭代器的前面加个const就可以了,比如说这样:

const _list_iterator<T> it1 = it,begin();

但是这里有个问题,这里const修饰的并不是it1所指向的内容,而是it1这个迭代器本身,也就是说我们可以对it1指向的值进行修改,但是不能对it1本身进行修改,就好比指针中的int* const p1 = &a;变量a的值可以进行修改但是指针变量p1无法被修改,但是const修饰的对象本身是无法被修改的,所以当我们这样实现const迭代器的话就会发生权限放大的问题,所以这种实现方法肯定是不行的,那么这里还有人会这么想const迭代器的功能是保证迭代器指向的对象无法被修改,而迭代器本身是可以修改的,对迭代器解引用会返回迭代器指向的值,那我对这个返回值加上一个const是不是就可以了呢?既然是const修饰的对象那么该对象中的this指针也会有const修饰,那么这里为了防止解引用出现权限放大的问题,我们这里再给this指针加上一个const来进行修饰,那么这里的代码就如下:

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

好解引用问题解决了,那++和- -的函数重载怎么实现呢?啊这里有小伙伴就要说了,这不是一样的嘛,我们照葫芦画瓢,在返回值和this指针上加个const修饰一下不就可以了吗?

		_list_iterator& operator++()
		{
			_pnode = _pnode->_next;
			return *this;
		}
		const _list_iterator& operator++() const
		{
			_pnode = _pnode->_next;
			return *this;
		}

但是大家仔细想一下修饰this指针的const是保证迭代器中的成员变量无法被修改,但是这里的成员变量是_pnode啊,他不能被修改可是我们却在++操作符重载里面修改了他这不就矛盾了嘛!所以这种方式是不可以。其实上面这种实现最根本的问题并不是++操作符重载带来的问题,大家想一下我们在迭代器类里面实现了const迭代器,那与之对应的我们是不是得在list类里面实现const类型的begin和end函数啊,就好比这样:

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

但是这里真的能返回const修饰迭代器指向对象的迭代器吗?我们一开始就说过const iterator修饰的是迭代器本身而不是迭代器指向的对象,所以这里的const iterator是一样的道理,当我们用这个返回值进行解引用的时候,根本就不会调用const修饰的解引用重载而是普通版本的,所以这里才是问题所在,那么为了解决这个问题,我们就必须再写一个迭代器类,该类专门用来修饰const对象,那么这个类的实现大致与前面那个迭代器类的实现相同,最主要的区别就在于解引用重载的返回值得加上一个const:

	template<class T> 
	struct __list_const_iterator
	{
		typedef list_node<T> node;
		node* _pnode;
		__list_const_iterator(node* p)
			:_pnode(p)
		{}
		const T& operator*()
		{
			return _pnode->_data;
		}
		__list_const_iterator<T>& operator++()
		{
			_pnode = _pnode->_next;
			return *this;
		}
		__list_const_iterator<T>& operator--()
		{
			_pnode = _pnode->_prev;
			return *this;
		}
		bool operator!=(const __list_const_iterator<T>& it)
		{
			return _pnode != it._pnode;
		}
	};

那与之对应的我们在list类中就也得写个专门的begin和end函数:

		typedef _list_iterator<T> iterator;
		typedef __list_const_iterator<T> const_iterator;
		iterator begin()
		{
			return iterator(_head->_next);
		}
		iterator end()
		{
			return iterator(_head);
		}
		const_iterator begin() const
		{
			return const_iterator(_head->_next);
		}
		const_iterator end() const
		{
			return const_iterator(_head);
		}

那么这样的话我们就可以用下面的代码来测试一下这里从const迭代器实现的正确性:

void test5(const ycf::list<int>& it)
{
	ycf::list<int>::const_iterator itr = it.begin();
	while (itr != it.end())
	{
		cout << *itr << " ";
		++itr;
	}
}
void test4()
{
	ycf::list<int> it1;
	it1.push_back(1);
	it1.push_back(2);
	it1.push_back(3);
	it1.push_back(4);
	test5(it1);
}
int main()
{
	test4();
	return 0;
}

我们将这段代码运行一下就可以发现我们这里的实现是正确的:
在这里插入图片描述
但是这里有个问题:这两个类的实现几乎是差不多的,就一个函数的返回值不同,如果我们这里真就这么实现的话,看起来就非常的臃肿那能不能将其合并成为一个类呢?答案是可以的,这里大家可以这么想一个类模板:template<class T>这里的T会被转换成不同的类型,那不同类型的T所生成的具体的类是相同的吗?很明显不是的,那这里我们就可以产生一个新的思路:如果我们再给这个类模板一个参数,用这个参数表示是否是const类型,那这个模板是不是就可以通过这个参数产生两个大类一个是const类型的模板,另一个是非const类型的模板,再通过第一个参数T让这两个模板生成更加具体的类,那么这样的话我们是不是就可以通过一个双参数模板实现了两个单参数模板的功能,那么这里我们就叫第二个参数为Ref,类的模板也就变成了这样:

	template<class T,class Ref>

Ref专门用来表示类是const类型还是非const类型,那么这里我们就可以把解引用重载的函数的返回值改成Ref

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

那么这里为了方便以后的修改我们将这里的类名进行一下重命名:

		typedef _list_iterator<class T, class Ref> self;

那么这里我们完整的代码就如下:

template<class T,class Ref>
	struct _list_iterator
	{
		typedef list_node<T> node;
		typedef _list_iterator<class T, class Ref> self;
		node* _pnode;
		_list_iterator(node* p)
			:_pnode(p)
		{}
		Ref operator*()
		{
			return _pnode->_data;
		}
		self& operator++()
		{
			_pnode = _pnode->_next;
			return *this;
		}
		self operator++(int)
		{
			_list_iterator tmp(*this);
			_pnode = _pnode->_next;
			return tmp;
		}
		self& operator--()
		{
			_pnode = _pnode->_prev;
			return *this;
		}
		self operator--(int)
		{
			_list_iterator tmp(*this);
			_pnode = _pnode->_prev;
			return tmp;
		}
		bool operator!=(const _list_iterator& it)
		{
			return _pnode != it._pnode;
		}
	};

将类里面的代码修改完之后我们还得在list类中对重命名进行一下修改:

		//typedef _list_iterator<T> iterator;
		//typedef __list_const_iterator<T> const_iterator;
		typedef _list_iterator<T,T&> iterator;
		typedef _list_iterator<T,const T&> const_iterator;

首先在list对象创建出来的时候会将T的类型确定下来,当T的类型确定时这里的typedef就会将迭代器模板的两个类型的确定下来,从而生成两个不同的迭代器类,一个是const类型,另外一个是非const类型,这时候就相当于我们前面写的那两个相差不大的迭代器类,那么这就是我们修改的过程,我们将下面的代码再运行一下看看结果如何:

void test5(const ycf::list<int>& it)
{
	ycf::list<int>::const_iterator itr = it.begin();
	while (itr != it.end())
	{
		cout << *itr << " ";
		++itr;
	}
}
void test4()
{
	ycf::list<int> it1;
	it1.push_back(1);
	it1.push_back(2);
	it1.push_back(3);
	it1.push_back(4);
	test5(it1);
}
int main()
{
	test4();
	return 0;
}

运行一下就可以发现这里的代码是没有问题的:
在这里插入图片描述

list迭代器区间构造

这个形式的构造函数有两个参数并都是迭代器类型,那么这里为了面对各种各样的参数,我们就使用函数模板的形式,那么这个函数的声明如下:

	template<class InPutIterator>
	list(InPutIterator first, InPutIterator last)
	{}

在这个函数里面,我们先将对象的内容初始化一下,先创建一个节点然后再让节点的prev和next都指向自己,接下来我们就可以通过while循环和push_back函数将迭代器中的数据尾插到对象里面:

		list(InPutIterator first, InPutIterator last)
		{
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;
			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}

那么这里的函数就完成了,我们可以通过下面的代码来测试一下这个函数的正确性:

void test6()
{
	ycf::list<int> it1;
	it1.push_back(1);
	it1.push_back(2);
	it1.push_back(3);
	it1.push_back(4);
	ycf::list<int>it2(++it1.begin(), --it1.end());
	ycf::list<int>::iterator itr = it2.begin();
	while (itr != it2.end())
	{
		cout << *itr << " ";
		++itr;
	}
}

这段代码的运行结果如下:
在这里插入图片描述

swap

库中的swap函数会造成三次深拷贝从而导致效率低下,所以我们这里自己提供一个swap函数,每个list对象中都含有个头指针,这个指针指向的是链表中的头节点,所以这里的swap函数就直接将对象中的指针的值交换一下就可以了,那么这里的代码如下:

		void swap(list<T>& it1)
		{
			std::swap(_head, it1._head);
		}

下面这个函数的测试代码如下:

void test7()
{
	ycf::list<int> it1;
	it1.push_back(1);
	it1.push_back(2);
	it1.push_back(3);
	it1.push_back(4);	
	ycf::list<int> it2;
	it2.push_back(5);
	it2.push_back(6);
	it2.push_back(7);
	it2.push_back(8);
	it1.swap(it2);
	cout << "it1中的内容为:";
	for (auto itr : it1)
	{
		cout << itr << " ";
	}
	cout << endl;
	cout << "it2中的内容为:";
	for (auto itr : it2)
	{
		cout << itr << " ";
	}
}

代码的运行结果如下:
在这里插入图片描述
那么这就说明该函数的实现是真确的。

现代拷贝构造函数

现代构造函数的精髓就在于让别人干事,将别人的运算结果转换到自己头上来,那么这里我们就可以创建一个临时对象tmp出来,并使用迭代器构造函数将其内容初始化为参数中的内容,然后再用swap函数将两个对象的内容进行交换,那么这里我们的代码就如下:

		list(const list<T>& x)
		{
			list<T> tmp(x.begin(), x.end());
			swap( tmp);
		}

但是这里大家要注意一点就是本对象的_head是一个随机值,当我们使用完swap函数之后会将这个随机值给tmp,而swap函数结束之后会调用tmp的析构函数,由于tmp指向的地方是随机值,所以在指向析构函数的时候就会报错,那么这里我们就得想将本对象的_head进行初始化,那么完整的代码就如下:

		list(const list<T>& x)
		{
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;
			list<T> tmp(x.begin(), x.end());
			swap( tmp);
		}

我们来看看下面的测试代码:

void test8()
{
	ycf::list<int> it1;
	it1.push_back(1);
	it1.push_back(2);
	it1.push_back(3);
	it1.push_back(4);
	ycf::list<int> it2(it1);
	cout << "it2中的内容为:";
	for (auto ch : it2)
	{
		cout << ch << " ";
	}
}

代码的运行结果如下:
在这里插入图片描述

现代赋值重载

这个函数的实现原理和上面的拷贝构造差不多,但是这里我们实现的方法更加的简洁,我们可以通过形参是实参的性质来创建这里的临时对象然后使用swap函数进行交换数据,那么这里的代码就如下:

		list<T>& operator= (list<T> x)
		{
			swap(x);
			return *this;
		}

我们来看看下面的测试代码:

void test9()
{
	ycf::list<int> it1;
	it1.push_back(1);
	it1.push_back(2);
	it1.push_back(3);
	it1.push_back(4);
	ycf::list<int> it2;
	it2 = it1;
	cout << "it2中的内容为:";
	for (auto ch : it2)
	{
		cout << ch << " ";
	}
}

代码的运行结果如下:
在这里插入图片描述
那么这里的结果就是真确的。

size

对于list中size函数的实现有两种:一个是通过循环将整个对象的数据遍历一边,一个是再创建一个私有成员变量通过该变量的来记录list的长度,那么这里我们肯定是采用第二种以空间来换取时间,再创建一个私有成员变量:

	private:
		Node* _head;
		size_t size;

那么在对应的insert函数和push_back函数erase函数都得做出对应的修改,插入数据就让该值加1,删除数据就让该值减一,那么在size函数里面我们就可以直接返回这个值:

		size_t size()
		{
			return _size;
		}

empty

当我们有了成员变量_size,那么这里的empty函数就可以非常好的实现了,我们直接将表达式_size==0作为返回值即可,那么这里的代码如下:

		bool empty()
		{
			return _size == 0;
		}

->重载

我们首先来看看下面的代码:

struct Pos
	{
		int _row;
		int _col;

		Pos(int row = 0, int col = 0)
			:_row(row)
			,_col(col)
		{}
	};
void test_list5()
{
	list<Pos> lt;
	Pos p1(1, 1);
	lt.push_back(p1);
	lt.push_back(p1);
	lt.push_back(p1);
	lt.push_back(Pos(2, 2));
	lt.push_back(Pos(3, 3));
	list<Pos>::iterator it = lt.begin();
	while (it != lt.end())
	{
		cout << (*it)._row << ":" << (*it)._col << endl;
		++it;
	}
	cout << endl;
}

我们这里创建了一个结构体,然后再使用list类创建一个list对象,这个对象里面装的就是结构体pos,然后我们往list对象里面插入数据,再使用迭代器将这个数据给打印出来,首先我们对迭代器解引用能够获取对应的结构体再对结构体执行(.)这个操作符从而获取结构体里面的数据,那么这就是我们打印的方法,这段代码的执行结果如下:
在这里插入图片描述
那么这里大家有没有感觉我们这里写的好像太麻烦了,我们记得当结构体是一个指针的时候我们可以通过这个->操作符来获取结构体里面的数据,那这里可以吗?答案是可以,但是我们得现在迭代器类里面实现该操作符的操作,c++规定这个操作符的重载将返回对应数据的地址,所以这里的函数实现如下:

		T* operator ->()
		{
			return &_pnode->_data;
		}

我们把这个操作符的重载实现之后,我们这里打印就变成了这样:

cout << it->_row << ":" << it->_col << endl;

并且上面的函数执行的结果也是正确的:
在这里插入图片描述
但是大家这里肯定会有个问题,这个函数返回的是数据对应的地址,而我们知道这里的操作符重载只是将运算符的形式发生了更改,真正起到作用的还是函数调用,比如上面的式子真正的形式是这样的:

cout << it.operator->()_row << ":" << it->_col << endl;

it.operator->()是一个函数,这个函数的返回值是一个地址,那我们在地址的后面紧跟着的是_row,这是不是感觉缺了点什么,好像再加个->才合理吧,比如说这样:
cout << it.operator->()->_row << ":" << it->->_col << endl;但是如果真的是这样的话我们在用的时候是不是就非常的不好看啊,所以编译器在这里就做了一个操作他隐藏了一个->,也就是说我们看上去就写了一个实际上编译器这里有两个这样的->操作符,好这个知道以后我们再来看一个问题,这个函数是在iterator类中实现的该类既要面对const对象还要面对普通对象,但是该操作符重载的时候好像返回的类型是T*,无论是什么样的对象调用这个操作符的时候都可以改变对象的值,所以我们这里也得给该操作符实现两个类型,那么我们这里为了不创建两个类,选择在模板中创建第三个参数,专门用来描述const指针,那么这里的模板参数如下:

template<class T, class Ref, class ptr >

list对象的重命名修改成这样:

		typedef _list_iterator<T,T&,T*> iterator;
		typedef _list_iterator<T,const T&,const T*> const_iterator;

将操作符->重载的返回值进行修改:

		ptr operator ->()
		{
			return &_pnode->_data;
		}

重命名进行修改:

	typedef _list_iterator< T, Ref,ptr> self ;

这样我们的函数实现就完成了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/482367.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Python基础合集 练习22 (错误与异常处理语句2)

‘’’ try: 语句块 except: 语句块2 else ‘’’ class Mobe1(): def init(self) -> None: pass def mob1(self):while True:try:num int(input(请输入一个数: ))result 50 / numprint(result)print(50/{0}{1}.format(num, result))except (ZeroDivisionError, ValueEr…

时代浪潮已经袭来 AI人工智能频频爆火 ChatGPT改变行业未来

目录 1 人工智能的发展 1.1人工智能发展历程 1.1.1 人工智能的起源 1.1.2 人工智能发展的起起伏伏 1.1.3 人工智能多元化 2 什么是ChatGPT 2.1 ChatGPT的主要功能 2.2ChatGPT对企业的多种优势 2.3 不必担心ChatGPT带来的焦虑 3 人工智能对行业未来的影响 3.1 人工智…

UG NX二次开发(C++)-建模-根据UFun创建的Tag_t转换为Body

文章目录 1、前言2、用UF_MODL_create_block1创建一个块3、将Tag_t转换为Body出现错误3、解决方法4、结论 1、前言 经常采用UG NX二次开发&#xff08;NXOpen C#&#xff09;&#xff08;UG NX二次开发&#xff08;C#&#xff09;专栏&#xff09;&#xff0c;在用UFun创建一个…

【Git 入门教程】第十节、Git的常见问题

Git是一个强大的版本控制系统&#xff0c;它可以帮助开发者管理和协调代码库。然而&#xff0c;初学者使用Git时可能会遇到一些问题。本文将列举一些常见的问题&#xff0c;并提供相应的解决方案。 1. Git无法识别文件权限 在使用Git时&#xff0c;有时候你可能会遇到类似于“…

异构无线传感器网络路由算法研究(Matlab代码实现)

目录 &#x1f4a5;1 概述 &#x1f4da;2 运行结果 &#x1f389;3 参考文献 &#x1f468;‍&#x1f4bb;4 Matlab代码 &#x1f4a5;1 概述 ​无线传感器网络(Wireless Sensor Networks, WSN)是一种新型的融合传感器、计算机、通信等多学科的信息获取和处理技术的网络,…

Ubuntu下跑通 nnUNet v2

网上关于nnUNet运行的教程大部分是针对nnUNet v1的。但由于nnUNet v2已经推出&#xff0c;而且相对于v1有了很大的更新。所以个人只能啃nnUNet的英文文档参考在Windows上实现nnU-Net v2的环境配置_netv2_无聊的程序猿的博客-CSDN博客 实现了代码的复现。 1.System requirement…

树与图的存储-邻接表与邻接矩阵-深度广度遍历

全部代码 全部代码在github acwing 上 正在更新 https://github.com/stolendance/acwing 图论 欢迎star与fork 树与图的存储 无论是树 还是无向图 都可以看成有向图 有向图可以采用邻接矩阵与邻接表进行存储 邻接矩阵 邻接矩阵 采用矩阵存储,graph[i][j] 代表i到j边的权重…

Python学习15:恺撒密码 B(python123)

描述 恺撒密码是古罗马凯撒大帝用来对军事情报进行加解密的算法&#xff0c;它采用了替换方法对信息中的每一个英文字符循环替换为字母表序列中该字符后面的第三个字符&#xff0c;即&#xff0c;字母表的对应关系如下&#xff1a;‪‬‪‬‪‬‪‬‪‬‮‬‪‬‫‬‪‬‪‬‪…

Markdown编辑器快捷方式

这里写自定义目录标题 欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants 创建一个自定义列表如何创建一个…

Linux安装离线版MySql客户端

本文采用rpm安装方式。 下载文件&#xff1a; mysql-community-libs-5.7.41-1.el7.x86_64 mysql-community-common-5.7.41-1.el7.x86_64 mysql-community-client-5.7.41-1.el7.x86_64 本文件可以在MYSQL官网进行下载 MySQL :: Download MySQL Community Server (Archive…

C语言入门篇——文件操作篇

目录 1、为什么使用文件 2、什么是文件 2.1程序文件 2.2数据文件 2.3文件名 3、文件的打开和关闭 3.1文件指针 3.2文件的打开和关闭 4、文件的顺序读写 5、文件的随机读写 5.1fseek 5.2ftell 5.3rewind 6、文本文件和二进制文件 7、文件读取结束的判定 8、文件…

玩转肺癌目标检测数据集Lung-PET-CT-Dx ——④转换成PASCAL VOC格式数据集

文章目录 关于PASCAL VOC数据集目录结构 ①创建VOC数据集的几个相关目录XML文件的形式 ②读取dcm文件与xml文件的配对关系③创建VOC格式数据集④创建训练、验证集 本文所用代码见文末Github链接。 关于PASCAL VOC数据集 pascal voc数据集是关于计算机视觉&#xff0c;业内广泛…

【五一创作】Pytroch nn.Unfold() 与 nn.Fold()图码详解

文章目录 Unfold()与Fold()的用途nn.Unfold()Unfold()与Fold() 变化模式图解 nn.Fold()单通道 滑动窗口无重叠模拟图片数据&#xff08;b,3,9,9&#xff09;&#xff0c;通道数 C 为3&#xff0c;滑动窗口无重叠。单通道 滑动窗口有重叠。 卷积等价于&#xff1a;Unfold Matri…

Hadoop 2:MapReduce

理解MapReduce思想 MapReduce的思想核心是“先分再合&#xff0c;分而治之”。 所谓“分而治之”就是把一个复杂的问题&#xff0c;按照一定的“分解”方法分为等价的规模较小的若干部分&#xff0c;然后逐个解决&#xff0c;分别找出各部分的结果&#xff0c;然后把各部分的结…

从C语言到C++④(第二章_类和对象_上篇)->类->封装->this指针

目录 1. 面向对象 1.1 类的引入 1.2 class 关键字 2. 类的访问限定符及封装 2.1 访问限定符 2.2 封装 2.2.2 封装的本质 3. 类的作用域和实例化 3.1 类定义的两种方式 3.2 类的作用域 3.3 类的实例化 3.3.1 声明和定义的区别 4. 类对象模型 4.1 计算类的存储大小…

Java开发者在Windows环境安装各类开发工具汇总

Java开发者在Windows环境安装各类开发工具汇总 前言Java JDK下载配置 Tomcat下载配置 Maven下载配置配置仓库 Nginx下载启动关闭 MySQL下载配置my.ini初始化MySQL数据文件安装MySQL服务启动MySQL登录MySQL重置登录密码 NodeJs下载安装与验证配置NPM Git下载配置git配置ssh免密登…

Oracle删除列操作:逻辑删除和物理删除

概念 逻辑删除&#xff1a;逻辑删除并不是真正的删除&#xff0c;而是将表中列所对应的状态字段&#xff08;status&#xff09;做修改操作&#xff0c;实际上并未删除目标列数据或恢复这些列占用的磁盘空间。比如0是未删除&#xff0c;1是删除。在逻辑上数据是被删除了&#…

【MATLAB数据处理实用案例详解(22)】——基于BP神经网络的PID参数整定

目录 一、问题描述二、算法仿真2.1 BP_PID参数整定初始化2.2 优化PID2.3 绘制图像 三、运行结果四、完整程序 一、问题描述 基于BP神经网络的PID控制的系统结构如下图所示&#xff1a; 考虑仿真对象&#xff0c;输入为r(k)1.0&#xff0c;输入层为4&#xff0c;隐藏层为5&…

04-Vue技术栈之组件化编程

目录 1、模块与组件、模块化与组件化1.1 模块1.2 组件1.3 模块化1.4 组件化1.5 传统方式编写应用1.6 组件方式编写应用 2、非单文件组件2.1 基本使用2.2 几个注意点2.3 组件的嵌套2.4 VueComponent2.5 一个重要的内置关系2.6 总结 3、单文件组件3.1 一个.vue 文件的组成(3 个部…

常用排序算法汇总—Python版

一、选择排序 1. 原理&#xff1a; 选择排序&#xff08;Selection Sort&#xff09;是一种简单直观的排序算法&#xff0c;它的基本思路是将数组按顺序分成已排序部分和未排序部分&#xff0c;然后每次从未排序部分中选择出最小的元素&#xff0c;将其添加到已排序部分的末尾…