list类

list类

list类的难点在于对迭代器的理解,所以其他链表相关的接口,与学习数据结构时的做法基本一致,不再赘述

节点:

	struct list_node
	{
		list_node(const T&val = T())
			:_val(val)
			,_prev(nullptr)
			,_next(nullptr){}
		
		T _val;
		list_node<T>* _prev;
		list_node<T>* _next;
	};

list类:

template<class T>
	class list
	{
		typedef list_node<T> Node;
	public:
		list() :_head(new Node()) { _head->_prev = _head->_next = _head; }

		void push_back(const T& val)
		{
			Node* newnode = new Node(val);
			Node* tail = _head->_prev;
			tail->_next = newnode;
			newnode->_prev = tail;
			newnode->_next = _head;
			_head->_prev = newnode;
		}

		void pop_back()
		{
			Node* tail = _head->_prev;
			tail->_prev->_next = _head;
			_head->_prev = tail->_prev;
			delete tail;
		}

	private:
		Node* _head;
	};

	void test1()
	{
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);
		lt.push_back(5);
		lt.push_back(6);
		lt.pop_back();
		lt.pop_back();
		lt.pop_back();
		lt.pop_back();
	}

如上,是一个实现了尾插和尾删的链表,实现了基本的插入,如果我们想要遍历和访问链表,就需要迭代器。

对于 string 和 vector,原生的指针就够用了。但是对于list是无法使用原生指针的。

举个例子:

如果我们使用原生指针,然后对节点解引用:

typedef list_node<int> Node
Node* p = new Node(10);
*p //错误
image-20220918082428365

每个Node类中都有三个数据,编译器不知道 *p 要的是哪个数据。

换句话说,我们没有重载 operator*(),明确地告诉编译器,我要的是Node里面的哪个数。


封装,就是在已有事物的基础上,扩展它的功能,原生的节点指针用不了,就对节点指针进行封装,重载运算符。

这也就是为什么list不使用原生指针,而是创造迭代器类,封装节点指针

list_iterator类(迭代器)

list<int> lt;
auto it = lt.begin();
while(it != lt.end())
{
    cout << *it << " ";
    ++it
}

如上代码,为了实现这份代码,我们需要:

  • 创建list_iterator类、begin函数 和 end 函数
  • it != lt.end() :重载迭代器比较
  • *it :重载operator *
  • ++it :重载operator++

list_iterator类:

template<class T>
	struct __list_iterator
	{
		typedef list_node<T> Node;
		__list_iterator<T>(Node* node) :_node(node){}
		typedef __list_iterator<T> iterator;
		
		T& operator*()  
		{
			return _node->_val;
		}

        iterator& operator++() //前置++
		{
			_node = _node->_next;
			return *this;
		}
        
		bool operator==(const iterator& cmp)
		{
			return _node == cmp._node;
		}
		
		bool operator!=(const iterator& cmp)
		{
			return !(*this == cmp);
		}
	private:
		Node* _node;
	};
class list
	{
		typedef list_node<T> Node;
	public:
    	typedef __list_iterator<T> iterator;
		list() :_head(new Node()) { _head->_prev = _head->_next = _head; }
		
    	iterator begin()
        {
            return iterator(_head->_next);//把第一个节点封装成迭代器,然后返回
        }
    	
    	iterator end()
        {
            return iterator(_head); //把头节点封装成迭代器,然后返回
        }

	private:
		Node* _head;
	};

样例:

list<int> lt;
auto it = lt.begin();
while(it != lt.end())
{
    *it *= 2; //可修改节点value值
    cout << *it << " ";
    ++it
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vL60Vjco-1679755761151)(C:/Users/Z-zp/AppData/Roaming/Typora/typora-user-images/image-20220918085924327.png)]


const迭代器

使用const迭代器,我们希望节点value值无法被修改

template<class T>
void const_print(const list<T>& lt)
{
    list::const_iterator it = lt.begin();
    while (it != lt.end())
    {
        *it += 2;
        cout << *it << " ";
        ++it;
    }
}

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

最重要的就是,operator*的返回值必须被const修饰,这样就能达到无法修改节点值的效果了。

但是要怎么实现?

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

const T& operator*()  //错误,不构成重载
{
    return _node->_val;
}

第一种方法就是再创建一个类,类名可以叫 const_list_iterator(叫什么其实不重要,因为最后都是直接一个typedef把名字改了,重要的是实现。

template<class T>
sturct const_list_iterator
{
    typedef const_list_iterators<T> iterator;
   	....
        
    const T& operator*()
    {
        return _node->val;
	}
    ...
     
    Node* node;
};

这样当然是可行的,但是这样,该类的接口与 list_iterator类的接口实现基本一致,无非是多了个const。

代码复用着实有点差。

因此,参照STL源码中list的实现,我们有第二种方法,在了解这种方法之前,我们必须确认对于模板足够了解。

模板是一种泛型编程,使用它意味着你将要描述的东西是模糊的、多样的、不确定的

因此,不要局限于参数或者类的成员变量,只要是模糊的数据,都应该想到模板!

带着这样的想法,我们再返回去看看,我们需要的是:

operator 的返回值,可能是T&,也可能是const T&*,这取决于我们定义的 list 使用的是什么样的迭代器。

想到了吗,operator*的返回值就是模糊的、不确定的,所以我们何不给返回值设置一个模板呢?

template<class T,class Ref>
struct __list_iterator
{
    typedef list_node<T> Node;
    __list_iterator<T, Ref>(Node* node) :_node(node) {}
    typedef __list_iterator<T, Ref> iterator;
    
    Ref operator*()  //返回值为模板参数
    {
        return _node->_val;
    }
}
template<class T>
	class list
	{
		typedef list_node<T> Node;
	public:
	typedef __list_iterator<T, T&> iterator; 
	typedef __list_iterator<T, const T&> const_iterator;  //const迭代器,就传带const参数
    list() :_head(new Node()) { _head->_prev = _head->_next = _head; }

		iterator begin()
		{
			return iterator(_head->_next);//把第一个节点封装成迭代器,然后返回
		}

		iterator end()
		{
			return iterator(_head); //把头节点封装成迭代器,然后返回
		}

		const_iterator begin()const 
		{
			return const_iterator(_node->_next);//用const迭代器封装
		}

		const_iterator end()const
		{
			return const_iterator(_node); //用const迭代器封装
		}


	private:
		Node* _head;
	};

最后当我们再次编译这段代码时,编译器报错就不允许我们给常量赋值了:

template<class T>
	void const_print(const list<T>& lt)
	{
		auto it  = lt.begin();
		while (it != lt.end())
		{
			*it += 2; //错误
			cout << *it << " ";
			++it;
		}
	}

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ibcipd5U-1679755761152)(C:/Users/Z-zp/AppData/Roaming/Typora/typora-user-images/image-20220918101744399.png)]

这里还有一些坑,是我自己在学习的时候踩的:

原本当我测试这段代码的时候,我是没有另写一个const_print函数去调用的,也就是这样开头:

void test2()
{
    const list<int> lt;
}

但当我试图这样做的时候,很快发现这是没有意义的,虽然语法上是允许的。但是我们在定义的时候就将list以

const修饰,也就是在list一个结点都没有的时候,将它限死不能改变,那既然一个数据都无法保存,创建list的意义在哪?这显然是不合理的。

于是我开始这样写:

void test2()
	{
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);
		list<int>::const_iterator it = lt.begin(); 
	}

但我发现它仍然不行,因为 lt 没有被const修饰,所以进去调的时候实际上是这样的:

image-20220918103135628

并且返回值也匹配不上,事实上编译器也给了报错:

image-20220918103228866

这都是对类的认识不足导致的,实际上我们只能确实只能这样使用:

template<class T>
	void const_print(const list<T>& lt)
	{
		auto it  = lt.begin();
		while (it != lt.end())
		{
			*it += 2; //错误
			cout << *it << " ";
			++it;
		}
	}

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

本质上是因为我们无法控制this指针的传递(无法修饰this),因为这根本不是我们在做的事,是编译器干的。

所以我们只能调用函数的时候传递const list,这时候list中也已经有节点了,满足对数据的存储。

然后又因为是 const list,编译器在调用函数的时候发现这是一个 const list,就会给this指针加上const。


迭代器重载operator-> + 迭代器的第三个模板参数

首先要明白什么时候我们会使用 ->

int x = 10;
int *p = &x;
cout << *p;
struct Date
{
    Date(int year,int month,int day):_year(year),_month(month),_day(day){}
    int _year;
    int _month;
    int _day;
}
Date *d = new Date();
d->_year = 2000;
d->_month = 10;
(*d)._day = 5;  //也可以这样访问,但相比起来,->会更简洁明了

当我们要访问的数据在一个类(或结构体)中,如果我们拿到了指向这个类的指针p,

那么访问该类的数据,就可以使用 ->


同理,如果list中保存的是内置类型数据(int、double…),自然用不到 ->

但是如果这样保存数据:

list<Date> lt;
lt.push_back(Date(2022,10,1));

我们拿到的节点数据是一个结构体,我们要访问的是结构体里的数据,这时候->就有用到的地方了。


因此我们要进一步完善迭代器,同样的,在重载operator->时

  • 对于普通迭代器,我们允许它修改数据,

  • 对于const迭代器,我们不允许它修改数据。

能否修改数据,取决于返回值是否以const修饰,所以,返回值是不确定的、模糊的,因此又需要用到模板。

template<class T, class Ref, class Ptr>   //第三个模板参数
struct __list_iterator
{
    typedef list_node<T> Node;
    __list_iterator<T, Ref, Ptr>(Node* node) :_node(node) {}
    typedef __list_iterator<T, Ref, Ptr> iterator;
    
    Ptr operator->()  
    {
        return &(_node->val);
    }
    
private:
	Node* _node;
};

注意:**operator->的返回值一定必须设置成一个地址。**这样当我们调用的时候就会变成这样

list<Date> lt;
auto it = lt.begin();
it->->_year;  //两个箭头
(*it)._year; //也可以这样写,但不够简洁

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qsQ4ltFr-1679755761152)(C:/Users/Z-zp/AppData/Roaming/Typora/typora-user-images/image-20220918122801142.png)]

第一个箭头拿到节点数据的地址——一个类或结构体的地址,第二个箭头拿到这个类里的数据。

但两个箭头看着很奇怪,所以编译器对这里做了优化,只要我们只需要给一个箭头就能表示获取数据:

it->_year;

那可能就有人要这样问了:取节点value的地址返回,然后在外面使用相当于 (&node->val)->

不就相当于默认了节点保存的是自定义类型(结构体)?是的,就是默认了

那如果val是一个int类型的数据,不就变成了 (&int) -> 可是int不像结构体有多个数据,只有一个,不会出错吗?

是的会出错,本来也就不允许这样使用,因为也没地方指,指向谁?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vx5jt9yE-1679755761153)(C:/Users/Z-zp/AppData/Roaming/Typora/typora-user-images/image-20220918123209909.png)]

所以对于内置类型,没有人会这么用 -> 箭头

我们敢operator->返回地址的底气也就在于:

-> 使用的前提就是类(结构体),节点值保存的一定是个自定义类型(结构体),

否则,对于内置类型根本不会用到 ->,从语法层面就是错的。


template<class T, class Ref>
struct __list_iterator
{
    typedef list_node<T> Node;
    __list_iterator<T, Ref, Ptr>(Node* node) :_node(node) {}
    typedef __list_iterator<T, Ref, Ptr> iterator;

private:
	Node* _node;
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿波呲der

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值