list的模拟实现(包括对迭代器知识的补充)

目录

list的基础框架

list的正向迭代器类(包括对const iterator和const_iterator的说明)

接下来说说反向迭代器(适配器模式)

简介

基础框架

前置后置的operator++和前置后置的operator--

operator*和operator->

operator=和operator!=

反向迭代器整体代码(包括对反向迭代器的一些总结)

迭代器的分类

list的rbegin()和rend()

开始测试反向迭代器

vector的反向迭代器

list的其他成员函数

1.insert以及复用insert实现的push_back,push_front

2.erase以及复用erase实现的pop_back,pop_front

3.拷贝构造(深拷贝)和迭代器区间构造

4.赋值运算符函数operator= 

5.析构函数和clear()

在类的内部,有时可以不写模板参数

list类的整体代码(可复制)


list的基础框架

#pragma once
#include<iostream>
using namespace std;

namespace mine
{

	template<class T>
	class list_node
	{
	public:                 //节点的所有成员都得是public,否则在list类中无法访问
		list_node()
			:_next(nullptr)
			, _prev(nullptr)
			, _data()
		{}
        
       
		list_node(const T&x)
			:_next(nullptr)
			, _prev(nullptr)
			, _data(x)
		{}
		

		list_node<T>* _next;
		list_node<T>* _prev;
		T _data;
	};


    template<class T>
	class list_iterator
	{
		typedef list_node<T> node;
	public:
		node* _n;

 /*注意构造函数的参数不能为const node*&n,因为成员_n的类型是非const的,接收不了。参数也不可以是node*&n,因为const list对象调用end()接口时会构造指向哨兵位的迭代器,这时会出问题,因为list对象被const修饰后,成员_head也变成了常量指针,即node*const _head,而构造函数的形参n不是node*const&类型,因此无法引用常量_head。*/

		list_iterator(node*n)                                                                                                    
            :_n(n)           
        {}	         
	};



	template<class T>
	class list
	{
		typedef list_node<T> node; //typedef受访问限定符的限制,
                                   //node类型只在list类内部用,因此定义成私有
	public:

        typedef list_iterator<T> iterator;

        //默认构造就是把哨兵位节点创建出来
        list()
		{
			_head = new node;
			_head->_next = _head;
			_head->_prev = _head;
		}
		
		iterator begin()
		{
			return iterator(_head->_next);
		}

		

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

       

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

	private:
		node* _head;
	};

}

如上代码则是基础框架。

上面代码中少了const版本的begin()和end()接口,由于这里涉及到const_iterator的知识点,详情在下文标题为<<list的迭代器类>>的部分说明。

list和【vector、string】不同,实现链表时我们需要定义两个类,一个是list类,一个是list_node类,可以思考下为什么list需要定义两个类呢?

答案很简单,因为list中的数据在物理空间上不连续。思考一下,list中的【list_node】和vector中的【元素】都是用于表示有效数据的,但因为vector的元素在物理空间上连续,只需对当前元素的地址++或者--即可轻松找到下一个或上一个元素,因此vector中的每个元素无需记录相邻元素的地址,只需要存储有效数据即可。但list就不同了,list中的每个元素在物理空间上不连续,因此对当前元素的地址++或者--是无法找到相邻元素的,所以list中的每个元素,不仅要存储有效数据,还要存储相邻元素的地址,不然相邻元素就找不到了,如果不定义一个list_node类,则list中的元素或者说list中的节点就是有效数据本身,此时当然无法同时存储有效数据和相邻节点的地址,因此我们额外定义了一个list_node类,这样就既可以存储有效数据,也可以存储相邻元素的地址了。

list类中有一个_head指针,它指向哨兵位节点。哨兵位是一个不存储有效数据的节点,我们通过哨兵位管理整个list对象,抽象图如下。

有人可能会疑惑,list类的默认构造中为什么要让哨兵位的_next指针和_prev指针指向哨兵位节点本身?既然list中最初只有哨兵位节点,为什么不让哨兵位节点的成员_next指针和_prev指针指向nullptr呢?

答案:

原因1:这里是为了使某些接口编写起来更精简,比如可以对比上面代码和下面代码两种版本的push_back,如果最初哨兵位节点的成员_next和_prev都指向nullptr,则push_back中需要对list为空时进行单独处理,如下面代码所示。

原因2:因为迭代器有需求,begin()和end()接口应该分别返回指向第一个元素和最后一个元素后一个位置的迭代器,当list中为空时,也就是只有一个哨兵位元素时,那么begin()和end()返回的迭代器都应该指向哨兵位,但如果最初哨兵位节点的成员_next和_prev都指向nullptr,观察begin()接口的实现,它就不符合要求了。

#pragma once
#include<iostream>
using namespace std;

namespace mine
{

	template<class T>
	class list_node
	{
	public:               //节点的所有成员都得是public,否则在list类中无法访问 
		list_node()
			:_next(nullptr)
			, _prev(nullptr)
			, _data()
		{}

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


    template<class T>
	class list_iterator
	{
		typedef list_node<T> node;
	public:
		node* _n;

		list_iterator(node*n)                                                                                                  
            :_n(n)          
        {}	               
		
	};

	template<class T>
	class list
	{
		typedef list_node<T> node; //typedef受访问限定符的限制,
                                   //node类型只在list类内部用,因此定义成私有
	public:
        
        typedef list_iterator<T> iterator;

		list()
		{
            _head = new node;
			_head->_next = nullptr;
			_head->_prev = nullptr;
        }
		
   
		
		iterator begin()
		{
			return iterator(_head->_next);
		}

	

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

		void push_back(const T& x)
		{
			if (_head->_next == nullptr)
			{
				node* newnode = new node(x);
				_head->_next = newnode;
				_head->_prev = newnode;
				newnode->_next = _head;
				newnode->_prev = _head;
			}
			else
			{
				node* newnode = new node(x);
				node* tail = _head->_prev;
				tail->_next = newnode;
				newnode->_prev = tail;
				newnode->_next = _head;
				_head->_prev = newnode;
			}
		}

	private:
		node* _head;
	};

}

list的正向迭代器类(包括对const iterator和const_iterator的说明)

基础框架

    template<class T>
	class list_iterator
	{
		typedef list_node<T> node;
	public:
		node* _n;

 /*注意构造函数的参数不能为const node*&n,因为成员_n的类型是非const的,接收不了。参数也不可以是node*&n,因为const list对象调用end()接口时会构造指向哨兵位的迭代器,这时会出问题,因为list对象被const修饰后,成员_head也变成了常量指针,即node*const _head,而构造函数的形参n不是node*const&类型,因此无法引用常量_head。*/
		list_iterator(node*n)                                                                                                       
              :_n(n)         
		{}	
		
	};

可以发现,list的迭代器类和vector或者string的迭代器类不同,我们为list的迭代器类单独封装了一个类,即list的迭代器类是一个自定义类型,而vector却只是在vector中重命名typedef T* iterator定义了一个迭代器类的成员,即vector的迭代器类就是一个原生指针,是一个内置类型。思考一下为什么list的迭代器类要被封装成一个自定义类型呢?直接在list类里typedef node* list_iterator让list的迭代器类是一个原生的指针类型即内置类型不行吗?

答案:不行,直接在list类里typedef node* list_iterator的确是给list定义出了一个迭代器类,但迭代器类本质就是一个node*的指针类型,即是一个内置类型,注意这个迭代器类是不能支持遍历list的,因为对迭代器++就是对node*的指针变量++,而list中node节点的物理空间不连续,因此对迭代器++是找不到list中的下一个元素的,所以我们就需要对operator++这个运算符函数进行重载,并且因为重载过后的operator++函数只能给list_iterator类使用,所以我们最好将operator++函数实现成list_iterator类的成员函数,既然list_iterator类都需要有成员函数了,所以我们就不得不将list_iterator类封装成自定义类型了。

那迭代器类需要实现析构函数吗?答案是不用,因为成员_n指向的是list节点所占的空间,节点不应该归迭代器管,如果一个指向list中某个节点的迭代器销毁导致它指向的节点也销毁,那这就扯淡了。那迭代器类需要实现拷贝构造吗?答案也是不用,因为我们不写,编译器也会默认生成,默认生成的拷贝构造完成值拷贝(浅拷贝),而我们就是想让两个不同的迭代器指向同一个节点,浅拷贝就能达成这个目的。

接下来咱们就来实现几个list_iterator这个自定义类型的成员函数,代码如下。

需要注意的是虽然迭代器是指向node对象的,但operator*对迭代器解引用时返回的不是整个node对象,而是node对象的成员_data。

    template<class T>
	class list_iterator
	{
		typedef list_node<T> node;
	public:
		node* _n;

		list_iterator(node*n)                                                                                                     
              :_n(n)          
		{}	


		bool operator!=(list_iterator<T>it)const
		{
			return _n != it._n;
		}



        //若有 list_iterator<T>it,则 *it完全等价于 it.operator*()
		T& operator*() 
		{
			return (*_n)._data;
		}


		//这里是前置++,比如++it,返回值是引用类型,减少拷贝
		list_iterator<T>& operator++()
		{
			_n = _n->_next;
			return *this;
		}


        //这里是后置++,比如it++,返回值不能是引用类型,因为temp是函数中的局部变量
        //后置++函数中有两个参数,其一是this指针,其二是int,注意第二个参数只能是int
        //这是规定,否则编译器报错,注意后序调用的时候无需传值给这个int类型的形参,编译器
        //会自己去自动处理。
		list_iterator<T> operator++(int)
		{
			list_iterator<T>temp(*this);
			_n = _n->_next;
			return temp;
		}

        //--it
		list_iterator<T>& operator--()
		{
			_n = _n->_prev;
			return *this;
		}

		//it--
		list_iterator<T> operator--(int)
		{
			list_iterator<T>temp(*this);
			_n = _n->_prev;
			return temp;
		}

	};

operator->()比较特殊,接下来咱们说明一下它。

前面也说过,虽然迭代器对象比如it是指向node对象的,但 *it调用operator*函数对迭代器对象解引用时,返回的不是整个node对象,而是node对象的成员_data。这里要注意的_data的类型为T,如果T是一个自定义类型,T类中有好几个成员比如_x和_y,那该如何拿到_x成员呢?

可以通过(*it)._x,但这样写有点不爽,我们说迭代器类虽然不一定是指针类型,但用法和指针是完全一样的,既然指向自定义类对象的指针可以通过p->_x来访问自定义类对象的成员,那么我们的迭代器类也可以重载一个operator->成员函数完成这样的功能。

代码如下。

     

    template<class T>
	class list_iterator
	{
		typedef list_node<T> node;
	public:
		node* _n;

		list_iterator(node*n)/*注意参数不能为const node*&n,因为成员_n的类型是非const的,接收                                                                                                        
              :_n(n)          不了。参数也不可以是node*&n,因为const list对象调用end()时会出    
         {}	                  问题,因为list对象被const修饰后,成员_head也变成了常量指针,即        
                              node*const _head,而构造函数的形参n不是node*const&类型,因此无法    
                              引用常量_head。*/
		

        T* operator->()
		{
            //写法1
			return &(_n->_data);

            //写法2
            //return &(operator*());
		}

	};

编写完operator->后,还是拿上面的例子,假如it是指向node节点对象的迭代器,则咱们现在就已经可以通过it->_x获取node对象的成员_data的成员_x了。

看完operator->的代码后,有同学可能会疑惑了,it->或者说it.operator->()明明只能获取_data的地址,为什么it->_x就可以获取_data的成员_x呢?按理说,应该是it->->_x才能获取_data的成员_x啊?

没错,你的想法完全正确,的确应该是it->->_x才能获取_data的成员_x,只是这里编译器为了语法的可读性,会帮你省略一个->。也就是说 it->_x从表面上看只有一个->,实际上它是有两个->的,即 it->_x等价于it->->_x。

const迭代器(const_iterator和const iterator)

    template<class T>
	class list_iterator
	{
		typedef list_node<T> node;
	public:
		node* _n;
        
 /*注意构造函数的参数不能为const node*&n,因为成员_n的类型是非const的,接收不了。参数也不可以是node*&n,因为const list对象调用end()接口时会构造指向哨兵位的迭代器,这时会出问题,因为list对象被const修饰后,成员_head也变成了常量指针,即node*const _head,而构造函数的形参n不是node*const&类型,因此无法引用常量_head。*/
		list_iterator(node*n)                                                                                                        
              :_n(n)         
         {}	                
		
	};

一听到const迭代器,有人就会认为这不就是在迭代器变量前加个const吗?拿上面代码举例子,const迭代器不就是const list_iterator的对象吗?

还真不一定,口头说的const迭代器有可能是用const修饰的迭代器,比如const list_iterator的对象,也有可能是const_iterator的对象,那么这俩有什么区别呢?

const list_iterator对象的const修饰的是迭代器对象本身,即迭代器对象所有成员的值都不能被修改,拿上面代码举例,list_iterator的对象如果用const修饰,则只能保证指向某个node节点的_n这个指针的指向不能修改,但指向空间(节点)上的数据,也就是节点的数据是可以修改的。

我们说迭代器类不一定是指针类型,但迭代器类的用法和指针类是完全相同的,所以const list_iterator类的对象的功能就对应 int*const类的指针变量,const保证了迭代器的指向不能被修改,但指向空间上的数据是可以被修改的。(const左定值右定向)

接下来说说const_iterator,经过前面的铺垫,想必肯定有同学猜到它是干嘛的了。没错,const_iterator类的对象的功能就对应const int*类的指针变量,const保证了迭代器指向空间上的数据是不能被修改的,但迭代器的指向可以被修改。

所以也能得到一个结论,const iterator和const_iterator是两个完全不同的类。iterator和const_iterator唯一的区别在于可以通过前者对指向节点的数据做修改,而后者不能。

实际案例

走到这里,我们的list<T>类已经可以基本跑起来了,如下图所示。

但未解决的问题依然很多,比如下面代码就跑不了,下面代码的逻辑就是遍历并输出一个const list<int>&类型的链表对象。

void print_const_list(const mine::list<int>&ls)
{
	mine::list<int>::iterator it = ls.begin();
	while (it != ls.end())
	{
		cout << (*it) << ' ';
		it++;
	}
}

为什么编译不通过呢?很简单,因为ls是一个const对象,无法访问非const的成员函数begin(),因此上面代码中的 ls.begin()出错。

如何解决呢?答案:我们在类模板list<T>中重载一个const版本的函数模板begin()即可。同时,因为begin()接口用于返回指向哨兵位节点的下一个节点的迭代器,所以为了编写begin()接口,我们还得思考一下对于const list<T>类型的链表对象来说,指向这条链表上的节点的迭代器应该具有什么性质呢?

首先要知道的是:一个自定义类的对象通过const修饰后,对象中所有成员的值就不能被修改。所以一个const list<T>的对象通过const修饰符修饰后,const从语法角度上只能支持list对象中所有成员的值不发生变化,同时因为模拟实现的list中只有一个成员,即node* _head指针成员,所以const只能保证_head指向不发生变化,但无法支持_head指向空间(节点)上的数据,即node的_data不发生变化。目前做不到但我们期望做到的是:如果一个list<T>对象用const修饰,那么list对象中所有节点上的数据都不能再被修改。理解了前文,现在我们知道了光靠const修饰list<T>对象是无法完成我们的期望的,该怎么办呢?我们常说所有容器通用的遍历(读写)数据的方式是通过迭代器,甚至对于绝大多数容器来说是只能通过迭代器访问(读写)容器中的数据的,这是不是能给我们一个启示,只要让const list<T>类型的链表对象无法通过迭代器修改迭代器指向节点的数据,就可以完成我们的期望?没错,这就是正解!因此我们要通过编写代码,让指向const的链表对象的上节点的迭代器应该具有【类似于指针const node*p的不能修改指向空间(节点)上的数据】的性质。观察这个性质,这不就是上文中说的const_iterator所具有的性质吗?所以接下来我们只要编写出一个const_iterator类,就能完成我们的期望,一切问题也就迎刃而解了。

接下来我们就来编写const版本的begin接口以及const_iterator类,正确代码如下。

   
    template<class T>
	class const_iterator
	{
		typedef list_node<T> node;
	public:
		node* _n;

		const_iterator(node*n)
			:_n(n)
		{}

        bool operator!=(const_iterator<T>it)const
		{
			return _n != it._n;
		}

		//虽然迭代器指向node,但此接口返回的不是整个node对象,而是node中的_data成员
		const T& operator*()
		{
			return (*_n)._data;
		}

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

        //这里是前置++,比如++it,返回值是引用类型,减少拷贝
		const_iterator<T>& operator++()
		{
			_n = _n->_next;
			return *this;
		}
        //这里是后置++,比如it++
        const_iterator<T> operator++(int)
		{
			const_iterator<T>temp(*this);
			_n = _n->_next;
			return temp;
		}

        //--it
		const_iterator<T>& operator--()
		{
			_n = _n->_prev;
			return *this;
		}

		//it--
		const_iterator<T> operator--(int)
		{
			const_iterator<T>temp(*this);
			_n = _n->_prev;
			return temp;
		}

    };
    
    template<class T>
	class list_iterator
	{
		typedef list_node<T> node;
	public:
		node* _n;

		list_iterator(node*n)                                                                                                   
              :_n(n)          
		{}	


		bool operator!=(list_iterator<T>it)const
		{
			return _n != it._n;
		}



        //若有 list_iterator<T>it,则 *it完全等价于 it.operator*()
		T& operator*() 
		{
			return (*_n)._data;
		}

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


		//这里是前置++,比如++it,返回值是引用类型,减少拷贝
		list_iterator<T>& operator++()
		{
			_n = _n->_next;
			return *this;
		}


        //这里是后置++,比如it++,返回值不能是引用类型,因为temp是函数中的局部变量
        //后置++函数中有两个参数,其一是this指针,其二是int,注意第二个参数只能是int
        //这是规定,否则编译器报错,注意后序调用的时候无需传值给这个int类型的形参,编译器
        //会自己去自动处理。
		list_iterator<T> operator++(int)
		{
			list_iterator<T>temp(*this);
			_n = _n->_next;
			return temp;
		}

        //--it
		list_iterator<T>& operator--()
		{
			_n = _n->_prev;
			return *this;
		}

		//it--
		list_iterator<T> operator--(int)
		{
			list_iterator<T>temp(*this);
			_n = _n->_prev;
			return temp;
		}

	};

    template<class T>
	class list
	{
		typedef list_node<T> node; //typedef受访问限定符的限制,
                                   //node类型只在list类内部用,因此定义成私有
	public:
        typedef const_iterator<T> const_iterator;
        typedef list_iterator<T> iterator;

		list()
		{
            _head = new node;
			_head->_next = nullptr;
			_head->_prev = nullptr;
        }
		
        const_iterator begin()const
		{
			return iterator(_head->_next);
		}
		
		iterator begin()
		{
			return iterator(_head->_next);
		}

    private:
        node*_head;
    };

编写完const_iterator类和const版本的begin()接口后,之前编译不通过的但走到现在能正常运行的代码如下图所示。

void print_const_list(const mine::list<int>&ls)
{
	mine::list<int>::const_iterator it = ls.begin();
	while (it != ls.end())
	{
        //(*it)=10; ERROR,const_iterator对象指向的节点上的数据无法被修改
		cout << (*it) << ' ';
		it++;
	}
}

走到现在,如果是普通的list<T>对象调用begin()接口,则返回iterator类的迭代器对象,对它解引用会返回迭代器指向节点中的数据,即T&类型的_data,因为_data没有被const修饰,所以可以被修改,如果通过它调用operator->,返回的地址则是T*类型,*左边没有const修饰,因此可以通过迭代器对象对指向地址上的数据进行修改。而如果是const list<T>对象调用begin()接口,则只会返回const_iterator类的迭代器对象,对const_iterator类的迭代器对象解引用,则返回的节点中的数据_data是const T&类型的值,因为_data被const修饰,所以无法被修改,如果通过它调用operator->,返回的指针变量类型是const T*,*的左边有const修饰,因此无法通过迭代器对象对指向地址上的数据进行修改。此时就完成了我们的期望:如果一个list<T>对象用const修饰,那么list对象中所有节点上的数据都不能再被修改。

有同学在完成我们的期望时,或许会有一个错误的想法,会认为干嘛再实现一个const_iterator的类模板,如下面代码所示,我直接在list_iterator类内部再重载一个const版本的operator*和const版本的operator->函数,返回值类型分别是const T&和const T*,既然我们不想让const list<T>对象中的节点被修改,则之后只要有需要【指向const list<T>对象中节点的】迭代器的地方,我们都使用const list_iterator,因为const版本的operator*返回值是const T&,const版本的operator->返回值是const T*,这样即使对迭代器解引用或者通过迭代器调用->访问节点上的数据,也只能读节点上的数据而不能写节点上的数据,这不就完成我们的期望了吗?

你说的没错,这样一来的确无法通过const list_iterator类的迭代器对象修改它指向节点的数据,但这里有一个坑,那就是list_iterator类的迭代器对象通过const修饰后,会让node* _n这个指针成员的指向无法被修改,也就导致你这个迭代器只能固定地指向一个节点,无法完成迭代,因此我们不要这么做。但我们从也能从这个坑里得出一个结论,不管是普通的 iterator,还是const_iterator,如果在它们前面加了const修饰,比如const iterator和const const_iterator,都会导致迭代器的指向无法被修改,导致迭代器完成不了迭代。

    template<class T>
	class list_iterator
	{
		typedef list_node<T> node;
	public:
		node* _n;

		list_iterator(node*n)                                                                                                  
              :_n(n)          
		{}	


        //若有 list_iterator<T>it,则 *it完全等价于 it.operator*()
		T& operator*() 
		{
			return (*_n)._data;
		}
        
           
		const T& operator*()const 
		{
			return (*_n)._data;
		}

        list_iterator<T> operator++(int)
		{
			list_iterator<T>temp(*this);
			_n = _n->_next;
			return temp;
		}

    };

对于之前实现的正确的【const版本的begin接口以及const_iterator类】的代码,虽然能解决问题,但还存在一个弊端,那就是代码冗余了。观察代码可以发现list_iterator<T>和const_iterator<T>这两个类模板的各个成员函数中,除了数据(比如返回值和形参...等等)的类型不同以外,其余的所有因素是完全一致的,因此,我们可以考虑通过list_iterator<T>这个类模板实例化出两个不同的迭代器类,一个类具有iterator的性质,另一个类具有const_iterator的性质。

现在我们来思考一下该如何做到。前面也说过,iterator和const_iterator唯一的区别在于可以通过前者对指向节点的数据做修改,而后者不能,也就是说只要把下面代码中的operator*的返回值从T&修改成const T&,list_iterator<T>就能实例化出具有const_iterator性质的迭代器类,但我们不能这么做,因为这样会导致list_iterator<T>只能实例化出具有const_iterator性质的迭代器类,导致list_iterator<T>无法实例化出具有iterator性质的迭代器类。

    template<class T>
	class list_iterator
	{
		typedef list_node<T> node;
	public:
		node* _n;

		list_iterator(node*n)                                                                                                       
              :_n(n)          
		{}	

       

		T& operator*() 
		{
			return (*_n)._data;
		} 

	};

那该如何解决呢?

如下面代码即可解决代码冗余的问题。

    template<class T,class Ref,class Ptr>
	class list_iterator
	{
		typedef list_node<T> node;
	public:
		node* _n;

		list_iterator(node*n)
			:_n(n)
		{}
		
	
		//若有 list_iterator<T>it,则 *it完全等价于 it.operator*()
		Ref operator*()
		{
			return (*_n)._data;
		}
		
		
		Ptr operator->()
		{
			return &(_n->_data);
		}

	};

    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;
		
        list()
		{
			_head = new node;
			_head->_next = _head;
			_head->_prev = _head;
		}

		const_iterator begin()const
		{
			//方案1,使用别名
			//return const_iterator(_head->_next);

			//方案2,不使用别名
			return list_iterator<T, const T&, const T*>(_head->_next);

		}

		iterator begin()
		{
			return iterator(_head->_next);
		}
		
		const_iterator end()const
		{
			//方案1,使用别名
			//return const_iterator(_head);

			//方案2,不使用别名
			return list_iterator<T, const T&, const T*>(_head);
		}

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


	private:
		node* _head;
	};

这里得先知道两个知识点,之后才方便讲解上面的代码:

其一,类模板中的成员函数都是函数模板,因为类模板中的函数在类模板外定义时是需要template的。

其二,typedef一定只能给具体的类起别名,而不能给类模板起别名,忘记了去看笔者写的typedef和typename相关的文章。

如上面代码,首先把类模板list_iterator的模板参数从1个增加到3个,分别是T,Ref和Ptr。

其中模板参数T是为了让类模板list_iterator中的类模板list_node<T>能够被实例化,Ref则是让类模板list_iterator中的函数模板operator*能够被实例化,Ptr则是让类模板list_iterator中的函数模板operator->能够被实例化。

这里举个例子你就能理解上面代码是怎么解决代码冗余的问题的。

假如我定义一个对象list<int>ls,则流程是编译器会先将类模板list<T>实例化成一个具体的list<int>类,然后帮我定义对象。

既然类模板list<T>被实例化成了list<int>,此时T就是int,则list内部所有有T的地方全被替换成int,如上图红框处,这就相当于因为类模板list<T>实例化出了list<int>,导致其他类模板如上图红框处的两个类模板list_node<T>、list_iterator<T,Ref,ptr>也全部实例化,前者实例化出一个类,为list_node<int>类、后者实例化出两个类,分别是list_iterator<int,int&,int*>类,list_iterator<int,const int&,const int*>类。此时在list_iterator<int,int&,int*>类中,int就和如下面代码中的模板参数T对应,int&就和如下面代码中的模板参数Ref对应,int*就和如下面代码中的模板参数Ptr对应,这些模板参数也就是operator*和operator->的返回值都不带const,所以具有普通iterator性质的迭代器类也就实例化成功了。而在liest<int,const int&,const int*>类中,int就和如下面代码中的模板参数T对应,const int&和Ref对应,const int*和Ptr对应,这些模板参数也就是operator*和operator->的返回值都带了const,因此具有const_iterator性质的迭代器类也实例化成功。

    template<class T,class Ref,class Ptr>
	class list_iterator
	{
		typedef list_node<T> node;
	public:
		node* _n;

        
		list_iterator(node*n)
			:_n(n)
		{}
		
	
		//若有 list_iterator<T>it,则 *it完全等价于 it.operator*()
		Ref operator*()
		{
			return (*_n)._data;
		}
		
		
		Ptr operator->()
		{
			return &(_n->_data);
		}

	};

可以发现,通过多个模板参数可以很灵活的控制operator*和operator->的返回值类型,拿operator*举例,如果类模板list_iterator只有一个模板参数T,则operator*返回值的类型要么是T&,要么是const T&,此时就无法通过一个类模板list_iterator既实例化出具有iterator性质的迭代器类,又实例化出具有const_iterator性质的迭代器类。(list的迭代器具有iterator的性质表示可以通过迭代器修改它指向节点上的数据,具有const_iterator的性质表示不可以通过迭代器修改它指向节点上的数据)而如果有多个模板参数,比如添加了Ref,我给Ref传const int&,那它就是const int&,我给Ref传int&,那它就是int&,这样就可以灵活控制operator*的返回值类型。有人说,T和Ref明明都是一个模板参数,凭什么T不行而Ref行呢?很简单,因为Ref这个模板参数是专门为了控制operator*的返回值类型而设置的,但T不是,T这个模板参数是专门为了控制类模板list_iterator中的另一个类模板list_node<T>该如何实例化而设置的。

具有普通iterator性质的迭代器类和具有const_iterator性质的迭代器类都被实例化成功后,则下面代码中则可以按需使用这两个迭代器类了。

    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;
		
        list()
		{
			_head = new node;
			_head->_next = _head;
			_head->_prev = _head;
		}

        //方案1,使用别名
		//const_iterator begin()const
		//{
		//	return const_iterator(_head->_next);
		//}

        //方案2,不使用别名
        list_iterator<T,const T&,const T*> begin()const
		{						
			return list_iterator<T, const T&, const T*>(_head->_next);
		}

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

        //方案1,使用别名
		//const_iterator end()const
		//{
		//	return const_iterator(_head);
		//}

        //方案2,不使用别名
        list_iterator<T,const T&,const T*>end()const
		{						
			return list_iterator<T, const T&, const T*>(_head);
		}
	

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


	private:
		node* _head;
	};

走到这里,关于代码冗余的解决方案也就讲解完毕了。

接下来说说反向迭代器(适配器模式)

简介

如上图,rbegin()返回指向最后一个元素的反向迭代器,rend()返回指向哨兵位节点的反向迭代器,注意这和STL标准的rbegin()与rend()不同,STL版本的详情请见下文,但即使不同,像上图这么设计也是正确的,下面编写代码时咱们会以STL为标准,但这里讲解时就按照上图的版本举例。通过上图,反向迭代器给我们的第一印象就是迭代遍历的起始位置和正向迭代器不同,终止位置是相同的,成员函数operator*和operator->的功能与逻辑也是相同的。除了这些以外,似乎反向迭代器和正向迭代器也就一个区别,那就是反向迭代器的operator++是从右往左走,如上图rit++后会从指向5变成指向4,而正向迭代器it++是会从指向1变成指向2的。既然正向迭代器类和反向迭代器类的区别不大,那么模拟实现list的反向迭代器类时,有人的第一想法就是拷贝一份list的正向迭代器类的代码,只需要将operator++等等一些接口中的逻辑做修改即可。这是没问题的,但如果这样实现list的反向迭代器类,这意味着你实现其他的容器时,在编写该容器的反向迭代器类时,也是采用同样的思路,即先拷贝正向迭代器类的代码,然后做出修改。这样有一个弊端,是什么呢?因为每种容器底层的物理存储结构不同,所以每种容器遍历数据的方式也不同,所以我们不得不为每种容器单独设计正向迭代器,但如果为容器实现反向迭代器类的思路是通过【拷贝正向迭代器的代码后做一点修改】,这相当于我们还为每种容器又单独设计了反向迭代器,这会增加程序员的代码量,增加负担。

那该如何正确的实现list的反向迭代器类呢?

前面也说过,每种容器的正向迭代器和反向迭代器的区别不大,既然区别不大,我们就可以考虑复用正向迭代器的代码实现反向迭代器,注意这里的复用不是指拷贝正向迭代器的代码,而是通过模板将反向迭代器类设计成泛型,设计STL的大佬们就是这么做的,只需要设计一个反向迭代器的类模板,然后只要实现了某个容器的正向迭代器,通过这个反向迭代器的类模板,我都能瞬间实现该容器的反向迭代器,这就是反向迭代器的精髓,但注意通过这样的方法实现一个容器的反向迭代器是有一个条件的,简而言之就是该容器的正向迭代器必须是一个双向迭代器,详情在反向迭代器的整体代码部分再介绍。

首先说一下,反向迭代器类和正向迭代器类不同,反向迭代器类是一种正向迭代器的适配器,该类也是通过适配器模式编写的,这里提一下,设配器模式最大的特点就是复用,不用重头造轮子。

什么是正向迭代器的适配器?首先适配器就是一种在不同的环境下都能完成任务的东西,拿电源适配器比如我们手机的充电器来说明,手机的电源适配器只负责充电(任务),而不管你电压(环境)是多少。我可以插在家里的插座上,经过适配器将220v的电压转化成合适的大小给手机充电,也可以插在车上,经过适配器将车载电压转化成合适的大小给手机充电,甚至可以插在蓝牙耳机上给手机充电,也就是说电源适配器能够适应不同的电压(环境)完成充电(任务)。既然电源适配器是一个能够适配不同的电源(电压)完成充电任务的东西,那我们也就能通过它理解正向迭代器的适配器是什么了,它就是一种能够适配不同的正向迭代器(环境)的具有反向迭代器功能(任务)的东西。

基础框架

走到这里就已经可以开始编写反向迭代器的代码了,先来看基础框架,代码如下。模板参数Iterator就表示某种容器的正向迭代器的类型,这也对应上文中说反向迭代器是一种正向迭代器的适配器,反向迭代器是通过适配该正向迭代器完成反向迭代的功能的。

#pragma once
#include<iostream>
using namespace std;

namespace mine
{
	template<class Iterator>
	class reverse_iterator
	{
	public:
		reverse_iterator(const Iterator& it)
			:_it(it)
		{}

	private:
		Iterator _it;
	};

}

前置后置的operator++和前置后置的operator--

下面代码中为什么要typedef reverse_iterator<Iterator> r_iterator呢?为了在类内部需要reverse_iterator<Iterator>类型的变量时,统一通过r_iterator定义变量。这是一个小技巧,因为reverse_iterator<Iterator>带了模板参数,如果使用reverse_iterator<Iterator>大量的定义变量,假如在编写reverse_iterator类时发现需要增加其他模板参数,则所有通过reverse_iterator<Iterator>定义变量的位置都得修改,都得增加该模板参数,这样就太麻烦了,而如果通过r_iterator定义变量,template<>中增加模板参数后,只需将typedef的位置进行修改即可。

#pragma once
#include<iostream>
using namespace std;

namespace mine
{
	template<class Iterator>
	class reverse_iterator
	{
		typedef reverse_iterator<Iterator> r_iterator;//typedef受到访问限定符限制
	public:
		reverse_iterator(const Iterator& it)
			:_it(it)
		{}

		//前置++,反向迭代器的++it就对应正向迭代器的--it
		r_iterator operator++()
		{
			return r_iterator(--_it);
		}

		//前置--,反向迭代器的--it就对应正向迭代器的++it
		r_iterator operator--()
		{
			return r_iterator(++_it);
		}

		//后置++,反向迭代器的it++就对应正向迭代器的it--
		r_iterator operator++()
		{
			r_iterator temp(_it);
			_it--;
			return temp;
		}

		//后置--,反向迭代器的it--就对应正向迭代器的it++
		r_iterator operator--(int)
		{
			r_iterator temp(_it);
			++_it;
			return temp;
		}
	private:
		Iterator _it;
	};

}

operator*和operator->

编写该接口前,我们得先知道一件事情。之前其实在简介部分也提过,事实上STL标准的rbegin()和rend()返回的反向迭代器所指向的位置和咱们简介部分举例时所说的有一些区别。如下图,rbegin()返回指向哨兵位节点的反向迭代器,rend()返回指向链表首元素的反向迭代器,下图两个红框部分是STL的源码。为什么这么设计呢?是为了看起来和begin()与end()接口对称,你看下图,rbegin()返回的反向迭代器和end()返回的正向迭代器指向同一位置,rend()返回的反向迭代器和begin()返回的正向迭代器指向同一位置,意为:反向迭代器的开始(begin)是正向迭代器的结束(end),反向迭代器的结束(end)是正向迭代器的开始(begin),我反向迭代器的起始位置和结束位置要和你正向迭代器倒着来。

但STL版本的rbegin()和rend()面临一个问题。

上文中说过虽然迭代器指向node节点,但operator*解引用迭代器时返回的不是节点node,而是node中的成员:为T类型的有效数据本身(注意返回值不是该T类数据的拷贝),也就是说operator*的返回值类型为T&,如果list被const修饰,则还需要operator*的返回值类型为const T&;operator->则返回的是T类型数据本身的地址,返回值类型为T*,如果list被const修饰,则operator->的返回值类型还需要为const T*。我们不知道T&、T*、const T&、const T*中的T是什么,因此需要增加1个模板参数Arg,让别人显示传。注意之前编写正向迭代器时我们为operator*和operator->这俩个函数的返回值增加两个模板参数Ref(reference引用)和Ptr(pointer指针),但现在我们只为反向迭代器增加了一个模板参数Arg,所以这里我们是换一种写法的,当然你也可以选择为反向迭代器增加Ref和Ptr这两个模板参数,两种写法都可以。当只增加一个模板参数Arg时,我们设定operator*的返回值是Arg&,operator->的返回值为Arg*。这时如果list对象没有被const修饰,我们给Arg传int,则operator*的返回值是int&,operator->的返回值为int*;如果list对象被const修饰,则给Arg传const int,则operator*的返回值是const int&,operator->的返回值为const int*。

同时,这里将reverse_iterator<Iterator>重命名typedef成r_iterator的优势就出来了,所有通过r_iterator定义变量的位置都不必增加这个模板参数Arg,只有typedef的位置需要增加。

#pragma once
#include<iostream>
using namespace std;

namespace mine
{
	template<class Iterator,class Arg>
	class reverse_iterator
	{
		typedef reverse_iterator<Iterator,Arg> r_iterator;//注意typedef受到访问限定符限制
	public:
		reverse_iterator(const Iterator& it)
			:_it(it)
		{}

		//前置++,反向迭代器的++it就对应正向迭代器的--it
		r_iterator operator++()
		{
			return r_iterator(--_it);
		}

		//前置--,反向迭代器的--it就对应正向迭代器的++it
		r_iterator operator--()
		{
			return r_iterator(++_it);
		}

		//后置++,反向迭代器的it++就对应正向迭代器的it--
		r_iterator operator++()
		{
			r_iterator temp(_it);
			_it--;
			return temp;
		}

		//后置--,反向迭代器的it--就对应正向迭代器的it++
		r_iterator operator--(int)
		{
			r_iterator temp(_it);
			++_it;
			return temp;
		}
	private:
		Iterator _it;
	};

}

现在rbegin()是指向哨兵位,operator*解引用 *rbegin()时,岂不是要返回一个哨兵位中的无效数据?上图中反向迭代器rit迭代时,当迭代到rit==It.rend()就不进入循环去*rit了,这岂不是遍历到的数据为【无效数据、5、4、3、2】?

并不会发生这样的事情,我们会对operator*和operator->的逻辑做修改,代码如下。

#pragma once
#include<iostream>
using namespace std;

namespace mine
{
	template<class Iterator,class Arg>
	class reverse_iterator
	{		
	public: 

        Arg& operator*()
		{
			Iterator temp = _it;
			temp--;
			return *temp;
		}

		Arg* operator->()
		{
			//写法1
			//return &operator*();

			//写法2
			Iterator temp = _it;
			temp--;
			return &(*temp);
		}

	private:
		Iterator _it;
	};

}

拿下图来解释一下上面的代码,It是一个非const的list对象,当反向迭代器rit指向哨兵位时,解引用哨兵位并不会获取哨兵位节点的T类型的数据,而是获得哨兵位节点前一个节点的T类型的数据,也就是下图中的5;反向迭代器指向2时,解引用该节点时也不会获得2,而是获得2的前一个节点中的T类型的数据,也就是1;最后当反向迭代器指向节点1时,此时rit==It.rend(),不进入循环,也就不会对节点1的前一个节点即哨兵位节点解引用获得一个无效的T类型的数据了。所以通过这样的方式就可以遍历到【5、4、3、2、1】了。

operator=和operator!=

代码如下。 

#pragma once
#include<iostream>
using namespace std;

namespace mine
{
	template<class Iterator,class Arg>
	class reverse_iterator
	{
		typedef reverse_iterator<Iterator,Arg> r_iterator;//typedef受到访问限定符限制
	public:

		bool operator==(const r_iterator& rit)
		{
			return _it == rit._it;
		}

		bool operator!=(const r_iterator& rit)
		{
			return _it != rit._it;
		}
	private:
		Iterator _it;
	};

}

 

反向迭代器整体代码(包括对反向迭代器的一些总结)

反向迭代器走到这就就编写完毕了,将上文中的代码整合后,完整代码如下。这里注意我们的reverse_iterator被单独放在了头文件reverse_iterator.h中,不要忘记了,我们说过反向迭代器和正向迭代器不同, 反向迭代器是正向迭代器的适配器,因此我们编写的reverse_iterator不仅能支持list的反向迭代器,还能支持vector、string的反向迭代器,在下文中我们会对vector的反向迭代器作演示。

#pragma once
#include<iostream>
using namespace std;

namespace mine
{
	template<class Iterator,class Arg>
	class reverse_iterator
	{
		typedef reverse_iterator<Iterator,Arg> r_iterator;//typedef受到访问限定符限制
	public:
		reverse_iterator(const Iterator& it)
			:_it(it)
		{}

		//前置++,反向迭代器的++it就对应正向迭代器的--it
		r_iterator operator++()
		{
			return r_iterator(--_it);
		}

		//前置--,反向迭代器的--it就对应正向迭代器的++it
		r_iterator operator--()
		{
			return r_iterator(++_it);
		}

		//后置++,反向迭代器的it++就对应正向迭代器的it--
		r_iterator operator++(int)
		{
			r_iterator temp(_it);
			_it--;
			return temp;
		}

		//后置--,反向迭代器的it--就对应正向迭代器的it++
		r_iterator operator--(int)
		{
			r_iterator temp(_it);
			++_it;
			return temp;
		}

		Arg& operator*()
		{
			Iterator temp = _it;
			temp--;
			return *temp;
		}

		Arg* operator->()
		{
			//写法1
			//return &operator*();

			//写法2
			Iterator temp = _it;
			temp--;
			return &(*temp);
		}

		bool operator==(const r_iterator& rit)
		{
			return _it == rit._it;
		}

		bool operator!=(const r_iterator& rit)
		{
			return _it != rit._it;
		}
	private:
		Iterator _it;
	};

}

走到这里,再次理解反向迭代器的精华:只需要设计一个反向迭代器的类模板,然后只要实现了某个容器的正向迭代器,通过这个反向迭代器的类模板,我都能瞬间实现该容器的反向迭代器类,这就是反向迭代器的精髓。注意通过这样的方法实现一个容器的反向迭代器是有一个条件的,简而言之就是该容器的正向迭代器必须是一个双向迭代器,该容器的正向迭代器必须支持operator++和operator--,或者说必须支持operator--,因为没有迭代器不支持operator++,但有迭代器并不支持operator--,比如下图红框框起来的容器都不支持operator--,因此无法通过【反向迭代器适配它们的正向迭代器】这样的方法形成这些容器的反向迭代器,当然stack或者queue这些容器适配器压根没有迭代器,更不用谈反向迭代器了。

为什么正向迭代器必须支持operator--函数呢?很简单,因为反向迭代器的operator++函数需要调用正向迭代器的operator--函数。

为什么只要某容器的正向迭代器是双向迭代器,就能通过reverse_iterator这个类模板实例化出该容器的反向迭代器呢?

因为反向迭代器本质只是正向迭代器的一层封装,大部分逻辑和功能其实已经在正向迭代器中实现了,反向迭代器只是稍作修改,举个例子,我正向迭代器类已经实现完毕,即operator++、operator--、operator->、operator*、operator!=、operator==等等成员函数都已经实现了,反向迭代器的operator++只需要调正向迭代器的operator--就能实现;反向迭代器的operator==只需要调用正向迭代器实现的operator==就能实现,你看其实功劳都在正向迭代器上,也正是因为正向迭代器能够很好的支持它的成员函数,所以作为正向迭代器一层封装的反向迭代器当然也能很方便实现它该具有的功能。

既然说到了双向迭代器,咱们就顺便提一提迭代器的分类相关的知识点。

迭代器的分类

 

如上图所示,forward_iterator表示单向迭代器,因此不支持operator--();bidirectional_iterator表示双向迭代器,该迭代器虽然能++和--,但不能+2或者-2,也不能两个迭代器相加或者相减;random_access表示随机存取迭代器,除了具有双向迭代器的全部功能外,该迭代器还支持+2或者-2,也支持两个迭代器相加或者相减这样的操作。

常见容器的迭代器从功能的角度去分类时,分类况如下图所示。

如何得知一个容器的迭代器按照功能的角度去分类时是什么类别的呢?文档里的Member types中有。

知道这些从功能角度上分类过的迭代器后有什么用处呢?最常见的,很多算法会有它们的身影。比如文件<algorithm>中的reverve函数和sort函数,如下图,如果你不知道上面的知识点,你连下图红框中的类型是什么都不知道。

这里要说一下,虽然reverse的参数类型为双向迭代器,但你传随机存取迭代器给reverse也是可以的,因为随机存取迭代器具有双向迭代器的全部功能。有人可能会说,你这不是扯淡吗,如果没有发生类型转化,两种不同类型的变量之间还能互相赋值?你没说错,但这里它们其实是一种继承关系,相当于把派生类对象的切片赋值给了基类对象。同理,如果某函数的参数类型为单项迭代器类,那么此时可以传单项迭代器对象给它、也可以传双向迭代器对象给它、也可以传随机存取迭代器给它,这三种迭代器类之间也是有继承关系的,单项为祖父,双向为父亲,随机为孩子。

list的rbegin()和rend()

向外辐射了迭代器的分类的一些知识点后,咱们再回归正题。有了反向迭代器这个适配器后,我们就能编写list类的成员函数rbegin()和rend()接口了。

代码需要注意几点。

因为typedef受到访问限定符的限制,并且typedef的别名有时会需要在类外使用,所以必须在public中typedef重命名迭代器类。

关于typedef,这里还有一个很难发现的巨坑,如果你在给类模板reverse_iterator实例化出的类typedef起别名时typedef的顺序和下图中一样,那就会报错,错误原因也是奇奇怪怪,为什么会这样呢?

答案:因为在1号红框处,我们将reverse_iterator<iterator,T>重命名成了reverse_iterator,所以2号红框处的reverse_iterator就相当于reverse_iterator<iterator, T>了,所以2号红框的一行代码等同于typedef reverse_iterator<iterator, T><const_iterator, const T> const_reverse_iterator;所以语法上就错了。如何解决该问题呢?typedef时,先将2号红框的代码放到1号红框的代码之上就能解决该问题。

注意从下面的代码也能呼应上文,反向迭代器的rbegin()是正向迭代器的end(),反向迭代器的rend()是正向迭代器的rbegin()。

当const的list对象调用begin()或者rbegin()等接口时,必须分别返回const_iterator和const_reverse_iterator类型的迭代器,以此保证list上每个节点中的T类数据无法被修改,注意不要将它们和【const iterator、const reverse_iterator】弄混淆了,前两个意味着迭代器指向节点上的T类数据不能被修改,这是通过operator*和operator->的返回值保证的,而后者只能保证迭代器的指向不变,实际上就凭迭代器的指向不能变这一点,就可以认为在实际中根本不会定义出const iterator和const reverse_iterator类型的对象,因为迭代器不能迭代这不就扯了嘛,一般来说只有他俩的引用会有点用途,会被当作函数的形参被使用,以此减少拷贝,如形参类型为const iterator&或者const reverse_iterator&。

代码如下。

    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;

		typedef reverse_iterator<const_iterator, const T> const_reverse_iterator;

		typedef reverse_iterator<iterator, T> reverse_iterator;

        //方案1,使用别名
		/*const_iterator begin()const
		{
			return const_iterator(_head->_next);
		}*/

		//方案2,不使用别名
		list_iterator<T, const T&, const T*> begin()const
		{
			return list_iterator<T, const T&, const T*>(_head->_next);
		}

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

		//方案1,使用别名
		/*const_iterator end()const
		{	
			return const_iterator(_head);		
		}*/

		//方案2,不使用别名
		list_iterator<T, const T&, const T*>end()const
		{
			return list_iterator<T, const T&, const T*>(_head);
		}

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

		reverse_iterator rbegin()
		{
			return reverse_iterator(end());
		}

		const_reverse_iterator rbegin()const
		{
			return const_reverse_iterator(end());
		}

		reverse_iterator rend()
		{
			return reverse_iterator(begin());
		}

		const_reverse_iterator rend()const
		{
			return const_reverse_iterator(begin());
		}

开始测试反向迭代器

非const的list对象。

可以看到可以修改反向迭代器it指向节点上的T类数据。

const的list对象

可以看到不可以修改反向迭代器it指向节点上的T类数据。

vector的反向迭代器

上文中也说过,反向迭代器这种正向迭代器的适配器还可以适配vector的正向迭代器,从而形成vector的反向迭代器。

咱们来分析一下之前编写的反向迭代器这个适配器能否适配vector,并以此形成vector的反向迭代器类,当然上文中也揭示了答案,是可以的,但并没有进行解释,这里咱们来分析分析。

vector的begin()和end()接口返回的正向迭代器指向如上图的位置,根据咱们按照STL标准编写的反向迭代器的代码,现在反向迭代器的开始(begin)是正向迭代器的结束(end),反向迭代器的结束(end)是正向迭代器的开始(begin),我反向迭代器的起始位置和结束位置要和你正向迭代器倒着来,则rbegin()和rend()返回的反向迭代器分别指向如上图的位置,那么反向迭代器的operator*和operator->对vector适用吗?毕竟operator*解引用反向迭代器时不是获得当前迭代器指向元素的数据,而是获得当前迭代器指向位置的前一个位置的元素的数据,比如*rbegin()时获得5。

答案:可以发现是适用的,当有如下代码,通过反向迭代器反向遍历vector中的1、2、3、4、5时,因为*rbegin()获得5,所以*it开始就能获得5,it反向迭代到元素2时,*it能获得元素1,当it反向迭代器到元素1时,因为此时it==v1.rend(),不满足循环条件,因此不会*it获得元素1前面一个位置的无效数据

接下来给vector增加rbegin()等接口,代码如下。

#pragma once
#include<iostream>
#include<cassert>
#include"reverse_iterator.h"
using namespace std;


namespace mine
{
	template<class T>
	class vector
	{
	public:
		typedef T* iterator;
		typedef const T* const_iterator;
		typedef reverse_iterator<const_iterator, const T> const_reverse_iterator;
		typedef reverse_iterator<iterator, T> reverse_iterator;

        const_iterator begin()const//该接口也可以叫cbegin
		{
			return _start;
		}

		iterator begin()
		{
			return _start;
		}

		const_iterator end()const
		{
			return _finish;
		}

		iterator end()
		{
			return _finish;
		}

		reverse_iterator rbegin()
		{
			return reverse_iterator(end());
		}

		reverse_iterator rend()
		{
			return reverse_iterator(begin());
		}

		const_reverse_iterator rbegin()const
		{
			return const_reverse_iterator(end());
		}

		const_reverse_iterator rend()const
		{
			return const_reverse_iterator(begin());
		}
		
	private:
		iterator _start;//指向第一个元素位置的迭代器。
		iterator _finish;//指向最后一个元素后一个位置的迭代器。
		iterator _end_of_storage;//指向容器最大容量的后一个位置的迭代器。
	};

}

给vector新增rbegin()等接口后,就可以开始测试了,测试代码如下图。 

走到这里,可以看到通过正向迭代器适配出的反向迭代器是具有一个反向迭代器该具有的功能的。为什么会这样呢?或者从根本问题上,为什么只要某容器的正向迭代器是双向迭代器,就能通过reverse_iterator这个类模板实例化出该容器的反向迭代器呢?

其实走到这里,这是对该问题的第二次解答,之前可能理解不太深刻,所以这里我们以vector举例再次理解一遍。因为反向迭代器本质只是正向迭代器的一层封装,大部分逻辑和功能其实已经在正向迭代器中实现了,反向迭代器只是稍作修改,举个例子,vector的正向迭代器类已经实现完毕,即operator++、operator--、operator->、operator*、operator!=、operator==等等成员函数都已经实现了。有人说不对啊,我没写过啊。笨!vector的正向迭代器类就是typedef后的原生指针类,所以这些成员函数都不必自己实现,指针自己就支持了它们。反向迭代器的operator++只需要调用正向迭代器的operator--就能实现;反向迭代器的operator==只需要调用正向迭代器实现的operator==就能实现,你看其实功劳都在正向迭代器上,也正是因为正向迭代器能够很好的支持它的成员函数,所以作为正向迭代器一层封装的反向迭代器当然也能很方便实现它该具有的功能。

list的其他成员函数

1.insert以及复用insert实现的push_back,push_front

    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;
		list()
		{
			_head = new node;
			_head->_next = _head;
			_head->_prev = _head;
		}
	
		iterator end()
		{
			return iterator(_head);
		}

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

		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;*/

			insert(end(), x);
		}
        
        //在pos位置插入值
		iterator insert(iterator pos, const T& x)
		{
			node* cur = pos._n;
			node* prev = (pos._n)->_prev;
			node* newnode = new node(x);

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

			return iterator(newnode);
		}
		void push_front(const T& x)
		{
			//方式1
			//insert(++end(), x);

			//方式2
			insert(begin(), x);
		}

	private:
		node* _head;
	};

对于insert的实现,因为我们实现的是带哨兵位的双向循环链表,因此是不可能存在nullptr空节点的,所以可以放心连接前后节点。

注意STL规定insert插入新节点后,是需要返回指向新节点的迭代器的,因此咱们模拟实现也按照规则来。

有了insert后,咱们可以复用它实现push_back和push_front,代码如上所示,逻辑图如下所示。

2.erase以及复用erase实现的pop_back,pop_front

    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;
		
        list()
		{
			_head = new node;
			_head->_next = _head;
			_head->_prev = _head;
		}

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


		iterator end()
		{
			return iterator(_head);
		}
		
		iterator erase(iterator pos)// x 2 3
		{
			assert(pos != end());
			node* prev = pos._n->_prev;
			node* next = pos._n->_next;
			prev->_next = next;
			next->_prev = prev;
			delete pos._n;
			return iterator(next);
		}

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

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

	private:
		node* _head;
	};

3.拷贝构造(深拷贝)和迭代器区间构造

编译器默认生成的拷贝构造所存在的问题

和vector一样,模拟实现list时,如果不自己编写深拷贝的拷贝构造而使用默认生成的拷贝构造,此时也会出现2种问题。

因为编译器默认生成的拷贝构造对内置类型完成值拷贝,对自定义类型去调用自定义类型的拷贝构造完成拷贝,我们list里只有一个内置类型的node*_n成员,因此当ls2(ls1)调用默认生成的拷贝构造就只是将ls1的_n成员的值拷贝给ls2的_n成员,这就导致两个链表的_n成员指向同一个哨兵位节点,如下图左半部分2个红框处的地址相同。所以问题1就是链表销毁前调用析构函数释放节点所占空间时,会对同一个节点释放两次。前面也说了两个链表的哨兵位是同一个,相当于两条链表管理的是同一堆节点,所以问题2就是修改ls1的节点上的数据时,ls2的节点上的数据也跟着被修改了,如下图右半部分代码所示。

注意上图代码能正常运行只是因为目前没有实现析构函数释放node节点所占的空间,导致不会释放野指针导致程序崩溃,但会导致内存泄漏,所以虽然能正常运行,但并不代表代码没有问题。

接下来开始编写拷贝构造

和vector一样,list的拷贝构造也有现代写法和传统写法,我们更推荐现代写法, 所以先来看看如何实现。

首先,因为我们需要一个打工人帮我们构造出临时的list对象,然后后序才能swap交换,因此得先实现一个list的带参的构造函数,我们选择【通过迭代器区间构造list】这个构造函数,如下面代码所示。

类模板中的函数都是函数模板,但当有需求时,我们可以给函数模板再添加模板参数,可以看到我们给【通过迭代器区间构造list】这个构造函数增加了模板参数InputIterator,表示这个迭代器的类型可以是任意容器的迭代器类型,比如vector的迭代器,list的迭代器,string的迭代器等等都可以。

    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;
		
        list()
		{
			_head = new node;
			_head->_next = _head;
			_head->_prev = _head;
		}

		template<class InputIterator>
		list(InputIterator first, InputIterator last)
		{
			//在push_back插入迭代器区间的元素前,得先把list的哨兵位节点构造出来,不然插入时会解引用野指针报错
			_head = new node;
			_head->_next = _head;
			_head->_prev = _head;
			//开始插入
			while (first != last)
			{
				push_back(*first);
				first++;
			}
		}

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

		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;*/

			insert(end(), x);
		}

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

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

			return iterator(newnode);
		}		

	private:
		node* _head;
	};

有了【通过迭代器区间构造list】这个构造函数后,编写现代写法的拷贝构造就简单了,代码如下所示。

现代写法的原理:首先要知道一点,所有容器管理的资源,比如list中管理的节点,vector中存储的元素,这些资源都是new出来的,在堆上的数据。现代写法就是通过复用其他的构造函数,让它去帮我们创建这些资源,然后接管它们。举个例子,假如有ls1(ls2),则函数体内先通过ls2的迭代器区间,去构造一个临时list对象temp,temp已经new创建了和ls2管理的一模一样的资源,最后让ls1接管temp的资源,即让ls1和temp互相交换哨兵位节点的值,此时就完成了ls1对ls2的深拷贝。同时因为临时的list对象temp是在函数体内定义的,出了作用域会销毁,而在销毁前会调用析构函数释放temp管理的所有节点,因为此时swap语句已经执行,所以此时temp管理的节点就是ls1和temp交换前ls1所管理的节点,所以temp对象销毁前还会释放这些节点,避免内存泄漏。

所以temp对象帮我们打了两份工,第一份工是让我ls1完成了对ls2的深拷贝,第二份工是让temp对象帮我释放了swap语句执行前ls1所管理的所有节点,避免内存泄漏,因为当前情景是在构造函数中构造ls1,在ls1和temp对象swap前,ls1管理的只有一个哨兵位节点,所以通过temp对象销毁释放的节点也只有一个,就是ls1的哨兵位节点。以此可见ls1这个老板多黑心,我不仅要榨取获得你temp的资源,我还要你temp帮我ls1清理属于我ls1的垃圾资源。

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;
		list()
		{
			_head = new node;
			_head->_next = _head;
			_head->_prev = _head;
		}

		template<class InputIterator>
		list(InputIterator first, InputIterator last)
		{
			//在插入迭代器区间的元素前,得先把list的哨兵位节点构造出来
			_head = new node;
			_head->_next = _head;
			_head->_prev = _head;
			//开始插入
			while (first != last)
			{
				push_back(*first);
				first++;
			}
		}

        //拷贝构造的现代写法
		list(const list<T>& ls)
		{
			//在swap交换哨兵位前,得先把list的哨兵位节点构造出来,否则解引用野指针导致程序崩溃
			_head = new node;
			_head->_next = _head;
			_head->_prev = _head;

			list<T>temp(ls.begin(), ls.end());
			std::swap(_head, temp._head);
		}

      

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

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

		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;*/

			insert(end(), x);
		}

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

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

			return iterator(newnode);
		}

	private:
		node* _head;
	};

如上面代码所示,我们现代写法的拷贝构造中和临时的list交换时用的是std的swap,但因为list本来也要提供一个成员函数swap,所以我们可以先实现一个成员函数swap,然后在拷贝构造中复用swap成员函数完成和临时对象的交换,如下图所示。

4.赋值运算符函数operator= 

代码如下,我们实现的operator=也是现代写法的版本,原理和拷贝构造的现代写法的原理完全相同,假如此时有ls1=ls2,ls1这个老板不仅要榨取获得你temp的资源,我还要你temp帮我ls1清理属于我ls1的垃圾资源,只不过对比拷贝构造,在operator=函数里temp需要帮ls1清理的垃圾资源一般会更多,因为在operator=中,ls1和temp在swap前ls1管理的节点一般不止只有哨兵位节点一个(当然也有可能是只有哨兵位一个,但概率低),而在拷贝构造中,ls1和temp在swap前ls1管理的节点一定只有一个哨兵位节点。

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;
		
        list()
		{
			_head = new node;
			_head->_next = _head;
			_head->_prev = _head;
		}

		template<class InputIterator>
		list(InputIterator first, InputIterator last)
		{
			//在插入迭代器区间的元素前,得先把list的哨兵位节点构造出来
			_head = new node;
			_head->_next = _head;
			_head->_prev = _head;
			//开始插入
			while (first != last)
			{
				push_back(*first);
				first++;
			}
		}

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

		list(const list<T>& ls)
		{
			//在swap交换哨兵位前,得先把list的哨兵位节点构造出来,否则解引用野指针导致程序崩溃
			_head = new node;
			_head->_next = _head;
			_head->_prev = _head;

			list<T>temp(ls.begin(), ls.end());
			swap(temp);
		}

		//operator=()的现代写法1
		list<T>& operator=(const list<T>& ls)
		{
			list<T>temp(ls.begin(), ls.end());
			swap(temp);
			return *this; 
		}

		//更精简版本的operator=()的现代写法2
		/*list<T>& operator=(list<T> ls)
		{
			swap(ls);
			return *this;
		}*/
	
		iterator begin()
		{
			return iterator(_head->_next);
		}	

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

		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;*/

			insert(end(), x);
		}

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

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

			return iterator(newnode);
		}

	private:
		node* _head;
	};

5.析构函数和clear()

    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 iterator(_head);
		}		
		
		iterator erase(iterator pos)// x 2 3
		{
			assert(pos != end());
			node* prev = pos._n->_prev;
			node* next = pos._n->_next;
			prev->_next = next;
			next->_prev = prev;
			delete pos._n;
			return iterator(next);
		}

		void clear()
		{
			iterator first = begin();
			while (first != end())
			{
				first=erase(first);
                //这里不能first++找到下一个元素,因为first指向的节点已经被erase销毁了,调用++会解引用野指针报错
			}
		}
		~list()
		{
			clear();
			delete _head;
		}


	private:
		node* _head;
	};

析构和erase的代码如上面所示。这里说一下,他俩是有区别的,clear()会删除除了哨兵位以外的所有节点,而析构函数是list对象要销毁了,在list对象销毁前把所有节点包括哨兵位节点的空间释放。

在类的内部,有时可以不写模板参数

有时你可以发现一些奇怪的现象,如上图红框处,明明是类模板,但却能省略模板参数,把const list<T>& x写成const list&x,把list<T>&x写成list&x,那么这样是正确的吗?

是正确的写法,在类中,如果有对象的类型是个类模板,这个类模板的模板参数是可以省略不写的。但一定要注意一点,只有在类内部才可以省略,可以认为这是编译器做的特殊处理,因此在类外是绝对不能省略的。

如下面代码,两种swap的写法都是可以编译通过的。

    template<class T>
	class list
	{
		typedef list_node<T> node;
	public:
		
        //不省略模板参数的写法
		void swap(list<T>& ls)
		{
			std::swap(_head, ls._head);
		}
        
        //省略模板参数的写法
		void swap(list& ls)
		{
			std::swap(_head, ls._head);
		}

	private:
		node* _head;
	};

list类的整体代码(可复制)

文件reverse_iterator.h的代码如下。因为reverse_iterator是一个适配器,能够形成多种不同容器的反向迭代器,因此咱们单独放在一个文件里,供其他容器也能使用。

#pragma once
#include<iostream>
using namespace std;

namespace mine
{
	template<class Iterator,class Arg>
	class reverse_iterator
	{
		typedef reverse_iterator<Iterator,Arg> r_iterator;//typedef受到访问限定符限制
	public:
		reverse_iterator(const Iterator& it)
			:_it(it)
		{}

		//前置++,反向迭代器的++it就对应正向迭代器的--it
		r_iterator operator++()
		{
			return r_iterator(--_it);
		}

		//前置--,反向迭代器的--it就对应正向迭代器的++it
		r_iterator operator--()
		{
			return r_iterator(++_it);
		}

		//后置++,反向迭代器的it++就对应正向迭代器的it--
		r_iterator operator++(int)
		{
			r_iterator temp(_it);
			_it--;
			return temp;
		}

		//后置--,反向迭代器的it--就对应正向迭代器的it++
		r_iterator operator--(int)
		{
			r_iterator temp(_it);
			++_it;
			return temp;
		}

		Arg& operator*()
		{
			Iterator temp = _it;
			temp--;
			return *temp;
		}

		Arg* operator->()
		{
			//写法1
			//return &operator*();

			//写法2
			Iterator temp = _it;
			temp--;
			return &(*temp);
		}

		bool operator==(const r_iterator& rit)
		{
			return _it == rit._it;
		}

		bool operator!=(const r_iterator& rit)
		{
			return _it != rit._it;
		}
	private:
		Iterator _it;
	};

}

文件list.h的代码如下。

#pragma once
#include<iostream>
#include<cassert>
#include"reverse_iterator.h"
using namespace std;

namespace mine
{

	template<class T>
	class list_node
	{
	public:
		list_node()
			:_next(nullptr)
			, _prev(nullptr)
			, _data()
		{}

		list_node(const T& x)
			:_next(nullptr)
			, _prev(nullptr)
			, _data(x)
		{}

		list_node<T>* _next;
		list_node<T>* _prev;
		T _data;
	};

	


	template<class T,class Ref,class Ptr>
	class list_iterator
	{
		typedef list_node<T> node;
	public:
		node* _n;

		list_iterator(node*n)
			:_n(n)
		{}

		bool operator!=(list_iterator<T,Ref,Ptr>it)const
		{
			return _n != it._n;
		}
	
		//若有 list_iterator<T>it,则 *it完全等价于 it.operator*()
		Ref operator*()
		{
			return (*_n)._data;
		}
		//++it
		list_iterator<T, Ref, Ptr>& operator++()
		{
			_n = _n->_next;
			return *this;
		}
		//it++
		list_iterator<T, Ref, Ptr> operator++(int)
		{
			list_iterator<T,Ref, Ptr>temp(*this);
			_n = _n->_next;
			return temp;
		}
		//--it
		list_iterator<T, Ref, Ptr>& operator--()
		{
			_n = _n->_prev;
			return *this;
		}

		//it--
		list_iterator<T, Ref, Ptr> operator--(int)
		{
			list_iterator<T, Ref, Ptr >temp(*this);
			_n = _n->_prev;
			return temp;
		}

		Ptr operator->()
		{
			return &(_n->_data);
		}

	};

	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;

		typedef reverse_iterator<const_iterator, const T> const_reverse_iterator;

		typedef reverse_iterator<iterator, T> reverse_iterator;

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

		template<class InputIterator>
		list(InputIterator first, InputIterator last)
		{
			//在插入迭代器区间的元素前,得先把list的哨兵位节点构造出来
			_head = new node;
			_head->_next = _head;
			_head->_prev = _head;
			//开始插入
			while (first != last)
			{
				push_back(*first);
				first++;
			}
		}

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

		list(const list<T>& ls)
		{
			//在swap交换哨兵位前,得先把list的哨兵位节点构造出来,否则解引用野指针导致程序崩溃
			_head = new node;
			_head->_next = _head;
			_head->_prev = _head;

			list<T>temp(ls.begin(), ls.end());
			swap(temp);
		}

		//operator=()的现代写法1
		/*list<T>& operator=(const list<T>& ls)
		{
			list<T>temp(ls.begin(), ls.end());
			swap(temp);
			return *this; 
		}*/

		//更精简版本的operator=()的现代写法2
		list<T>& operator=(list<T> ls)
		{
			swap(ls);
			return *this;
		}

		//方案1,使用别名
		/*const_iterator begin()const
		{
			return const_iterator(_head->_next);
		}*/

		//方案2,不使用别名
		list_iterator<T, const T&, const T*> begin()const
		{
			return list_iterator<T, const T&, const T*>(_head->_next);
		}


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

		//方案1,使用别名
		/*const_iterator end()const
		{	
			return const_iterator(_head);		
		}*/

		//方案2,不使用别名
		list_iterator<T, const T&, const T*>end()const
		{
			return list_iterator<T, const T&, const T*>(_head);
		}

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

		reverse_iterator rbegin()
		{
			return reverse_iterator(end());
		}

		const_reverse_iterator rbegin()const
		{
			return const_reverse_iterator(end());
		}

		reverse_iterator rend()
		{
			return reverse_iterator(begin());
		}

		const_reverse_iterator rend()const
		{
			return const_reverse_iterator(begin());
		}

		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;*/

			insert(end(), x);
		}

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

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

			return iterator(newnode);
		}
		void push_front(const T& x)
		{
			//方式1
			//insert(++end(), x);

			//方式2
			insert(begin(), x);
		}

		iterator erase(iterator pos)// x 2 3
		{
			assert(pos != end());
			node* prev = pos._n->_prev;
			node* next = pos._n->_next;
			prev->_next = next;
			next->_prev = prev;
			delete pos._n;
			return iterator(next);
		}

		void clear()
		{
			iterator first = begin();
			while (first != end())
			{
				first=erase(first);
			}
		}
		~list()
		{
			clear();
			delete _head;
		}


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

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

	private:
		node* _head;
	};

}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值