《C++ Primer Plus 第6版》第16章 string类和标准模板库 学习笔记——标准模板库和泛型编程

1. 标准模板库

1.1 模板类vector

分配器:与string类相似,各种STL容器模板都接受一个可选的模板参数,该参数指定使用哪个分配器对象来管理内存。例如,vector模板的开头与下面类似:

template <class T, class Allocator = allocator<T> >
    class vector{
        //...
    };

如果省略该模板参数的值,则容器模板将默认使用allocator<T>类。这个类使用new和delete。

1.2 可对矢量执行的操作

什么是迭代器?它是一个广义指针。通过将指针广义化为迭代器,让STL能够为各种不同的容器类,提供统一的接口。该迭代器的类型是一个名为iterator的typedef,其作用域为整个类

vector<double>::iterator pd;      //pd是vector<double>类的一个迭代器对象

push_back()方法,将元素添加到矢量末尾。

erase()方法删除矢量中给定区间的元素。它接受两个迭代器参数,这些参数定义了要删除的区间。第一个迭代器指向区间的起始处,第二个迭代器位于区间终止处的后一个位置。注意,由于vector提供了随机访问功能,因此vector类迭代器定义了诸如begin()+2等操作:

scores.erase(scores.begin(), scores.begin()+2);  //删除scores的第1,2个元素,即begin()和begin()+1指向的元素

insert()方法接受3个迭代器参数。第一个参数制定了新元素插入的位置,第二个和第三个迭代器参数定义了被插入区间,该区间通常是另一个容器对象的一部分。

vector<int> old_v;
vector<int> new_v;
//...
//插入old_v矢量的一个元素前面
old_v.insert(old_v.begin(), new_v.begin()+1, new_v.end());  

1.3 对矢量可执行的其他操作

程序员通常要对数组执行很多操作,如搜索、排序、随机排列等。矢量模板类包含类执行这些常见的操作的方法吗?没有!STL从更广泛的角度定义了非成员函数来执行这些操作。即定义了一个适用于所有容器类的非成员函数find()。

另一方面,即使有执行相同任务的非成员函数,STL有时也会定义一个成员函数,因为对有些操作来说,类特定算法的效率比通用算法高。因此vector的成员函数swap()的效率比非成员函数swap()高,但非成员函数能够交换两个类型不同的容器的内容。

下面研究3个具有代表性的STL函数:for_each()、random_shuffle()、sort()。

for_each()接受3个参数。前两个是定义容器中区间的迭代器,最后一个是函数对象。for_each()函数将被指向的函数用于容器区间中的各个元素,被指向的函数不能修改容器元素的值。可以用for_each()函数来代替for循环:

vector<Review>::iterator pr;
for(pr = books.begin(); pr != books.end(); pr++)
    ShowReview(*pr);
//上述for循环可替换为下列for_each()函数:
for_each(books.begin(), books.end(), ShowReview);

random_shuffle()函数接收两个指定区间的迭代器参数,并随机排列该区间中的元素。注意该函数要求容器类允许随机访问,vector类可以做到这一点。

sort()函数也要求容器支持随机访问。该函数有两个版本,第一个:接受两个定义区间的迭代器参数,并使用为存储在容器中的类型元素定义的<运算符,对区间中的元素进行操作。下面的语句按升序排序,排序时用内置的<运算符对值进行比较:

vector<int> coolstuff;
//...
sort(coolstuff.begin(), coolstuff.end());

如果容器元素是用户定义的对象,则要使用sort()对象,必须定义能够处理该类型对象的operator<()函数。

第二个版本:接受3个参数,前两个参数是指定区间的迭代器,第3个参数是指向要使用的函数的指针(函数对象):

bool WorseThan(const Review& r1, const Review& r2)
{
    if(r1.rating < r2.rating)
        return true;
    else
        return false;
}
sort(books.begin(), books.end(), WorseThan);

全排序(rotal ordering)、完整弱排序(strick weak ordering)

1.4 基于范围的for循环(C++11)

基于范围的for循环是为用于STL而设计的。

double prices[5] = { 4.99, 10.99, 6.87, 7.99, 8.49 };
for(double x: prices)
    cout << x << std:endl;

不同于for_each(),基于范围的for循环可修改容器的内容,诀窍是指定一个引用参数:

void InflateReview(Review& r) { r.rating++; }
//可使用如下循环对books的每个元素执行该函数:
for(auto& x: books)
    InflateReview(x);

2. 泛型编程

面向对象编程关注的是编程的数据方面,而泛型编程关注的是算法。

2.1 为何使用迭代器

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

STL遵行一些规则:首先,每个容器类(vector、list、deque等)定义了相应的迭代器类型。对于其中的每个类,迭代器可能是指针;而对于另一个类,则可能是对象。其次,每个容器类都有一个超尾标记,每个容器类都有begin()和end()方法。每个容器类都使用++操作,用于遍历容器中的每一个元素。

使用C++11新增的自动类型推断可进一步简化:

list<double>::iterator pr;
for(pr = scores.begin(); pr != scores.end(); pr++)
    cout << *pr << endl;
//可以简化为:
for(auto pr = scores.begin(); pr != scores.end(); pr++)
    cout << *pr << endl;

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

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

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

2.2 迭代器类型

STL定义了5种迭代器,并根据所需的迭代器类型对算法进行了描述。分别为:输入迭代器、输出迭代器、正向迭代器、双向迭代器、随机访问迭代器。

//find()的原型与下面类似,这指出这种算法需要一个输入迭代器
template<class InputIterator, class T>
InputIterator find(InputIterator first, InputIterator last, const T& value);
//排序算法的原型指出需要一个随机访问迭代器:
template<class RandomAccessIterator>
void sort(RandomAccessIterator first, RandomAccessIterator last);

(1)输入迭代器

输入迭代器可被程序用来读取容器中的信息。需要输入迭代器的算法将不会修改容器中的值。输入迭代器必须能够访问容器中所有的值,这通过支持++运算符来实现。注意,并不能保证输入迭代器第二次遍历容器时,顺序不变。输入迭代器被递增后,也不能保证其先前的值仍然可以被解除引用。

基于输入迭代器的任何算法都应当是单通行的,不依赖于前一次遍历时的迭代器值,也不依赖于本次遍历中前面的迭代器值。

输入迭代器是单向迭代器,可以递增,但不能倒退。

(2)输出迭代器

与输入迭代器类似,只是解除引用让程序能过够修改容器值,而不能读取。发送到显示器上的输出就是如此,cout可以修改发送到显示器的字符流,但不能读取屏幕上的内容。

简而言之,对于单通行、只读算法,用输入迭代器;对于单通行、只写算法,用输出迭代器。

(3)正向迭代器

正向迭代器只使用++运算符来遍历容器,所以它每次沿容器向前移动一个元素。与输入、输出迭代器不同,它总是按相同的顺序遍历一系列值。另外,将正向迭代器递增后,仍然可以对前面的迭代器值解除引用(如果保存了它),并可以得到相同的值。这些特征使得多次通行算法成为可能。

正向迭代器既可以读取+修改数据,也可以只读数据:

int* pirw;     //read-write iterator
const int* pir;  //read-only iterator

(4)双向迭代器

双向迭代器拥有正向迭代器的所有属性,同时支持两种(前缀和后缀)递减运算符。

(5)随机访问迭代器

有些算法(如标准排序和二分检索)要求能够直接跳到容器中的任何一个元素,这叫做随机访问。随机访问迭代器具有双向迭代器的所有特性,同时添加了支持随机访问的操作(如指针增加运算)和用于对元素进行排序的关系运算符。表16.3列出了除双向迭代器的操作外,随机访问迭代器还支持的操作。其中,X表示随机迭代器类型,T表示被指向的类型,a和b都是迭代器值,n为整数,r为随机迭代器变量或引用。

随机访问迭代器操作
表达式描  述
a + n指向a所指向的元素后的第n个元素
n + a与a+n相同
a - n指向a所指向的元素前的第n个元素
r += n

等价于 r = r + n

r -= n等价于 r = r - n
a[n]等价于*(a+n)
b - a结果为这样的n值,即 b = a + n
a < b如果 b - a > 0, 则为真
a > b如果 b < a, 则为真
a >= b如果!(a < b),则为真
a <= b如果!(a > b),则为真

像a+n这样的表达式仅当a和a+n都位于容器区间(包括超尾)内时,才合法。

2.3 迭代器层次结构

注意到,迭代器类型形成了一个层次结构。

根据特定迭代器类型编写的算法可以使用该种迭代器,也可以使用具有所需功能的任何其他迭代器。所以具有随机访问迭代器的容器可以使用为输入迭代器编写的算法。

之所以有这么多类型的迭代器,目的是为了在编写算法时尽可能使用要求最低的迭代器,并让它适用于容器的最大区间。这样,通过使用级别最低的输入迭代器,find()函数便可用于任何包含可读取值的容器。而sort()函数由于需要随机访问迭代器,所以只能用于支持这种迭代器的容器。

vector的迭代器是随机访问迭代器,它允许使用基于任何迭代器类型的算法。list<int>类的迭代器类型为list<int>::iterator,STL实现了一个双向链表,它使用双向迭代器,因此不能使用基于随机访问迭代器的算法。

2.4 概念、改进和模型

(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);

同样,可以将STL算法用于自己设计的数组形式,只要提供适当的迭代器(可以是指针,也可以是对象)和超尾迭代器即可。

copy()、ostream_iterator、istream_iterator

copy()的前两个参数表示要复制的范围,最后一个迭代器参数表示要将第一个元素复制到什么位置。前两个参数必须是(或最好是)输入迭代器,最后一个参数必须是(或最好是)输出迭代器。Copy()函数将覆盖目标容器中已有的数据,同时目标容器必须足够大,以便能够容纳被复制的元素。

现在,假设要将信息复制到显示器上。如果有一个表示输出流的迭代器,则可以使用copy()。STL为这种迭代器提供了ostream_iterator模板。用STL的话说,该模板是输出迭代器概念的一个模型,它也是一个适配器(adapter)——一个类或函数,可以将一些其他接口转换为STL使用的接口。

创建这种迭代器:

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

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

//下式意味着将15和空格组成的字符串发送到cout管理输出流中,并为下一个输出操作做好了准备。
*out_iter++ = 15;
//可以将copy()用于迭代器:
copy(dice.begin(), dice.end(), out_iter);
//这意味着将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());

使用构造函数参数cin意味着使用由cin管理的输入流,省略构造函数参数表示输入失败,因此上述代码从输入流中读取,直到文件结尾、类型不匹配或出现其他输入故障为止。

(2)其他有用的迭代器

除了ostream_iterator和istream_iterator外,头文件iterator还提供了其他一些专用的预定义迭代器类型。它们是reverse_iterator、back_insert_iterator、front_insert_iterator和insert_iterator。

reverse_iterator:对其执行递增操作将导致它被递减。

vector类有一个rbegin()的成员函数和一个名为rend()的成员函数,前者返回一个指向超尾的反向迭代器,后者返回一个指向第一个元素的反向迭代器。可以使用下面的语句来反向显示内容:

copy(dice.rbegin(), dice.rend(), out_iter);

注意rbegin()和end()返回相同的值(超尾),但类型不同。这里有一个问题,必须对反向指针做一种特殊补偿,因为rbegin()返回超尾,那么对其解引用会有错。反向迭代器通过先递减,再解除引用的方式解决这两个问题,即,如果rp指向位置6,则*rp将是位置5的值,依次类推。

// 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.end(), ++ri)
        cout << *ri << ' ';
    cout << endl;

    return 0;
}

如果可以在显式声明迭代器和使用STL函数来处理内部问题(如通过将rbegin()返回值传递给函数)之间选择,请采用后者。

另外三种迭代器(back_insert_iterator、front_insert_iterator、insert_iterator)也将提高STL算法的通用性。

copy()函数不能自发根据发送值调整目标容器的长度,如果预先不知道目标容器的长度,该怎么办呢?或者要将元素添加到目标容器中,而不是覆盖已有的内容,怎么办呢?

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

一些重要的限制:back_insert_iterator只能用于允许在尾部快速插入的容器(快插算法),vector类符合。front_insert_iterator只能用于允许在起始位置做时间固定插入的容器类型,vector类不满足,queue满足。insert_iterator没有这些限制,因此可以用它将信息插入到矢量的前端,但是对于支持前两种的容器来说,前两种迭代器更快。

要为名为dice的vector<int>容器创建一个back_insert_iterator(front_insert_iterator同理):

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

必须声明容器类型的原因是:迭代器必须使用合适的容器方法。

对于insert_iterator声明,还需要一个指示插入位置的构造函数参数:

insert_iterator<vector<int> > insert_iterator(dice, dice.begin());
// 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[3] = { "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;
}

注意,这些预定义迭代器提高了STL算法的通用性,因此copy()不仅可以将信息从一个容器复制到另一个容器,还可以将信息从容器复制到输出流,从输入流复制到容器中。还可以使用copy()将信息插入到另一个容器中。因此使用同一个函数可以完成很多操作。

2.5 容器种类

STL具有容器概念和容器类型。以前的11个容器类型分别是deque、list、queue、priority_queue、stack、vector、map、multimap、set、multiset 和 bitset(比特级处理数据的容器)。C++11新增了forward_list、unordered_map、unordered_multimap、unordered_set和unordered_multiset,且不再将bitset视为容器,而是将其视为一种独立的类别。

序列:序列(sequence)是一种重要的改进。7种STL容器类型(deque、C++11新增的forward_list、list、queue、priority_queue、stack 和 vector)都是序列。序列概念增加了迭代器至少是正向迭代器的要求,这保证了元素将按特定顺序排列,不会在两次迭代之间发生变化。array也被归类到序列容器,虽然它并不满足序列的所有要求。序列还要求其元素按严格的线性顺序排列。数组和链表都是序列,但分支结构不是。

因为序列中的元素具有确定的顺序,因此可以执行诸如将值插入到特定位置、删除特定区间等操作。表16.7列出了这些操作以及序列必须完成的其他操作。X表示容器类型,如vector;T表示存储在容器中的对象类型;a 和 b 表示类型为X的值;r 表示类型为X&的值;u 表示类型为X的标识符(即如果X表示vector<int>,则u是一个vector<int>对象);t 表示类型为T的值;n 表示整数;p、q、i、j 表示迭代器。

表16.7 序列的要求
表达式返回类型说  明
X a(n, t);声明一个名为 a 的 n 个 t 值组成的序列
X(n, t)创建一个由 n 个 t 值组成的匿名序列
X a(i, j)声明一个名为 a 的序列,并将其初始化为区间 [i, j) 的内容
X(i, j)创建一个匿名序列,并将其初始化为区间 [i, j) 的内容
a.insert(p, t)迭代器将 t 插入到 p 的前面
a.insert(p, n, t)void将 n 个 t 插入到 p 的前面

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值