C++ day 38泛型编程:STL的底层理念(迭代器的5个大类,6个预定义迭代器)

OOP VS GP

  • 关注点不同。

泛型编程关注算法,旨在编写独立于数据类型的代码,即任何类型都用一套代码,C++实现泛型编程的工具就是模板,可以用模板来定义函数或者类。但是模板的设计中,迭代器非常重要。理解迭代器是打开STL世界大门的钥匙,因为迭代器使得算法独立于容器类型

模板迭代器是STL通用方法的重要组成部分。
模板使得算法独立于存储的数据类型。
迭代器使得算法独立于使用的容器类型。

面向对象编程关注的是数据

  • 共同点:抽象,重用代码

迭代器

为什么需要:用一个例子引入需求

先给数组定义find()函数

double * find_ar(double * ar, int n, const double & val)
{
	for (int i=0;i<n;++i)
	{
		if (ar[i] == val)
			return &ar[i];
	}
	return nullptr;
}

这里用的时间复杂度最差的顺序查找,只是为了展示哈。

这种写法可以适合于所有数据类型的数组,不只是double,所以可以写为模板函数,就可以推广到各种类型。

再给链表定义find()函数

但是!如果数据使用链表存储呢?傻了,由于上面代码使用数字下标进行索引,而链表不支持随机访问,所以无法推广到链表。所以重用代码的程度不强,通用性不够好。

链表需要先定义一个结点的结构

struct Node{
	double item;
	Node * next;
};
Node * find_list(Node * head, const double & val)
{
	Node * start;
	for (start=head;start != nullptr;start=start->next)
		if (start->item == val)
			return start;
	return nullptr;
}

同样的,这个代码可以推广到所有用链表存储的数据类型,定义成模板函数,但是它仍然是一个和链表容器关联在一起的模板函数,通用性有限。

可不可以把数组和链表这两个模板函数压缩为一个呢?只用一个函数,实现数组表示法和链表表示法的遍历。毕竟这两个函数的算法实际上是一样的,只不过前者用了下标索引,后者用了p=p->next方法而已。

泛型编程就是基于这种需求,它想实现真正的大面积代码重用,抽象程度更高,用同一个find函数处理数组,链表,甚至其他的容器类型。即函数独立于容器类型,也独立于数据类型

在刚才的这个例子中,我们清楚的看到,函数独立于数据类型是通过模板实现的,但是只用模板不能让函数独立于容器类型,因为不同的容器的底层需要的操作不一样。而让函数实现独立于容器类型的目标的工具,正是迭代器

什么是迭代器,有哪些特征

迭代器是STL算法的接口
在这里插入图片描述

可以看到,迭代器需要的特征很少,根本不复杂的。

而且常规指针完全已经符合这三个特征了哦。所以可以这么写代码:

从常规指针到迭代器的雏雏雏形

typedef double * iterator;
iterator find_ar(iterator ar, int n, double & val)
{
	for (int i=0;i<n;++i)
		if (*ar == val)
			return ar;
	return nullptr;
}

挺难想到的诶。还以为iterator是个保留字不能自己定义呢。

但是,具有这三个特征的只是初级的简单的迭代器,STL按照功能强弱还定义了多种级别的迭代器。

改函数参数,得到迭代器雏形

再把上面的函数继续修改,改参数,改成像迭代器的那么个样子:

typedef double * iterator;
iterator find_ar(iterator begin, iterator end, const double & val)
{
	iterator ar;
	for (ar=begin;ar!=end;++ar)
		if (*ar == val)
			return ar;
	return end;//返回end就表示没找到
}

对链表容器,要专门定义迭代器类

class iterator{
	Node * pt;//私有数据是一个Node指针
public:
	iterator():pt(0){}
	iterator(Node * pn):pt(pn){}
	//重载*运算符函数,实现迭代器的解引用功能
	double operator*(){return pt->item;}
	//重载++运算符
	iterator operator++(int) // it++,后缀递增运算符
	{
		iterator tmp = *this;//先存住当前迭代器
		pt = pt->next;
		return tmp;
	}
	//注意后缀版本的int参数只是摆设,用不到的,只是C++编译器要区分一下,总不能前后缀的函数头一样吧
	iterator operator++() //++it,前缀递增运算符
	{
		pt = pt->next;
		return *this;
	}
	//重载==
	bool operator==(const iterator & it)
	{
		if (it->item == pt->item)
			return true;
		return false;
	}
	//重载!=
	bool operator!=(const iterator & it)
	{
		if (it->item == pt->item)
			return false;
		return true;
	}
};

有了迭代器类之后,链表的find函数就可以写为:

iterator find_list(iterator head, const double & val)
{
	iterator start;
	for (start=head;start!=nullptr;++start)
		if(*start == val)
			return start;
	return nullptr;
}

现在,链表和数组的代码基本一样了,唯一的差别是数组那边是end,链表这边是空指针判断链表尾,所以为了实现大一统,我们还有一步工作要做,即让链表也有超尾。当然啦,这并不是对迭代器的要求,这是对容器类的要求,毕竟是要求链表要有超尾元素得嘛。

STL就马上行动,先给所有容器类定义了对应的迭代器类型对于有的类比如vector,迭代器实际就是指针,就像咱们上面的代码一样,底层就是咱们这么写的;而对于链表这种类型,迭代器就是一个类的对象。不管是指针还是对象,总之迭代器的功能就是解引用,递增,==,!=这几个。

vector<int>类的迭代器是vector<int>::iterator
list<int>类的迭代器是list<int>::iterator

然后STL赶紧给所有容器类定义了超尾标记,且每个容器类都有begin(),end()方法,分别返回指向容器第一个元素的迭代器,和指向超尾位置的迭代器。

超尾,超 不是 超级 的意思,而是 超过 的意思。哈哈

注意所有容器类的迭代器的类型也许是不一样的哦。有的类的迭代器是指针类型,有的类的迭代器是类对象哦。但是我们使用者看来,看不到任何区别,底层的实现我们都是看不见的,因为C++采用了同一的风格设计迭代器的类。

现在,不管对数组还是链表进行查找,都是直接使用迭代器:

vector<double> scores;
for(auto p = scores.begin();p!=scores.end();++p)
	cout << *p << endl;
list<double> scores;
for(auto p = scores.begin();p!=scores.end();++p)
	cout << *p << endl;

小结

首先,泛型编程的理念是通用,抽象,重用代码。它强调算法,关键思想是要让算法独立于数据类型和容器类型。独立于数据类型可以用模板工具达到目的,但是用了模板以后,发现不同容器的具体情况有所差别,于是给容器类加了需求,即让所有容器类都要用超尾位置标记,然后让所有容器类都设计一个适合自己的迭代器类型,并且不同容器类的迭代器的类设计要一致统一。

我觉得这两句话特别精辟有力。

模板使得算法独立于存储的数据类型。
迭代器使得算法独立于使用的容器类型

迭代器在有的容器类中是指针,因为指针就可以满足需求;在有的容器类中则必须是类对象,因为常规指针满足不了这些容器类的需求,只能专门定义一个类来实现所有需要的方法。

迭代器的类型(5种)

为什么要对迭代器进行分类呢????

因为不同算法对迭代器的要求不同,比如:

  • 查找算法算是事儿少的,它只需要查看数据,不需要写数据。这就只需要一个输入迭代器。
    在这里插入图片描述
  • 排序算法事儿稍微多点,它需要随机访问数据,以便于交换任意两个位置的元素;还需要写数据。这就需要一个随机访问迭代器。
    在这里插入图片描述
    于是STL就按照功能强弱分成了5个大类。每一个算法根据自己的需求选择使用哪一种迭代器。

感觉STL像一个忠实的服务提供者,算法是客户,客户需要什么,STL钻研地透透的,然后完美地开发出多样的完美的工具供用户自行挑选使用。

但是这五种迭代器的共同点是:
都定义了解引用,==,!=, ++(包括前缀和后缀)这四个最基本操作。

输入迭代器(单通行,只读)

“输入”是指把容器的信息当做程序的输入,即只可以读取容器内信息,不可以输出到容器内。

  • 对输入迭代器解引用可以读取到容器中的值,但是不可以修改值。
  • 是单向迭代器,只能递增,不可倒退。因为没有重载递减--运算符。所以基于输入迭代器的算法都是单通行single-pass的,即本次遍历的顺序和上一次遍历的顺序完全无关,和本次遍历前面的迭代器也完全无关(即每次遍历时,遍历顺序都不一定一样哦)。
  • 只用++来遍历容器

输出迭代器(单通行,只写)

“输出”是指程序把信息输出到容器内,即可以修改容器的值。

  • 解引用时,程序不可以读取容器内的值,而是要修改容器内的值。即只能写不能读

其实cout也是如此啊,只可以修改发送到显示器的字符流,不能读取屏幕上的内容。

  • 只用++来遍历容器
  • 和输入迭代器样,每次遍历的遍历顺序都不一定一样

正向迭代器(多次通行,既可以读写,又可以只读)

  • 具有输入和输出迭代器的全部功能
  • 只用++来遍历容器,这和输入输出迭代器一样
  • 每次都按照相同的顺序遍历容器的所有值,这和输入输出迭代器不一样。
  • 对正向迭代器递增后,还是可以对前面一个迭代器解引用,只要你保存了的就可以。
  • 它可以读写数据;或者只读数据。这两种实现都可以做到。

常规指针就是可以读可以写的,const指针是只读的。
在这里插入图片描述

由于正向迭代器不支持递减操作,所以不可以用常规指针实现,一般会用一个来实现。

双向迭代器(支持递减运算符)

  • 具有正向迭代器的所有特性,所以双向迭代器是正向迭代器的一种改进refinement。
  • 还支持递减运算符(前缀后缀)

list类,双向链表,使用双向迭代器,所以不可以用于基于随机访问的算法。

由于双向迭代器支持正向迭代器的所有操作,还支持递减,所以可以用常规指针来实现。用常规指针实现的双向迭代器是C++的内置类型,不是类。但是也可以用类来实现。

随机访问迭代器

排序和二分查找等算法,需要从一个位置直接跳到任何一个位置,即随机访问。

  • 具有双向迭代器的所有特性
  • 还支持随机访问操作,一般是通过指针加减运算实现
  • 支持对元素进行排序的关系运算符

在这里插入图片描述

vector类的迭代器是随机访问迭代器,可以用于任何算法。

其实可以看到,指针满足所有迭代器的要求

小结

5个迭代器是有层次结构的,很像继承关系(但是并不是继承机制!因为正向迭代器可以实现为一个类,但是双向迭代器可以实现为常规指针!即这个层次里的5类迭代器,有的用类,有的用指针,并没有用C++的继承机制。):

  • 正向迭代器具有输入和输出迭代器的全部功能
  • 双向迭代器具有正向迭代器的全部功能
  • 随机访问迭代器具有双向迭代器的全部功能

看这个表,能对5种迭代器支持的操作更加清楚
在这里插入图片描述

在选择迭代器到自己的算法中时,尽量选择要求最低的最简单的可满足需求的迭代器,即级别最低的迭代器。比如,虽然find函数只需要输入迭代器,但是正向,双向,随机访问迭代器都是可以用的,但是我们并不会选择这些复杂的,而是选择简单的输入迭代器。

指针满足所有迭代器的要求

把指针用作迭代器

迭代器是广义指针,是STL算法的接口
指针满足所有迭代器的要求,指针就是迭代器
所以,STL算法也可以用于基于指针的非STL容器,比如数组。

迭代器是广义指针,但是指针就是迭代器。迭代器是一个比指针更宽大的概念,它包含了指针,指针确确实实一定的,百分百,是迭代器。而迭代器不一定是指针,因为它可能是用类来实现的,所以还可能是类的对象。

指针一定是迭代器;迭代器不一定是指针。

把STL算法用于常规数组(非STL容器)

以快排算法的实现sort()为例

const int SIZE = 5;
double receipts[SIZE];
sort(receipts, receipts + SIZE);//或者sort(&receipts[0], &receipts[SIZE]);

sort函数第一个参数是指向第一个元素的迭代器,第二个元素是指向超尾的迭代器

C++把超尾的概念也用到了非STL容器中。

能够把STL算法用到常规容器,就是因为指针是迭代器,且常规容器也有超尾的概念

copy()函数

  • 用于把数据从一个容器复制到另一个容器。
  • copy()算法是基于迭代器的,所以两个容器可以不是同一个类型。
  • 还可以在常规数组之间进行复制。
  • 接受三个参数,前两个参数必须是输入迭代器,表示要复制的范围。最后一个迭代器必须是输出迭代器,表示目标容器的位置。copy()函数会把目标容器对应位置的已有数据覆盖,且目标容器必须足够大以容纳下源数据。
  • 是一个独立于各种容器的函数,没有权限去动态调整容器的大小,所以一定要自己保证容器大小足够装。

只有容器类自己的成员方法,比如vector类的push_back()方法,有权限该容器的大小。

  • copy()函数很强大的,可以完成很多工作。不仅可以把信息从一个容器复制到另一个容器,还可以把信息从容器复制到输出流,或者从输入流复制到容器。
int casts[5] = {5, 6, 7, 3, 4};
vector<int> dice[5];
copy(casts, casts+5, dice.begin());//把数组copy给vector

iterator头文件提供的预定义迭代器

都是STL提供的

ostream_iterator:表示输出流的迭代器的模板

可以用来把信息复制到显示器上。有了ostream_iterator迭代器,就可以使用copy()函数了。

ostream_iterator模板是输出迭代器概念的一种模型,是一个适配器adapter(即一个类,或一个函数,可把其他接口转换为STL使用的接口)。

这里涉及到好几个STL领域内的术语,比如概念concept(STL用来描述一系列要求)适配器等。都是很抽象的很难理解的。

示例:搭配copy()

//创建迭代器
#include <iterator>
ostream_iterator<int, char> out_iter(cout, " ");//有两个模板参数哦

这时候out_iter迭代器就是一个接口,是一个ostream_iterator<int, char>类的对象,这个接口使用cout显示信息。

  • 第一个模板参数int表示发送给输出流的数据类型
  • 第二个模板参数char表示输出流使用的字符类型(还可以是wchar_t)。
  • 构造函数的第一个参数cout表示要使用的输出流(还可以是文件输出流)
  • 构造函数的第二个参数是发送给输出流后每个数据项后面的分隔符

所以用法是:

*out_iter++ = 15;//相当于cout << 15 << " ";

先把15给out_iter指向的位置,再给out_iter迭代器递增。其实就是先把15和分隔符发给输出流,再递增迭代器。

太高级了吧!!!!!太会玩了。。。。牛逼,还是C++牛逼。

如果用copy(),会更简单,可以一次输出多个数据:

#include <iostream>
#include <iterator>

int main(){
	using namespace std;
    ostream_iterator<double, char> out_iter(cout, " ");
    double prices[4]= {1.2, 3.5, 9.8, 5.2};
    copy(prices, prices+4, out_iter);//注意常规数组没有begin()和end()方法,要用指针做迭代器
	return 0;
}
1.2 3.5 9.8 5.2

还可以直接用匿名迭代器:

#include <iostream>
#include <iterator>

int main(){
	using namespace std;
    double prices[4]= {1.2, 3.5, 9.8, 5.2};
    copy(prices, prices+4, ostream_iterator<double, char> (cout, " "));
	return 0;
}

istream_iterator:把istream输入作为迭代器接口

是输入迭代器概念的一个模型

也有俩模板参数,第一个指出读取的数据类型,第二个指出输入流的字符类型。构造函数第一个参数cin表示用cin管理的输入流,如果不写构造函数的参数则表示输入失败。

示例:

copy(istream_iterator<int, char> (cin), istream_iterator<int, char>(), dice.begin());

示例

#include <iostream>
#include <iterator>
#include <vector>.
int main(){
	using namespace std;
    vector<int> dice(5);
    copy(istream_iterator<int, char> (cin), istream_iterator<int, char>(), dice.begin());//直到输入失败
	copy(dice.begin(), dice.end(), ostream_iterator<int, char>(cout, " "));
	return 0;
}

输入字母,不是int,输入失败,所以结束

1 2 3 4 a
1 2 3 4 0

输入浮点数,不是int,输入失败,所以结束,但是浮点数的点之前的数据会被读取哦

2 5 8
9
2.3
2 5 8 9 2

reverse_iterator:反向迭代器

vector类有一个成员函数叫做rbegin(),rend()。分别返回指向超尾的反向迭代器和指向第一个元素的反向迭代器。

rbegin()和end()返回的迭代器指向的位置都是超尾, 但是类型不同,前者是反向迭代器,后者是普通迭代器。

主要是用来反向显示容器的内容,或者其他反向的操作。它可以用来简化已有函数的使用。

不需要声明反向迭代器。。。。

emmmm,难道反向迭代器没有构造函数??不需要传入模板参数,它就只能通过rbegin(),rend()函数来访问???

反向指针/反向迭代器必须:先递减,再解引用

有一个严重问题:如果对dice.rbegin()解引用会怎么样???毕竟这返回的是超尾位置,解引用肯定出错。

所以,反向指针不可以像正向迭代器那样,先解引用再递增迭代器

而是要先递减,再解引用,这样,就不会对dice.rbegin()解引用了,且到了dice.rend(),也是先递减为第一个元素前面的那个元素位置,再对第一个元素解引用。

示例

#include <iostream>
#include <iterator>
#include <vector>.
int main(){
	using namespace std;
    vector<int> dice(5);//这里必须分配空间啊!
    copy(istream_iterator<int, char> (cin), istream_iterator<int, char>(), dice.begin());
	copy(dice.rbegin(), dice.rend(), ostream_iterator<int, char>(cout, " "));
	return 0;
}
4 5 6 7 8 a
8 7 6 5 4

示例:copy(), istream_iterator, reverse_iterator

#include <iostream>
#include <iterator>
#include <vector>.
int main(){
	using namespace std;
    
	int casts[10] = {6, 7, 8, 5, 6, 3, 71, 12, 56, 45};
	vector<int> dice(10);
	copy(casts, casts+10, dice.begin());
	cout << "Let the dice be cast!\n";
	ostream_iterator<int, char> out_iter(cout, " ");
	copy(dice.begin(), dice.end(), out_iter);
	cout << endl;
	cout << "Implicit use of reverse iterator.\n";
	copy(dice.rbegin(), dice.rend(), out_iter);
	cout << endl;
	cout << "Explicit use of reverse_iterator.\n";
	vector<int>::reverse_iterator ri;
	for (ri=dice.rbegin();ri!=dice.rend();++ri)
		cout << *ri << ' ';
	cout << endl;
	return 0;
}
Let the dice be cast!
6 7 8 5 6 3 71 12 56 45
Implicit use of reverse iterator.
45 56 12 71 3 6 5 8 7 6
Explicit use of reverse_iterator.
45 56 12 71 3 6 5 8 7 6

三种插入迭代器:把复制改为插入,使用自动内存分配

  • 插入是添加新元素,但是不覆盖已有数据
  • 使用自动内存分配以确保能够容纳的下新元素
  • 三个插入迭代器都是输出容器概念的模型(不是输出迭代器哦!)
  • 这三个迭代器把容器类型作为模板参数(之前的istream_iterator和ostream_iterator是把数据类型作为模板参数的,而reverse_iterator没有)

back_insert_iterator:新元素插入到容器尾部

  • 只允许插入到尾部,就可以使用一个快速插入算法,即一个时间固定的算法。
  • vector类就符合要求。
  • 模板参数是容器类型,比如下面的声明中是vector<int>,这是因为迭代器需要使用这个容器类自己的成员方法以动态调整容器的大小

非成员函数不可能有权限去修改容器的大小的,那还有什么隐私什么访问控制的天理?虽然是泛型编程的主场了,OOP的规则和思想还是要尊重的。

  • 构造函数的参数只有一个,即容器对象。
    声明:
back_insert_iterator<vector<int>> back_iter(dice);

front_insert_iterator:新元素插入到容器前端

  • 只能在起始位置插入,则也可以用时间固定的快速插入算法。
  • vector类不满足。queue类满足。
  • 模板参数和构造函数参数,以及声明方式,都和back_insert_iterator一样。
    声明:
front_insert_iterator<vector<int>> front_iter(dice);

insert_iterator:新元素插入到构造函数参数指定的位置的前面

  • 没有限制插入位置,所以速度相对来说就要慢一点。
  • 构造函数参数多了一个,第二个参数表示插入位置。新元素会被插入到这个位置的前面。
    声明:
insert_iterator<vector<int>> iter(dice, dice.begin());

示例

#include <iostream>
#include <string>
#include <iterator>
#include <vector>
#include <algorithm>
void output(const std::string & s){std::cout << s << " ";}
int main()
{
	using namespace std;
	string s1[4] = {"fine", "fish", "fashion", "fate"};
	string s2[2] = {"busy", "bats"};
	string s3[2] = {"silly", "singers"};
	vector<string> words(4);
	copy(s1, s1+4, words.begin());
	for_each(words.begin(), words.end(), output);
	cout << endl;

	//构建匿名末尾插入迭代器对象
	copy(s2, s2+2, back_insert_iterator<vector<string>>(words));
	for_each(words.begin(), words.end(), output);
	cout << endl;

	//构建匿名插入迭代器
	copy(s3, s3+2, insert_iterator<vector<string>>(words, words.begin()));//插入在words前端,比front_insert_iterator慢
	for_each(words.begin(), words.end(), output);
cout << endl;
	return 0;
}
fine fish fashion fate
fine fish fashion fate busy bats
silly singers fine fish fashion fate busy bats

总结

  • 术语“概念”被STL用来描述一系列要求。而概念的实现方法被称为“概念”的“模型”。
    在这里插入图片描述
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值