从list的模拟实现中了解迭代器的设计方式

欢迎来到博主的专栏:c++杂谈

博主ID:代码小豪


文章目录

迭代器------容器与算法的桥梁

如果你尝试使用过STL,那么一定对迭代器不感到陌生,迭代器作为STL六大组件之一,是连接算法与容器之间的桥梁,其重要性不言而喻。

相信大家在刚接触STL的时候都会对这个问题感到困惑,那就是为什么STL中的算法可以适用于不同的容器?

这其实是源自于学习C语言和c++的类与对象部分这一时期的惯性思维,那就是函数的参数和对象的类型一定要匹配,比如作用于某个类的函数,我们就要根据这个类的行为来设计函数。这就导致了STL中的某些算法看起来是不合理的,比如为什么find可以作用于vector,也能作用于list,这两个容器连数据结构都不一样,为什么能有同样的效果?

这件事情就好比,将数学上将数字和几何学结合起来一样难以理解。但是数学上还真有一个东西将数字和几何学结合起来了,那就是函数图像。我们可以尝试理清一下函数图像是如何将集合和数字结合在一起的。一个数字,一个集合,明明是八竿子打不着的关系,就像list和vector一样。其实原理也很简单,其实只需要有一个东西作为这两者之间的桥梁一样,这便是坐标系。有了坐标系,我们就能通过函数来分析几何(圆的函数),也能通过几何来分析函数(微积分)。

那么迭代器和容器和算法的关系也是如此,有了迭代器,我们就能让算法使用在不同的容器上。那么迭代器又是怎么做到的呢?别急,我们慢慢看

容器与迭代器

相信大家用迭代器遍历整个容器应该是再熟悉不过的操作了吧。我们尝试分别用list、vector的迭代器遍历各自的容器。

cpp 复制代码
	void TestIterator()
	{
		vector<int> v1{ 1,2,3,4,5,6,7,8,9,10 };
		list<int> list1{ 1,2,3,4,5,6,7,8,9,10 };

		vector<int>::iterator vi = v1.begin();//v1的迭代器
		list<int>::iterator li = list1.begin();//list1的迭代器

		while (vi != v1.end()){
			cout << *vi;//{1,2,3,4,5,6,7,8,9,10}
			vi++;//让迭代器访问下一个元素
		}

		while (li != list1.end()) {
			cout << *li;//{1,2,3,4,5,6,7,8,9,10}
			li++;//让迭代器访问下一个元素
		}
	}

我们观察上面的代码,可以发现一个有意思的事情,那就是无论是list还是vector的迭代器,其都支持++,和*的操作。实际上,STL中的迭代器都会支持以下操作

操作符 执行操作
++ 让迭代器来到下一个元素
-- 让迭代器来到上一个元素
* 访问迭代器中的元素
-> 访问迭代器中的元素的成员

这就发现一个很有意思的现象了,那就是无论STL中各个容器的数据结构和成员有多么的不一样,我们都能用相同的方法访问到容器中的元素,那就是通过迭代器。

算法与迭代器

我们以find()为例,在c++标准中,find()函数的声明如下:

cpp 复制代码
template <class InputIterator, class T>
   InputIterator find (InputIterator first, 
   InputIterator last,
    const T& val);

在调用find()函数时,我们需要传入同一容器中的两个迭代器,还有一个代表迭代器中的值。其中T是解引用迭代器得到的值的类型。其作用为:在[first,find)之间找到val这个元素,并返回val元素的迭代器,如果没有找到,就返回last

我们尝试使用一下这个函数:

cpp 复制代码
	void TestFind()
	{
		vector<int> v1{ 1,2,3,4,5,6,7,8,9,10 };
		list<int> list1{ 1,2,3,4,5,6,7,8,9,10 };

		vector<int>::iterator viter;//v1的迭代器
		list<int>::iterator liter;//liter的迭代器

		viter=find(v1.begin(), v1.end(), 4);
		liter=find(list1.begin(), list1.end(), 11);

		if (viter == v1.end())
			cout << "not find" << endl;
		else
			cout << "find " << *viter<<endl;

		if (liter == list1.end())
			cout << "not find" << endl;
		else
			cout << "find" << *liter<<endl;
	}

运行结果如下:在v1容器中找到了4,并且返回了4这个位置的迭代器,在list1中并没有找到11,于是返回了list1.end(),说明没有找到这个元素。

如果我没有学习过STL,我会感到非常好奇的点是:list和vector在底层上是两个截然不同的数据结构,list的链表,而vector是顺序表,链表的遍历方式是通过节点遍历,而顺序表的遍历方式则是通过下标遍历。链表对于元素访问的方式是读取节点的值,而顺序表对于元素访问的方式则是去下标。但是在上述的两个例子当中,各自的迭代器竟然用相同的方式就完成了这些任务。

这里博主尝试模拟实现一个find(),希望方便读者进行理解:

cpp 复制代码
template <class InputIterator, class T>
InputIterator myfind(InputIterator first,
    InputIterator last,
    const T& val)
{
    while (first != last)
    {
        if (*first == val)
            return first;
        first++;
    }
    return last;
}

从这个模拟实现我们不难看出一个点,那就是STL中的算法不需要考虑容器的底层到底是什么数据结构,也不需要考虑这个数据结构遍历元素时需要用到什么算法,只需要用户能将容器的迭代器(无论是什么容器)传入函数中,通过对迭代器的一致操作(所有的迭代器都会的操作,比如*)进行操作即可。于是这个函数就变成了能用于各个容器的通用算法。

迭代器

我们讲了这么多有关于迭代器和容器,迭代器和算法之间的各个作用方式,但是想要了解迭代器,最好的方法还是从迭代器开始入手。博主在此之前曾写过list的模拟实现,但是这个博客有一点我很不满意,那就是关于list的迭代器被简单提到就一笔带过了,主要原因还是我想在list的模拟实现中展示更多的是容器的设计方式,而非迭代器。因此在完成那篇博客之后,我为list的迭代器写了这篇博客。

以list为例,我尝试为其设计一个迭代器。先来看看list中大概有什么内容。

cpp 复制代码
template<class T>
	struct ListNode//结点
	{
		T _data;//数据
		ListNode* _next;//指向下一个节点的指针
		ListNode* _prev;//指向上一个节点的指针
	};

	template<class T>
	class list
	{
		typedef ListNode<T> Node;
		Node* _head;//指向链表头结点
	};

既然想要设计list的迭代器,就要思考一个点,那就是list的迭代器解引用后得到的东西是什么?是list本身,还是list的数据(listnode)?显然是后者,那么list的迭代器的底层显然是指向listnode的指针

cpp 复制代码
template<class T, class ref, class ptr>
	struct listiterator
	{
		typedef ListNode<T> Node;
		typedef listiterator self;
		
		Node* linkptr;
	};

接着就是为其设计哪些迭代器的通用操作,比如解引用操作(*),成员访问(->)。自加和自减操作符,相等与不等操作符。

cpp 复制代码
	template<class T, class ref, class ptr>
	struct listiterator
	{
		typedef ListNode<T> Node;
		typedef listiterator self;
		listiterator(Node* listnode = nullptr)//默认构造
		{
			linkptr = listnode;
		}
		self operator++()//前置++;
		self operator++(int);//后置++

		self operator--();//前置--
		self operator--(int);//后置++

		ref operator*();

		ptr operator->();

		bool operator!=(listiterator it);

		bool operator==(listiterator it);

		Node* linkptr;
	};

接着就是设计这些操作符的行为逻辑了。我们就拿最复杂的自加和自减操作符为例:

list的迭代器自加会来到下一个元素的位置,而list中下一个元素位置在哪呢?没错,就是下一个节点的位置。于是operator++应该是这样的:

cpp 复制代码
self operator++()//前置++
{
	linkptr = linkptr->next;
	return linkptr;
}
		
self operator++(int)//后置++
{
	listiterator tmp = linkptr;
	linkptr = linkptr->next;
	return tmp;
}

自减操作也是同理,让节点来到上一个节点的位置。

cpp 复制代码
self operator--()
{
	linkptr = linkptr->prev;
	return linkptr;
}

self operator--(int)
{
	listiterator tmp = linkptr;
	linkptr = linkptr->next;
	return tmp;
}

解引用和成员访问符的重载操作逻辑应该如下:解引用可以得到元素,也就是节点的值,而成员访问符由于还需要一个->才能访问元素的成员,于是只需要返回元素的指针即可

cpp 复制代码
ref operator*()
{
	return linkptr->data;
}

ptr operator->()
{
	return &(linkptr->data);
}

于是list的迭代器整体实现如下:

cpp 复制代码
template<class T, class ref, class ptr>
struct listiterator
{
	typedef ListNode<T> Node;
	typedef listiterator self;
	listiterator(Node* listnode = nullptr)
	{
		linkptr = listnode;
	}
	self operator++()//前置++
	{
		linkptr = linkptr->next;
		return linkptr;
	}

	self operator++(int)
	{
		listiterator tmp = linkptr;
		linkptr = linkptr->next;
		return tmp;
	}

	self operator--()
	{
		linkptr = linkptr->prev;
		return linkptr;
	}

	self operator--(int)
	{
		listiterator tmp = linkptr;
		linkptr = linkptr->next;
		return tmp;
	}

	ref operator*()
	{
		return linkptr->data;
	}

	ptr operator->()
	{
		return &(linkptr->data);
	}

	bool operator!=(listiterator it)
	{
		return linkptr != it.linkptr;
	}

	bool operator==(listiterator it)
	{
		return linkptr == it.linkptr;
	}

	Node* linkptr;
};

但是这还不够,因为我们可以发现迭代器是定义在类域中的,但是我们设计的迭代器似乎还在类外,而且还需要实例化才能用

没关系,只要简单的步骤就可以了

cpp 复制代码
template<class T>
	class list
	{
	public:
		typedef ListNode<T> Node;
		typedef listiterator<T, T&, T*> iterator;
		typedef listiterator<T, const T&, const T*> const_iterator;
		
		Node* _head;//指向链表头结点
	};
}

在list类的内部为迭代器起一个别名即可,这样子list便有了可以用于算法的迭代器了。

总结

迭代器其实一个设计理念,不仅仅用用STL库,我们甚至还能在自定义类中用到迭代器,甚至还能将这些迭代器传到STL的算法中。

实际上迭代器是容器和算法之间的桥梁,如果没有迭代器,我们需要为每个容器设计底层不一样的算法,比如find算法,在list中就需要通过访问下一个节点来查找,而在vector中就需要通过访问下标来查找,非常麻烦。

而如果能设计出行为一致,底层不一致的迭代器,那么在设计算法时,就不需要考虑到容器之间的底层差异了。直接通过迭代器来操作,因为迭代器具有一致的行为(自加、自减等),就可以让一个算法服务于多个容器。

本博客的具体代码上传至博主的代码仓库,仓库链接放在主页。

相关推荐
小俊俊的博客4 分钟前
海康RGBD相机使用C++和Opencv采集图像记录
c++·opencv·海康·rgbd相机
_WndProc19 分钟前
C++ 日志输出
开发语言·c++·算法
薄荷故人_20 分钟前
从零开始的C++之旅——红黑树及其实现
数据结构·c++
m0_7482400220 分钟前
Chromium 中chrome.webRequest扩展接口定义c++
网络·c++·chrome
qq_4335545428 分钟前
C++ 面向对象编程:+号运算符重载,左移运算符重载
开发语言·c++
努力学习编程的伍大侠32 分钟前
基础排序算法
数据结构·c++·算法
XiaoLeisj1 小时前
【递归,搜索与回溯算法 & 综合练习】深入理解暴搜决策树:递归,搜索与回溯算法综合小专题(二)
数据结构·算法·leetcode·决策树·深度优先·剪枝
yuyanjingtao1 小时前
CCF-GESP 等级考试 2023年9月认证C++四级真题解析
c++·青少年编程·gesp·csp-j/s·编程等级考试
闻缺陷则喜何志丹2 小时前
【C++动态规划 图论】3243. 新增道路查询后的最短距离 I|1567
c++·算法·动态规划·力扣·图论·最短路·路径
charlie1145141912 小时前
C++ STL CookBook
开发语言·c++·stl·c++20