当前位置:嗨网首页>书籍在线阅读

22-概念、改进和模型

  
选择背景色: 黄橙 洋红 淡粉 水蓝 草绿 白色 选择字体: 宋体 黑体 微软雅黑 楷体 选择字体大小: 恢复默认

16.4.4 概念、改进和模型

STL有若干个用C++语言无法表达的特性,如迭代器种类。因此,虽然可以设计具有正向迭代器特征的类,但不能让编译器将算法限制为只使用这个类。原因在于,正向迭代器是一系列要求,而不是类型。所设计的迭代器类可以满足这种要求,常规指针也能满足这种要求。STL算法可以使用任何满足其要求的迭代器实现。STL文献使用术语概念(concept)来描述一系列的要求。因此,存在输入迭代器概念、正向迭代器概念,等等。顺便说一句,如果所设计的容器类需要迭代器,可考虑STL,它包含用于标准种类的迭代器模板。

概念可以具有类似继承的关系。例如,双向迭代器继承了正向迭代器的功能。然而,不能将C++继承机制用于迭代器。例如,可以将正向迭代器实现为一个类,而将双向迭代器实现为一个常规指针。因此,对C++而言,这种双向迭代器是一种内置类型,不能从类派生而来。然而,从概念上看,它确实能够继承。有些STL文献使用术语改进(refinement)来表示这种概念上的继承,因此,双向迭代器是对正向迭代器概念的一种改进。

概念的具体实现被称为模型(model)。因此,指向int的常规指针是一个随机访问迭代器模型,也是一个正向迭代器模型,因为它满足该概念的所有要求。

1.将指针用作迭代器

迭代器是广义指针,而指针满足所有的迭代器要求。迭代器是STL算法的接口,而指针是迭代器,因此STL算法可以使用指针来对基于指针的非STL容器进行操作。例如,可将STL算法用于数组。假设Receipts是一个double数组,并要按升序对它进行排序:

const int SIZE = 100;
double Receipts[SIZE];

STL sort()函数接受指向容器第一个元素的迭代器和指向超尾的迭代器作为参数。&Receipts[0](或Receipts)是第一个元素的地址,&Receipts[SIZE](或Receipts + SIZE)是数组最后一个元素后面的元素的地址。因此,下面的函数调用对数组进行排序:

sort(Receipts, Receipts + SIZE);

C++确保了表达式Receipts + n是被定义的,只要该表达式的结果位于数组中。因此,C++支持将超尾概念用于数组,使得可以将STL算法用于常规数组。由于指针是迭代器,而算法是基于迭代器的,这使得可将STL算法用于常规数组。同样,可以将STL算法用于自己设计的数组形式,只要提供适当的迭代器(可以是指针,也可以是对象)和超尾指示器即可。

copy()、ostream_iterator和istream_iterator

STL提供了一些预定义迭代器。为了解其中的原因,这里先介绍一些背景知识。有一种算法(名为copy())可以将数据从一个容器复制到另一个容器中。这种算法是以迭代器方式实现的,所以它可以从一种容器到另一种容器进行复制,甚至可以在数组之间复制,因为可以将指向数组的指针用作迭代器。例如,下面的代码将一个数组复制到一个矢量中:

int casts[10] = {6, 7, 2, 9 ,4 , 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(以前为iterator.h)并作下面的声明来创建这种迭代器:

#include <iterator>
...
ostream_iterator<int, char> out_iter(cout, " ");

out_iter迭代器现在是一个接口,让您能够使用cout来显示信息。第一个模板参数(这里为int)指出了被发送给输出流的数据类型;第二个模板参数(这里为char)指出了输出流使用的字符类型(另一个可能的值是wchar_t)。构造函数的第一个参数(这里为cout)指出了要使用的输出流,它也可以是用于文件输出的流(参见第17章);最后一个字符串参数是在发送给输出流的每个数据项后显示的分隔符。

可以这样使用迭代器:

*out_iter++ = 15; // works like cout << 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_iterator<int, 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_iterator<int, 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_iterator和iterator)。同样,rend()和begin()也返回相同的值(指向第一个元素的迭代器),但类型不同。

必须对反向指针做一种特殊补偿。假设rp是一个被初始化为dice.rbegin()的反转指针。那么*rp是什么呢?因为rbegin()返回超尾,因此不能对该地址进行解除引用。同样,如果rend()是第一个元素的位置,则copy()必须提早一个位置停止,因为区间的结尾处不包括在区间中。

反向指针通过先递减,再解除引用解决了这两个问题。即rp将在rp的当前值之前对迭代器执行解除引用。也就是说,如果rp指向位置6,则*rp将是位置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;
    for (ri = dice.rbegin(); ri != dice.rend(); ++ri)
        cout << *ri << ' ';
    cout << endl;
    return 0;
}

程序清单16.10中程序的输出如下:

Let the dice be cast!
6 7 2 9 4 11 8 7 10 5
Implicit use of reverse iterator.
5 10 7 8 11 4 9 2 7 6
Explicit use of reverse iterator.
5 10 7 8 11 4 9 2 7 6

如果可以在显式声明迭代器和使用STL函数来处理内部问题(如通过将rbegin()返回值传递给函数)之间选择,请采用后者。后一种方法要做的工作较少,人为出错的机会也较少。

另外三种迭代器(back_insert_iterator、front_insert_iterator和insert_iterator)也将提高STL算法的通用性。很多STL函数都与copy()相似,将结果发送到输出迭代器指示的位置。前面说过,下面的语句将值复制到从dice.begin()开始的位置:

copy(casts, casts + 10, dice.begin());

这些值将覆盖dice中以前的内容,且该函数假设dice有足够的空间,能够容纳这些值,即copy()不能自动根据发送值调整目标容器的长度。程序清单16.10考虑到了这种情况,将dice声明为包含10个元素。然而,如果预先并不知道dice的长度,该如何办呢?或者要将元素添加到dice中,而不是覆盖已有的内容,又该如何办呢?

三种插入迭代器通过将复制转换为插入解决了这些问题。插入将添加新的元素,而不会覆盖已有的数据,并使用自动内存分配来确保能够容纳新的信息。back_insert_iterator将元素插入到容器尾部,而front_insert_iterator将元素插入到容器的前端。最后,insert_iterator将元素插入到insert_iterator构造函数的参数指定的位置前面。这三个插入迭代器都是输出容器概念的模型。

这里存在一些限制。back_insert_iterator只能用于允许在尾部快速插入的容器(快速插入指的是一个时间固定的算法,将在本章后面的“容器概念”一节做进一步讨论),vector类符合这种要求。front_insert_iterator只能用于允许在起始位置做时间固定插入的容器类型,vector类不能满足这种要求,但queue满足。insert_iterator没有这些限制,因此可以用它把信息插入到矢量的前端。然而,front_insert_iterator对于那些支持它的容器来说,完成任务的速度更快。

提示: 可以用insert_iterator将复制数据的算法转换为插入数据的算法。

这些迭代器将容器类型作为模板参数,将实际的容器标识符作为构造函数参数。也就是说,要为名为dice的vector容器创建一个back_insert_iterator,可以这样做:

back_insert_iterator<vector<int> > back_iter(dice);

必须声明容器类型的原因是,迭代器必须使用合适的容器方法。back_insert_iterator的构造函数将假设传递给它的类型有一个push_back()方法。copy()是一个独立的函数,没有重新调整容器大小的权限。但前面的声明让back_iter能够使用方法vector::push_back(),该方法有这样的权限。

声明front_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;
    return 0;
}

程序清单16.11中程序的输出如下:

fine fish fashion fate
fine fish fashion fate busy bats
silly singers fine fish fashion fate busy bats

第一个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函数之一,因此这些预定义迭代器也增加了这些函数的功能。