C++泛型编程——迭代器

前言:18年公司代码使用的是C风格代码,20年开始公司代码架构开始使用C++(嗯,C风格的C++代码)。今年开始考虑代码优化重构,泛型编程就是很好的策略。

泛型编程(Generic Programming)最初提出时的动机很简单直接:编写独立于数据类型的代码,只关注于算法不需要考虑数据的类型。
STL是一种泛型编程(generic programming)。C++面向对象编程关注的是编程的数据方面,二泛型编程关注的是算法。两者的共同点是抽象和创建可重用代码

泛型编程的代表作品STL是一种高效、泛型、可交互操作的软件组件。STL以迭代器 (Iterators)和容器(Containers)为基础,是一种泛型算法(Generic Algorithms)库,容器的存在使这些算法有东西可以操作。STL包含各种泛型算法(algorithms)、泛型迭代器(iterators)、泛型容器(containers)以及函数对象(function objects)。

为何使用迭代器?
理解迭代器是理解STL的关键。模板是的算法独立于存储的数据类型。而迭代器是算法独立于使用的容器类型。篇幅有限这里先介绍迭代器,函数模板于下篇《泛型编程之函数模板》中介绍。

首先看一个在double数组中搜索特定值的函数,可以这样编写函数实现:

double *findArrd(double *arg, int len, const double &val)
{
	for(int idx = 0; idx < len; idx++)
	{
		if(arg[idx] == val)
		{
			return &arg[idx];
		}
	}
	return 0;		/*in c++11, return nullptr*/
} 

如果函数在数组中找到这个值,则返回该值在数组中的地址,否则返回一个空地址。该函数是使用数组下标来遍历数组,从而找到需求值。可以用模板将这种算法推广到包含==运算符的、任意类型的数组。尽管如此,这种算法仍然于特定的数据结构——数组关联在一起。

下面看另一种数据结果i有——链表。链表由指针链接在一起的Node组成:

struct Node
{
	double value;
	Node *p_next;
};

假设由一个指向链表第一个节点的指针,每个节点的p_next指针都指向下一个节点,链表最后一个节点的p_next指针被设置为0,则可以编写这样的函数:

Node *findll(Node *head, const double &val)
{
	for(Node *pNode = head; pNode != 0; pNode = pNode->next)	
	{
		if(pNode->value == val)
		{
			return pNode;
		}
	}
	return 0;
}

同样也可以使用模板将这两种算法推广到支持==运算符的任何数据类型的链表。然而这种算法也是于特定的数据结构——链表关联在一起的。

从实现细节上来看,这两个函数的算法是不同的:一个使用数组索引来遍历元素,另一个是通过pNode->next来遍历元素。但从广义上来看这两种算法是相同的:将值依次与容器中的每个值进行比较,知道匹配为止。

泛型编程旨在使用用一个函数来处理数组、链表或任何其他容器类型。即
函数不仅独立于容器中存储的数据类型,而且独立于容器本身的数据结构。模板提供了存储在容器中的数据类型的通用表示,迭代器就是这样的统一表示。
要达到这些需求,迭代器需要具有下面的一些特征:

  • 应能够对迭代器执行接触引用的操作,以便能够访问它引用的值。即如果p是一个迭代器,。则应对*p进行定义。
  • 应能将一个迭代器付给另一个迭代器。即如果p和q都是迭代器,则应能满足p = q的操作。
  • 应能够对一个迭代器于另一个进行比较,看他们是否相等。即如果p和q都是迭代器,则应对p == q和p != q进行比定义
  • 应能够使用迭代器遍历容器中的每一个元素,这可以通过迭代器p定义++p和p++来实现。

常规指针也可以完成迭代器的操作,因此也可以这样编写上面的函数:

typedef double* iterator;
iterator findAddr(iterator arg, int len, const dounble &val)
{
	for(int idx = 0; idx < len; arg++)
	{
		if(*arg == val)
		{
			return arg;
		}
	}
	return 0} 

然后可以修改参数列表,使之接收两个指针区间的指针参数,其中一个指向数组的其实位置,另一个指向数组的超尾。(这个迭代器不指向实际的元素,而是表示末端元素的下一个元素,这个迭代器起一个哨兵的作用,表示已经处理完所有的元素)

typedef double* iterator;
iterator findAddr(iterator start, iterator end, const double &val)
{
	iteraror it;
	for(it = start; it!=end; it++)
	{
		if(*it == val)
		{
			return it;
		}
	}
	return 0;
}

对于findll()函数,可以定义一个迭代器类,其中定义了运算符*和++:

struct Node
{
		double value;
		Node* p_next;
};

class iterator
{
private:
	Node *pt;
public:
	iterator():pt(0){}
	iterator(Node *pn):pt(pn){}
	double operator*(){return pt->value;}
	iterator& operator++()
	{
		pt = pt->next;
		return *this;
	}
	iterator operator++(int)	//for it++
	{
		iterator tmp = *this;
		pt = pt->next;
		return tmp;
	}
	// ...operator==(),operator!=(),etc.
};

为区分++运算符的前缀和后缀版本,c++将operator作为前缀版本麻将operator++(int)作为后缀版本;其中的参数永远不会用到,所以不用指定其名称。

这里重点不是怎么定义iterator类,二十有了这样的类后,第二个find函数可以这样写:

iterator findll(iterator head, const double& value)
{
	iterator start;
	for(start = head; start != 0; ++start)
	{
		if(*start == value)
			return start;
	}
	
	return 0;
}

这和findAddr几乎相同。差别在于如何谓词已到达最后一个值,findAddr()函数使用超尾迭代器,而findll()使用存储在最后一个节点的空值。除了这种差别外,两者完全相同。例如可以要求链表的最后一个元素后面还有一个额外的元素让数组和链表都有超尾元素,并在跌倒其达到超尾元素时结束搜索,这样,findAddr()和findll()检测超尾的方式都相同,从而成为相同的算法。注意,增加超尾元素后,对迭代器的要求成为了对容器的要求。

STL遵循上面的方法。首先,每个容器类(vector、list、deque等)定义了相应的迭代器类型。对于去哦中每个类,迭代器可能是指针;而对于另一个类,则可能是对象。不管实现方式如何,迭代器都将提供所需的操作,如*和++(有些类需要的操作比其他的类多)。其次,每个容器都有一个超尾标记,当迭代器递增到超越容器的最后一个值是,这个值将被赋予给迭代器,每个容器类都有begin()和end()方法,他们分别返回一个指向容器第一个元素和超尾元素的迭代器,每个容器类都是用++操作,让迭代器从指向第一个元素逐渐指向超尾位置,从而遍历容器中的每一个元素。

使用容器类时,无需知道其迭代器时如何实现的,也无需知道超尾时如何实现的,而只知道他有迭代器,其begin()返回一个指向第一个元素的迭代器,end()返回一个指向超尾的迭代器即可。例如,假设要打印vector对象中的值,则可以这样做:

vector<double>::iterator it;
for(it = scores.begin(); it != score.end(); it++)
	cout<<*it<<endl;

其中下面的代码行将it的类型声明为vector类的迭代器:

vector<double> class;
vector<double>::iterator it;

如果要使用list类模板来存储分数,则代码如下:

list<double>::iterator it;
for(it = score.begin();  it != score.end(); it++)
	cout<<*it<<endl;

唯一不同的是it的类型。因此,STL通过每个类定义适当的迭代器,并以统一的风格设计类,能够对内部表示绝然不同的容器,编写相同的代码。
使用C++11新增的自动类型推断可以进一步简化,对于矢量或列表,都可以使用如下代码:

for(auto it = score.begin(); it != score.endf(); it++)
	count<<*it<<endl;

实际上,作为一种比编程风格,最好避免直接使用迭代器,而应尽可能的使用STL函数(如for_each())来处理细节。也可以使用C++新增的基于范围的for循环:

for(auto x : scores)	cout<<x<<endl;

来总结一下STL方法。首先是处理容器的算法,应尽可能通用的术语来表达算法,是指独立于数据类型和容器类型。为使通用算法能够适用于具体情况,应定义能够满足算法需求的迭代器,并能把要求加到容器设计上去。即基于算法的要求设计基本迭代器的特征和容器特征。

本文思想借鉴于《C++ Primer Plus》第六版,u1s1这本书也是C++程序员进阶经典之一。

  • 1
    点赞
  • 2
    收藏
  • 打赏
    打赏
  • 0
    评论

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

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:终极编程指南 设计师:CSDN官方博客 返回首页
评论

打赏作者

生命如歌,代码如诗

听说,打赏我的人都找到了真爱!

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

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

打赏作者

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

抵扣说明:

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

余额充值