c++ primer plus 第16章string 类和标准模板库, 泛型编程----概念、改进和模型
c++ primer plus 第16章string 类和标准模板库, 泛型编程----概念、改进和模型
文章目录
16.4.4概念、改进和模型
STL有若干个用 C++语言无法表达的特性,如迭代器种类。因此,虽然可以设计具有正向迭代器特征的类,但不能让编译器将算法限制为只使用这个类。原因在于,正向迭代器是一系列要求,而不是类型。所设计的迭代器类可以满足这种要求,常规指针也能满足这种要求。STL,算法可以使用任何满足其要求的迭代器实现。STL文献使用术语概念(concept)来描述一系列的要求。因此,存在输入迭代器概念、正向迭代器概念,等等。顺便说一句,如果所设计的容器类需要迭代器,可考虑STL,它包含用于标准种类的迭代器模板。
概念可以具有类似继承的关系。例如,双向迭代器继承了正向迭代器的功能。然而,不能将C++继承机制用于迭代器。例如,可以将正向迭代器实现为一个类,而将双向迭代器实现为一个常规指针。因此,对 C++而言,这种双向迭代器是一种内置类型,不能从类派生而来。然而,从概念上看,它确实能够继承。有些 STL 文献使用术语改进(refinement)来表示这种概念上的继承,因此,双向迭代器是对正向迭代器概念的一种改进。
概念的具体实现被称为模型(model)。因此,指向int 的常规指针是一个随机访问迭代器模型,也是一个正向迭代器模型,因为它满足该概念的所有要求。
1.将指针用作迭代器
迭代器是广义指针,而指针满足所有的迭代器要求。迭代器是 STL算法的接口,而指针是迭代器,因此STL算法可以使用指针来对基于指针的非STL 容器进行操作。例如,可将STL 算法用于数组。假设Receipts 是一个 double 数组,并要按升序对它进行排序:
const int SI2E=100;
double Receipts[SIZE];
STL SOrt()函数接受指向容器第一个元素的迭代器和指向超尾的迭代器作为参数。&Receipts0是第一个元素的地址,&ReceiptSIZE是数组最后一个元素后面的元素的地址。因此,下面的函数调用对数组进行排序:
sort(Receipts,Receipts +SIZE);
C++确保了表达式 Receipts+n是被定义的,只要该表达式的结果位于数组中。因此,C++支持将超尾概念用于数组,使得可以将 STL算法用于常规数组。由于指针是迭代器,而算法是基于迭代器的,这使得可将STL算法用于常规数组。同样,可以将STL算法用于自己设计的数组形式,只要提供适当的选代器(可以是指针,也可以是对象)和超尾指示器即可。
copy()、ostream iterator和istream iterator
STL 提供了一些预定义迭代器。为了解其中的原因,这里先介绍一些背景知识。有一种算法(名为copy())可以将数据从一个容器复制到另一个容器中。这种算法是以选代器方式实现的,所以它可以从一种容器到另一种容器进行复制,甚至可以在数组之间复制,因为可以将指向数组的指针用作迭代器。例如,下面的代码将一个数组复制到一个天量中:
int casts[10]=6,7,2,94,11,8,7,10,5};
vector<int>dice[10];
copy(casts,casts+10,dice.begin());// copy array to vector
copy()的前两个迭代器参数表示要复制的范围,最后一个迭代器参数表示要将第一个元素复制到什么位置。前两个参数必须是(或最好是)输入迭代器,最后一个参数必须是(或最好是)输出迭代器。Copy()函数将覆盖目标容器中已有的数据,同时目标容器必须足够大,以便能够容纳被复制的元素。因此,不能使用copy()将数据放到空矢量中–至少,如果不采用本章后面将介绍的技巧,则不能这样做。现在,假设要将信息复制到显示器上。如果有一个表示输出流的迭代器,则可以使用copy()。STL为这种迭代器提供了 ostream iterator 模板。用 STL的话说,该模板是输出迭代器概念的一个模型,它也是一个适配器(adapter)–一个类或函数,可以将一些其他接口转换为STL使用的接口。可以通过包含头文件iterator(以前为iteratorh)并作下面的声明来创建这种迭代器:
#include <iterator>
...
ostream iteratorsint,char>out iter(cout."")
out iter 迭代器现在是一个接口,让您能够使用 cout 来显示信息。第一个模板参数(这里为int)指出了被发送给输出流的数据类型;第二个模板参数(这里为char)指出了输出流使用的字符类型(另一个可能的值是 wchar t)。构造函数的第一个参数(这里为cout)指出了要使用的输出流,它也可以是用于文件输出的流(参见第17章):最后一个字符串参数是在发送给输出流的每个数据项后显示的分隔符。
可以这样使用迭代器:
*out iter++ = 15://works like cout <s 15 <<”"
对于常规指针,这意味着将15赋给指针指向的位置,然后将指针加1。但对于该ostream iterator,这意味着将15和由空格组成的字符串发送到cout 管理的输出流中,并为下一个输出操作做好了准备。可以将copy()用于迭代器,如下所示:
copy(dice.begin(),dice.end(),out iter);
//copy vector to output stream
这意味着将 dice 容器的整个区间复制到输出流中,即显示容器的内容。也可以不创建命名的迭代器,而直接构建一个名迭代器。即可以这样使用适配器:
copy(dice.begin(),dice.end(),ostream iterator<int,char>(cout,""));
iterator 头文件还定义了一个istream iterator 模板,使 istream 输入可用作迭代器接口。它是一个输入迭代器概念的模型,可以使用两个istream_iterator对象来定义copy()的输入范围:
copy(istream iterator<int,char>(cin)
istream iteratorcint,char>(),dice.begin());
与 ostream iterator 相似,istream iterator 也使用两个模板参数。第一个参数指出要读取的数据类型,第二个参数指出输入流使用的字符类型。使用构造函数参数 cin 意味着使用由cin 管理的输入流,省略构造函数参数表示输入失败,因此上述代码从输入流中读取,直到文件结尾、类型不匹配或出现其他输入故障为止。
2.其他有用的选代器
除了 ostream iterator 和istream iterator 之外,头文件 iterator 还提供了其他一些专用的预定义迭代器类型。它们是reverse iterator、back insert iterator、front insert iterator 和 insert iterator.我们先来看reverse-iterator的功能。对reverse iterator 执行递增操作将导致它被递减。为什么不直接对常规迭代器进行递减呢?主要原因是为了简化对已有的函数的使用。假设要显示 dice 容器的内容,正如刚才介绍的,可以使用copy()和ostream iterator 来将内容复制到输出流中:
ostream iteratorsint,char>out_iter(cout,"");
copy(dice.begin(),dice.end(),out iter); // display in forward order
现在假设要反向打印容器的内容(可能您正在从事时间反演研究)。有很多方法都不管用,但与其在这里耽误工夫,不如来看看能够完成这种任务的方法。vector类有一个名为rbegin()的成员函数和一个名为rend()的成员函数,前者返回一个指向超尾的反向迭代器,后者返回一个指向第一个元素的反向迭代器。因为对迭代器执行递增操作将导致它被递减,所以可以使用下面的语句来反向显示内容:
copy(dice.rbegin(),dice.rend(),out iter);// display in reverse order
甚至不必声明反向迭代器。
注意:rbegin()和 end()返回相同的值(超尾),但类型不同(reverse itcrator和itcrator)。同样,rend()和 begin()也返回相同的值(指向第一个元素的迭代器),但类型不同。
必须对反向指针做一种特殊补偿。假设p是一个被初始化为dice.rbegin()的反转指针。那么rp是什么呢?因为rbegin()返回超尾,因此不能对该地址进行解除引用。同样,如果 rend()是第一个元素的位置,
则 copy()必须提早一个位置停止,因为区间的结尾处不包括在区间中。反向指针通过先递减,再解除引用解决了这两个问题。即p将在mp的当前值之前对迭代器执行解除引用。也就是说,如果p指向位置6,则p 将是位置5的值,依次类推。程序清单16.10演示了如何使用copy()、istream 迭代器和反向迭代器,
程序清单 16.10 copyit.cpp
// copyit.cpp -- copy() and iterators
#include <iostream>
#include <iterator>
#include <vector>
int main()
{
using namespace std;
int casts[10] = {6, 7, 2, 9 ,4 , 11, 8, 7, 10, 5};
vector<int> dice(10);
// copy from array to vector
copy(casts, casts + 10, dice.begin());
cout << "Let the dice be cast!\n";
// create an ostream iterator
ostream_iterator<int, char> out_iter(cout, " ");
// copy from vector to output
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; // use if auto doesn't work
for (auto ri = dice.rbegin(); ri != dice.rend(); ++ri)
cout << *ri << ' ';
cout << endl;
// cin.get();
return 0;
}
如果可以在显式声明迭代器和使用STL函数来处理内部问题(如通过将rbegin()返回值传递给函数)之间选择,请采用后者。后一种方法要做的工作较少,人为出错的机会也较少。
另外三种迭代器(back insert iterator、ftont insert iterator和insert iterator)也将提高 STL,算法的通用性。很多STL函数都与 copy()相似,将结果发送到输出迭代器指示的位置。前面说过,下面的语句将值复制到从 dice.begin()开始的位置:
copy(casts,casts+10,dice.begin());
这些值将覆盖 dice 中以前的内容,且该函数假设 dice 有足够的空间,能够容纳这些值,即 copy()不能自动根据发送值调整目标容器的长度。程序清单16.10考虑到了这种情况,将 dice 声明为包含 10 个元素。然而,如果预先并不知道 dice 的长度,该如何办呢?或者要将元素添加到 dice 中,而不是覆盖已有的内容,又该如何办呢?
三种插入迭代器通过将复制转换为插入解决了这些问题。插入将添加新的元素,而不会覆盖已有的数据,并使用自动内存分配来确保能够容纳新的信息。backinsertiterator将元插入到容器尾部,而front insert iterator 将元素插入到容器的前端。最后,insert iterator 将元素插入到insert iterator 构造函数的
参数指定的位置前面。这三个插入迭代器都是输出容器概念的模型。
这里存在一些限制。backinsert iterator 只能用于允许在尾部快速插入的容器(快速插入指的是一个时间固定的算法,将在本章后面的“容器概念”一节做进一步讨论),vector类符合这种要求。ftont insent iteratol只能用于允许在起始位置做时间固定插入的容器类型,vector类不能满足这种要求,但queue 满足。insert iterator 没有这些限制,因此可以用它把信息插入到矢量的前端。然而,ffront insert iterator 对于那些支持它的容器来说,完成任务的速度更快。
提示:可以用imnsert iterator将复制数据的算法转换为插入数据的算法。
这些迭代器将容器类型作为模板参数,将实际的容器标识符作为构造函数参数。也就是说,要为名为dice 的 vector容器创建一个back insert iterator,可以这样做:
back insert iterator<vector<int>>back iter(dice);
必须声明容器类型的原因是,迭代器必须使用合适的容器方法。backinsentiterator的构造函数将假设传递给它的类型有一个 push_back()方法。copy()是一个独立的函数,没有重新调整容器大小的权限。但前面的声明让 back iter 能够使用方法 vector::push back(),该方法有这样的权限。声明 ffont insert iterator 的方式与此相同。对于 insert iterator 声明,还需一个指示插入位置的构造函数参数:
insert_iterator<vector<int>>insert_iter(dice, dice.begin());
程序清单 16.11演示了这两种迭代器的用法,还使用for each()而不是 ostream 迭代器进行输出。
程序清单 16.11 inserts.cpp
// inserts.cpp -- copy() and insert iterators
#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;
// construct anonymous back_insert_iterator object
copy(s2, s2 + 2, back_insert_iterator<vector<string> >(words));
for_each(words.begin(), words.end(), output);
cout << endl;
// construct anonymous insert_iterator object
copy(s3, s3 + 2, insert_iterator<vector<string> >(words, words.begin()));
for_each(words.begin(), words.end(), output);
cout << endl;
// cin.get();
return 0;
}
第一个copy()从s1中复制4个字符串到 words中。这之所以可行,在某种程度上说是由于 words 被声明为能够存储4个字符串,这等于被复制的字符串数目。然后,back insert iterator将s2中的字符串插入到 words数组的末尾,将 words的长度增加到6个元素。最后,insert iterator 将s3 中的两个字符串插入到 words 的第一个元素的前面,将 words 的长度增加到8个元素。如果程序试图使用 words.end()和 words.begin( )作为迭代器,将s2和 s3 复制到 words 中,words 将没有空间来存储新数据,程序可能会由于内存违规而异常终止。
如果您被这些迭代器搞晕,则请记住,只要使用就会熟悉它们。另外还请记住,这些预定义迭代器提高了 STL 算法的通用性。因此,copy()不仅可以将信息从一个容器复制到另一个容器,还可以将信息从容器复制到输出流,从输入流复制到容器中。还可以使用copy()将信息插入到另一个容器中。因此使用同一个函数可以完成很多工作。copy()只是是使用输出迭代器的若干STL函数之一,因此这些预定义迭代器也增加了这些函数的功能。
1387

被折叠的 条评论
为什么被折叠?



