标准模板库、容器类、迭代器、泛型编程、函数对象
-
迭代器能够用来遍历容器的对象;函数对象是类似于函数的对象,可以是类对象或函数指针(包括函数名因为函数名被用作指针);STL 不是面向对象的编程,而是泛型编程;
-
模板类 vector p546
-
STL 在头文件 vector 中定义了一个 vector 模板;vector 模板使用动态内存分配,因此可以用初始化参数来指出需要多少矢量:
与 string 类相似,各种 STL 容器模板都接受一个可选的模板参数,该参数指定使用哪个分配器对象来管理内存。如,vector 模板的开头与下面类似:
template <class T, class Allocator = allocator<T>>
class vector{...}
如果该模板省略参数的值,则容器模板将默认使用 allocator<T> 类。这个类使用 new 和 delete。
-
对矢量可执行的操作 p547
size() :返回容器中元素数目 swap():交换两个容器的内容 begin():返回一个指向容器中第一个元素的迭代器 end():返回一个表示超过容器尾的迭代器,即 end() 成员函数标识超过结尾的位置 push_back():将元素添加到矢量末尾 erase():删除矢量中给定区间的元素;它接受两个迭代器参数,第一个迭代器指向区间的起始处,第二个迭代器指向区间终止处的后一个位置 insert():接受 3 个迭代器参数,第一个参数指定新元素插入的位置,第二个和第三个爹嗲气参数定义了被插入区间
每个容器类都定义了一个合适的迭代器;要为 vector 的 double 类型规范声明一个迭代器,可以:
vector<double>::iterator pd; // pd an iterator
vector<double> scores;
pd = scores.begin(); // have pd point to the first element
*pd = 22.3;
++pd;
此外,在初始化迭代器时,也可以使用 auto :
auto pd = scores.begin(); // C++11 automatic type deduction
-
STL 从更广泛的角度定义了非成员(non-member)函数来执行这些操作,即不是为每个容器类定义 find() 函数,而是定义了一个适用于所有容器类的非成员函数 find()。另一方面,即使有执行相同任务的非成员函数,STL 有时也会定义一个成员函数,这是因为对有些操作来说,类特定算法比通用算法高,例如,vector 的成员函数 swap() 的效率比非成员函数 swap() 高。 p550
3 个具有代表性的 STL 函数:for_each()、random_shuffle() 和 sort():
-
for_each()
for_each() 接受 3 个参数,前两个是定义容器中区间的迭代器,最后一个是一个函数对象;for_each() 函数将被指向的函数应用于容器区间中的各个元素;被指向的元素不能修改容器元素的值。可以用 for_each() 函数来代替 for 循环,如:
struct Review{
std::string title;
int rating;
};
...
vector<Review> books;
...
vector<Review>::iterator pr;
for (pr = books.begin(); pr != books.end(); pr++)
ShowReview(*pr);
替换为:
for_each(books.begin(), books.end(), ShowReview);
这样可以避免显式地使用迭代器变量。
-
random_shuffer()
Random_shuffer() 函数接受两个指定区间地迭代器参数,并随机排列该区间中的元素。例如,下面的语句随机排列 books 矢量中的所有元素:
random_shuffle(books.begin(), books.end())
与可用于任何容器类的 for_each() 不同,该函数要求容器类允许随机访问,vector 类可以做到这一点。
-
sort()
sort() 函数也要求容器支持随机访问。该函数有两个版本,第一个版本接受两个定义区间的迭代器参数,并使用为存储在容器中的 < 运算符,对区间中的元素进行操作。例如,下面的语句按升序对 coolstuff 的内容进行排序,排序时使用内置的 < 运算符对值进行比较:
vector<int> coolstuff;
...
sort(coolsutff.begin(), coolstuff.end());
如果容器元素是用户自定义的对象,则要使用 sort(),必须定义能够处理该类型对象的 operator<() 函数(即重载 < 运算符);即,如果为 Review 提供了成员或非成员函数 operator<(),则可以对包含 Review 对象的矢量进行排序。
由于
Review 是一个结构,其成员是公有的,因此可以定义这样的非成员函数(不需是友元)
book operator<(const Review & r1, const Review & r2)
{
if (r1.title < r2.title)
return true;
else if (r1.title == r2.title && r1.rating < r2.rating)
return true;
else
return false;
}
有了上述函数后,可以对包含 Review 对象(如 books)的矢量进行排序:
sort(books.begin(), books.end());
如果按照降序排序,可以使用第二个版本的 sort(),它接受 3 个参数,前两个参数是指定区间的迭代器,最后一个参数是函数对象。
book WorseThan(const Review & r1, const Review & r2)
{
if (r1.rating < r2.rating)
return true;
else
return false;
}
...
sort(books.begin(), books.end(), WorseThan);
-
基于范围的 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 循环中,括号内的代码声明一个类型与容器存储的内容相同的变量,然后指出了容器的名称;接下来,循环体使用指定的变量一次访问容器的每个元素。例如,对于:
for_each(books.begin(), books.end(), ShowReview);
可以更改为:
for (auto x : books) ShowReview(x);
不同于 for_each(),基于范围的 for 循环可以修改容器的内容(利用将参数设置为引用的方法):
void InflateReview(Review &r){r.rating++;}
可使用如下循环对 books 的每个元素执行该函数:
for (auto & x : books) InflateReview(x);
泛型编程
- 为何使用迭代器 p553
泛型编程旨在编写独立于数据类型的代码;STL 是一种泛型编程;
模板使得算法独立于存储的数据类型,迭代器使算法独立于使用的容器类型(同一算法使用不同容器来实现,都可以使用迭代器来遍历;
迭代器应该具备的最基本的特征:
- 应能够对迭代器执行解除引用;即如果 p 是一个迭代器,则应该对 *p 进行定义;
- 应能够将一个迭代器赋给另一个;即如果 p 和 q 是一个迭代器,则应对表达式 p = q 进行定义;
- 应能够将一个迭代器与另一个迭代器进行比较;即如果 p 和 q 是一个迭代器,则应该对 p == q 和 p != q 进行定义;
- 应能够使用迭代器遍历容器中的所有元素,即定义 ++p 和 p++;
实际上,STL 按功能的强弱定义了多种级别的迭代器,常规指针就能满足迭代器的要求
首先看一个在 double 数组中搜索特定值的函数,可以这样编写该函数:
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 0;
}
下面来看搜索另一种数据结构——链表的情况:
struct Node
{
double item;
Node * p_next;
};
...
Node* find_ll(Node * head, const double & val)
{
Node * start;
for (start = head; start != 0; start = start->p_next)
if (start->item == val)
return start;
return 0;
}
泛型编程旨在使用同一个 find() 函数来处理数组、链表或任何其他容器类型,即函数不仅独立于容器中存储的数据类型,而且独立于容器本身的数据结构;模板提供了存储在容器中的数据类型的通用表示,因此还需要遍历容器值的通用表示,迭代器正是这样的通用表示。
可以这样重新编写 find_ar() 函数:
typedef double * iterator;
iterator find_ar(iterator ar, int n, const double & val)
{
for (int i = 0; i < n; i++, ar++)
if (*ar == val)
return ar;
return 0;
}
然后可以修改函数,使之接受两个指示区间的指针参数,其中的一个指向数组的起始位置,另一个指向数组的超尾:
typedef double * iterator;
iterator find_ar(iterator begin, itreator end, const double & val)
{
iterator ar;
for (ar = begin; ar != end; ar++)
if(*ar == val)
return ar;
return end; // indicate val not found
}
对于 find_ll() 函数,可以定义一个迭代器类,其中定义了运算符 * 和 ++:
struct Node
{
double item;
Node * p_next;
}
...
class itreator
{
Node * pt;
public:
itreator() : pt(0) {}
iterator (Node * pn) : pt(pn) {}
double operator*() {return pt->item;}
itreator& operator++()
{
pt = pt->p_next;
return *this;
}
iterator operator++(int)
{
iterator tmp = *this;
pt = pt->p_next;
return tmp;
}
// ... operator==(), operator!=(), etc.
}
为区分 ++ 运算符的前缀版本和后缀,C++ 将 operator++ 作为前缀版本,将 operator++(int) 作为后缀版本;其中的参数永远也不会被用到,所以不必指定名称。
定义该 iterator 类后,可以这样编写 find 函数:
iterator find_ll(iterator head, const double & val)
{
iterator start;
for (start = head; start != 0; ++start)
if (*start == val)
return start;
return 0;
}
find_ar() 函数使用超尾迭代器,而 find_ll() 使用存储在最后一个节点的空值;可以要求链表的最后一个元素后面还有一个额外的元素,即让数组和链表都有超尾元素,并在迭代器达到超尾位置时结束搜索;这样,find_ar() 和 find_ll() 检测数据尾的方式将相同,从而成为相同的算法。
STL 遵循上面介绍的方法:p555
- 每个容器类(vector、list、deque 等)定义了相应的迭代器类型;对于其中的某个类,迭代器可能是指针;对于另一个类,可能是对象;不管类型如何,迭代器都将提供所需的操作,如 * 和 ++。
- 每个容器类都有一个超尾标记,当迭代器递增到超越容器的最后一个值后,这个值将被赋给迭代器;每个容器类都有 begin() 和 end() 方法,它们分别返回一个指向容器的第一个元素和超尾位置的迭代器;每个容器类都使用 ++ 操作。
使用 C++11 新增的自动类型推断可进一步简化:对于矢量或列表,都可使用如下代码:
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;
- 迭代器类型 p556
STL 定义了 5 种迭代器:输入迭代器、输出迭代器、正向迭代器、双向迭代器和随机访问迭代器
例如,find() 函数的原型与下面类似:
template<class InputIterator, class T>
void sort(InputItreator first, InputIterator last, const T& value);
这指出,该算法需要一个输入迭代器。
对于这 5 种迭代器,都可以执行解除引用操作(即为它们定义了 * 运算符),也可以进行比较(== 或 !=);如果两个迭代器相同,则对它们执行解除引用得到的值相同。
- 输入迭代器
迭代器连接容器与程序,输入迭代器指将迭代器中的信息传输到程序中去;即输入迭代器可被程序用来读取容器中的信息;具体的说,对输入迭代器进行解除引用将使程序能够读取容器中的值,但不一定能让程序修改值;因此,需要输入迭代器的算法不会修改容器中的值;
输入迭代器必须能够访问容器中所有的值,但不能保证输入迭代器第二次遍历容器时,顺序不变;另外,输入迭代器被递增后,也不能保证其先前的值仍然可以被解除引用;
注意,输入迭代器是单向迭代器,可以递增,但不能倒退。
- 输出迭代器
STL 使用术语“输出”来指用于将信息从程序传输给容器的迭代器,因此程序的输出就是容器的输入;输出迭代器解除引用让程序能修改容器值,而不能读取。
- 正向迭代器
正向迭代器只使用 ++ 运算符来遍历容器;与输入和输出迭代器不同的是,它总是按相同的顺序遍历一系列值;此外,将正向迭代器递增后,仍然可以对前面的迭代器值解除引用,并得到相同的值。
正向迭代器既可以使得能够读取和修改数据,也可以使得只能读取数据:
int * pirw; // read-write iterator
const int * pir; // read-only iterator
- 双向迭代器
双向迭代器具有正向迭代器的所有特性,同时支持两种(前缀和后缀)递减运算符。
- 随机访问迭代器
随机访问迭代器具有正向迭代器的所有特性,同时添加了支持随机访问的操作(如指针增加运算)和用于对元素进行排序的关系运算符;
- 迭代器的层次结构 p557
迭代器类型形成了一个层次结构,正向迭代器具有输入迭代器和输出迭代器的全部功能,同时还有自己的功能;双向迭代器具有正向迭代器的全部功能,同时还有自己的功能;随机访问迭代器具有正向迭代器的全部功能,同时还有自己的功能。
在编写算法时,应该尽可能使用要求最低的迭代器,并让它适用于容器的最大区间。这样,通过使用级别最低的输入迭代器,find() 函数便可用于任何包含可读取值的容器,而 sort() 函数由于需要随机访问迭代器,所以只能用于支持这种迭代器的容器。(在编写某个算法时,尽量使用某种级别较低迭代器实现;如果某个算法需要使用随机访问迭代器来实现,则不支持随机访问迭代器的容器就不能使用该算法)
每个容器类都定义了一个类级 typedef 名称——iterator,因此 vector<int> 类的迭代器类型为 vector<int>::iterator。然而,该类的文档指出,矢量迭代器是随机访问迭代器,它允许使用基于任何迭代器类型的算法,因为随机访问迭代器具有所有迭代器的功能。list<int>类的迭代器为 list<int>::iterator。STL 实现了一个双向链表,它使用双向迭代器,因此不能使用基于随机访问迭代器的算法,但可以使用基于要求较低的迭代器的算法。(因为 list<int>类支持双向迭代器,因此如果某个算法是使用随机访问迭代器来实现的,list<int> 类就不能使用该算法。而 vector<int> 类支持最高级别的随机访问迭代器,无论一个算法使用输入迭代器或者是随机访问迭代器来实现的,vector<int> 类都可以使用该算法)
- 概念、改进和模型 p558
STL 有若干个用 C++语言无法表达的特性,如迭代器种类;我们可以设计具有正向迭代器特征的类,但不能让编译器将算法限制为只使用这个类;因为正向迭代器是一系列要求,而不是类型。所设计的迭代器类可以满足这种要求,常规指针也能满足这种要求。STL 算法可以使用任何满足其要求的迭代器实现。STL 文献使用术语概念(concept)来描述一系列的要求,因此,存在输入迭代器概念、正向迭代器概念等;
概念可以具有类似继承的关系。例如,双向迭代器继承了正向迭代器的功能;有些 STL 文献使用术语改进(refinement)来表示这种概念上的继承,因此,双向迭代器是对正向迭代器概念的一种改进。
概念的具体实现被称为模型(model)。因此,指向 int 的常规指针是一个随机访问迭代器的模型,也是一个正向迭代器模型,因为它满足该概念的所有要求。
- 将指针用作迭代器 p558
迭代器是广义指针,而指针满足所有的迭代器要求,因此 STL 算法可以使用指针来对基于指针的非 STL 容器进行操作。例如,可以将 STL 算法用于数组:
const int SIZE = 100;
double Receipts[SIZE];
sort(Receipts, Receipts + SIZE); // STL算法可以使用指针来对基于指针的非 STL 容器进行操作
- STL 提供了一些预定义迭代器
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()。STL 为这种迭代器提供了 ostream_iterator模板,该模板是输出迭代器概念的一个模型,它也是一个适配器(adapter)——一个类或函数,可以将一些其他接口转换为 STL 使用的接口。其在头文件 itreator 中。
#include <iterator>
...
ostream_iterator<int, char> out_iter(cout, " ");
out_iter 迭代器现在是一个接口,能够使用 cout 来显式信息。第一个模板参数(这里为 int)指出了被发送给输出流的数据类型;第二个模板参数(这里为 char)指出了输出流使用的字符类型(另一个可能的值是 wchar_t)。构造函数的第一个参数(这里为 cout)指出了要使用的输出流,它也可以是用于文件输出的流;最后一个字符串参数是在发送给输出流的每个数据项后显式的分隔符。
可以这样使用迭代器:
*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), istream_iterator<int, char>(), dice.begin());
与 ostream_iterator 相似,istream_iterator 也使用两个模板参数;第一个参数指出要读取的数据类型,第二个参数指出输入流使用的字符类型;使用构造函数参数 cin 意味着使用由 cin 管理的输入流,省略构造函数参数(第二个参数)表示输入失败,因此上述代码从输入流中读取,直到文件结尾、类型不匹配或出现其他输入故障为止。
- 其他有用的迭代器 p559
除了 ostream_iterator 和 istream_iterator 之外,头文件 iterator 还提供了其他一些专用的预定义迭代器类型:reverse_iterator、back_insert_iterator、front_insert_iterator 和 insert_iterator。
reverse_iterator:
对 reverse_iterator 执行递增操作将导致它被递减。
vector 类有一个名为 rbegin() 的成员函数和一个名为 rend() 的成员函数,前者返回一个指向超尾的反向迭代器,后者返回一个指向第一个元素的反向迭代器。
可以使用下面的语句来反向显示内容:
copy(dice.rbegin(), dice.rend(), out_iter);
甚至不必声明反向迭代器。
rbegin() 和 end() 返回相同的值(超尾),但类型不同(reverse_iterator 和 iterator)。同样,rend() 和 begin() 也返回相同的值(指向第一个元素的迭代器),但类型不同。
此外,必须对反向指针做一种特殊补偿。假设 rp 是一个被初始化为 dice.rbegin() 的反转指针。那么 *rp 是什么呢?因为 rbegin() 返回超尾,因此不能对该地址进行解除引用。同样,如果 rend() 是第一个元素的位置,则 copy() 必须提早一个位置停止,因为区间的结尾处不包括在区间中。
反向指针通过先递减,再解除引用解决了这两个问题;即 *rp 将再 *rp 的当前值之前对迭代器进行解除引用。即,如果 rp 指向位置 6,则 *rp 将是位置 5 的值;
下面的程序演示了如何使用 copy()、istream 迭代器和反向迭代器。
#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());
// 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;
// Implicit use of reverse iterator
copy(dice.rbegin(), dice.rend(), out_iter);
// Explicit use of reverse iterator
vector<int>::reverse_iterator ri;
for (ri = dice.rbegin(); ri != dice.rend(); ++ri) // 对 reverse_iterator 执行 ++ 操作将导致它被递减
cout << *ri << ' ';
}
另外三种迭代器(back_insert_iterator、front_insert_iterator 和 insert_iterator)也将提高 STL 算法的通用性。下面的语句将值赋值到从 dice.begin() 开始的位置:
copy(casts, casts + 10, dice.begin());
这些值将覆盖 dice 中以前的内容,且该函数假设 dice 有足够的空间,能够容纳这些值。即 copy() 不能自动根据发送值调整目标容器的长度。
然而,如果预先并不知道 dice 的长度,或者要将元素添加到 dice 中,而不是覆盖已有内容:
三种插入迭代器通过将复制转换为插入解决了这些问题。插入将添加新的元素,而不会覆盖已有的数据,并使用自动内存分配来确保能够容纳新的信息。
back_insert_iterator 见元素插入到容器尾部;front_insert_iterator 将元素插入到容器的前端;insert_iterator 将元素插入到 insert_iterator 构造函数的参数指定的位置前面。这三个插入迭代器都是输出迭代器概念的模型。
back_insert_iterator 只能用于允许在尾部快速插入的容器;front_insert_iterator 只能用于允许在起始位置做时间固定插入的容器类型;insert_iterator 没有这些限制;然而,front_insert_iterator 对于那些支持它的容器来说,完成任务的速度更快。
这些迭代器将容器类型作为模板参数,将实际的容器标识符作为构造函数参数。也就是说,要为名为 dice 的 vector<int> 容器创建一个 back_insert_iterator,可以使用下面的代码:
back_insert_iterator<vector<int> > back_iter(dice);
必须声明容器类型的原因是,迭代器必须使用合适的容器方法;back_insert_iterator 的构造函数将假设传递给它的类型有一个 push_back() 方法,copy() 是一个独立的函数,没有重新调整容器大小的权限;但前面的声明让 back_iter 能够使用方法 vector<int>::push_back(),该方法有这样的权限。
声明 front_insert_iterator 的方式与此相同。对于 insert_iterator 声明,还需要一个指示插入位置的构造函数参数:
insert_iterator<vector<int> > insert_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); // 注意 for_each() 函数的使用,自动将第三个参数表示的函数用到每个元素上
cout << endl;
// construct anunymous back_insert_iterator object
copy(s2, s2 + 2, back_insert_iterator<vector<string> >(words)); // back_insert_iterator 能自动调整 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(), word.end(), output);
cout << endl;
return 0;
}
在上述程序中,back_insert_iterator 将 s2 中的字符串插入到 words 数组的末尾,将words 的长度增加到 6 个元素;最后,insert_iterator 将 s3 中的两个字符串插入到 words 的第一个元素的前面,将 words 的长度增加到 8 个元素。如果程序试图使用 words.end() 和 words.begin() 作为迭代器,将 s2 和 s3 复制到 words 中,words 将没有空间来存储新数据,程序可能会由于内存违规而异常终止。
- 容器种类 p562
-
STL 具有容器概念和容器类型。概念是具有名称(如容器、序列容器、关联容器等)的通用类别;容器类型是可用于创建具体容器对象的模板。
-
并非任何类型的对象都能存储在容器中,具体地说,类型必须是可复制构造的和可赋值的;基本类型满足这种要求,只要类定义没有将复制构造函数和赋值运算符声明为私有或保护的,则也满足这种要求。
-
序列
七种 STL 容器类型(deque、C++11 新增的 forward_list、list、queue、priority_queue、stack 和 vector)都是序列;序列概念增加了迭代器至少是正向迭代器这样的要求,这保证了元素将按特定的顺序排列,不会在两次迭代之间发生变化。array 也被归类到序列容器,虽然它并不满足序列的所有要求。
下面详细介绍这七种序列容器类型:p565
- vector
vector 是数组的一种类表示,它提供了自动内存管理功能,可以动态地改变 vector 对象的长度,并随着元素的增加和删除而增大和缩小。它提供了对元素的随机访问在尾部添加和删除元素的时间是固定的,但在头部或中间插入和删除元素的复杂度为线性时间。
除序列外,vector 还是可反转容器(reversible container)概念的模型。这增加了两个类方法:rbegin() 和 rend(),这两种方法返回的迭代器都是类级类型 reverse_iterator。
- deque
deque 模板类(在 deque 头文件中声明)表示双端队列(double-ended queue),通常被简称为 deque。在 STL 中,其实现类似于 vector 容器,支持随机访问;主要区别在于,从 deque 对象的开始位置插入和删除元素的时间是固定的。
- list
list 模板类(在 list 头文件中声明)表示双向链表。list 和 vector 的区别在于,list 在链表中任一位置进行插入和删除的时间都是固定的。vector 强调的是通过随机访问进行快速访问,list 强调的是元素的快速插入与删除。
与 vector 类似,list 也是可反转容器,但 list 不支持数组表示法和随机访问。与矢量迭代器不同,从容器中插入或删除元素之后,链表迭代器指向元素不变,即,在链表中插入系元素并不会移动已有的元素,而只是修改链接信息,指向某个元素的迭代器仍然指向该元素,但它链接的元素可能与以前不同。
除了序列和可反转容器的函数外,list 模板类还包含了链表专用的成员函数。如 merge(), remove(), sort(), splice(), unique() 等。 p566
forward_list 容器类实现了单链表。这种链表中,每个节点都只链接到下一个节点,而没有链接到前一个节点,因此 forward_list 只需要正向迭代器,而不需要双向迭代器,因此,forward_list 是不可反转的容器。
- queue
queue 模板类(在头文件 queue 中声明)是一个适配器类。ostream_iterator 模板就是一个适配器,让输出流能够使用迭代器接口;同样,queue 模板让底层类(默认为 deque)展示典型的队列接口。
queue 模板的限制比 deque 更多;它不仅不允许随机访问队列元素,也不允许遍历队列。它把使用限制在定义队列的基本操作上,可以将元素添加到队尾、从队首删除元素、查看队首和队尾的值、检查元素数目和测试队列是否为空。
- priority_queue
priority_queue 模板类(在 queue 头文件中声明)是另一个适配器类,它支持的操作与 queue 相同。两者之间的主要区别在于,在 priority_queue 中,最大的元素被移到队首;内部区别在于,默认的底层类是 vector。
- stack
与 queue 相似,stack(在头文件 stack 中声明)也是一个适配器类,它给底层类(默认情况下为 vector)提供了典型的栈接口。stack 模板不允许随机访问栈元素,不允许遍历栈;它把使用限制在定义栈的基本操作上,即可以将压入推到栈顶、从栈顶弹出元素、查看栈顶的值、检查元素数目和测试栈是否为空。
- array
在头文件 array 中定义,其并非 STL 容器,长度固定,因此 array 没有调整容器大小的操作,但可以随机访问元素。可将很多标准 STL 算法用于 array 对象,如 copy() 和 for_each()。
关联容器 p568
关联容器是对容器概念的另一个改进。关联容器将值与键关联在一起,并使用键来查找值。关联容器允许插入新元素,但不能指定元素的插入位置。原因是关联容器通常有用于确定数据放置位置的算法,以便能够快速检索信息。
关联容器通常是使用某种树实现的。(树的查找速度更快)
STL 提供了 4 种关联容器:set、multiset、map 和 multimap。前两种是在头文件 set 中定义的,后两种是在头文件 map 中定义的。
- 关联容器 set 的值类型与键相同,键是唯一的。这意味着集合中集合中不会有相同的键。对于 set 来说,值就是键。multiset 类似于 set,只是可能有多个值的键相同。
- 在 map 中,值与键的类型不同,键是唯一的,每个键只对应一个值;multimap 与 map 相似,只是一个键可以与多个值相关联。
set 示例
STL set 模拟了多个概念,他是关联集合,可反转,可排序,且键是唯一的,所以不能存储多个相同值。set 使用模板参数来指定要存储的值类型:
set<string> A; // a set of string objects
第二个模板参数是可选的,可用于指示用来对键进行排序的比较函数或对象。默认情况下,将使用模板 less< >;老式 C++ 实现可能没有提供默认值,因此必须显式指定模板参数:
const int N = 6;
string s1[N] = {"buffoon", "thinkers", "for", "heavy", "can", "for"};
set<string> A(s1, s1 + N); // initialize set A using a range from array
ostream iterator<string, char> out(cout, " ");
copy(A.begin(), A.end(), out);
在上述程序中,set 也有一个将迭代器区间作为参数的的构造函数。上述代码表明,键是唯一的,字符串 "for" 在数组中出现了两次,但在 set 集合中只出现了一次,且集合被排序。
STL 提供了一些数学操作(如并集)的通用函数(它们并非只能用于 set 对象)。所有 set 对象都自动满足使用这些算法的先决条件,即容器是经过排序的。
set_union() 函数接受 5 个迭代器参数:前两个迭代器定义了第一个集合的区间,接下来的两个定义了第二个集合的区间,最后一个迭代器是输出迭代器,指出将结果集合复制到什么位置。例如,要显示 A 和 B 的并集,可以这样做:
set_union(A.begin(), A.end(), B.begin(), B.end(), ostream_iterator<string, char> out(cout, " "));
假设要将结果放入集合 C 中,而不是显示它,则最后一个参数应是指向 C 的迭代器。但是不能是 C.begin()。首先,关联集合将键看作常量,所以 C.begin() 返回的迭代器是常量迭代器,不能用作输出迭代器;其次,set_union 将覆盖容器中已有的数据,并要求容器有足够的空间容纳新信息。C 是空的,不能满足这种要求。但模板 insert_iterator 可以解决这两个问题。它可以将复制转换为插入;它还模拟了输出迭代器的概念,可以用它将信息写入容器。因此,可以创建一个匿名 insert_iterator ,将信息复制给 C:
set_union(A.begin(), A.end(), B.begin(), B.end(), insert_iterator<set<string> >(C, C.begin())); //p561
multimap 示例:
multimap 是可反转的,经过排序的关联容器,但键和值的类型不同,且同一个键可能与多个值相关联。基本的 multimap 声明使用模板参数指定键的类型和存储的值的类型。下面的声明创建一个 multimap 对象,其中键类型为 int ,存储的值类型为 string:
multimap<int, string> codes;
第三个模板参数是可循阿德,指出用于对键进行排序的比较函数或对象。在默认情况下,将使用模板 less< >,将模板将键类型作为参数。
在 map 中,实际的值类型将键类型和数据类型结合为一对;为此,STL 使用模板类 pair<class T, class U>
将这两种值存储到一个对象中,如果 keytype 是键类型,而 datatype 是存储的数据类型,则值类型为 pair<const keytype, datatype>
。例如,前面声明的 codes 对象的值类型为 pair<const int, string>
。
示例:
假设用区号作为键来存储城市名,一种方法是创建一个 pair,再将它插入:
pair<const int, string> item(213, "Los Angeles");
codes.insert(item);
或使用一条语句创建匿名 pair 对象并将它插入:
codes.insert(pair<const int, string>) (213, "Los Angeles")); // 注意,pair<const int, string> 是根据模板类 pair<class T, class U> 产生的具体类
因为数据项是按键排序的,所以不需要指出插入位置。对于 pair 对象,可以使用 first 和 second 成员来访问其两个部分:
pair<const int, string> item(213, "Los Angeles");
cout << item.first << ' ' << item.second << endl;
numtimap 的成员函数 count() 接受键作为参数,并返回具有该键的元素数目。
函数对象
很多 STL 算法都是用函数对象——也叫函数符(functor)。函数符是可以以函数方式与()结合使用的任意对象,包括函数名、指向函数的指针和重载了 () 运算符的类对象(即定义了函数 operator()() 的类)。 p572
class Linear
{
private:
double slope;
double y0;
public:
Linear(double s1_ = 1, doble y_ = 0) : slope(s1_), y0(y_) {}
double operator()(double x) {return y0 + slope * x;}
};
...
Linear f1;
linear f2(2.5, 10.0);
double y1 = f1(12.5); // f1.operator()(12.5),使用()运算符重载函数
for_each() 函数中,第三个参数可以是常规函数,也可以是函数符。
- 函数符概念 p573
- 生成器(generator)是不用参数就可以调用的函数符
- 一元函数(unary function)是用一个参数可以调用的函数符
- 二元函数(binary function)是用两个参数可以调用的函数符
例如,提供给 for_each() 的函数符应当是一元函数,因为它每次用于一个容器元素。当然,这些概念都有相应的改进版:
- 返回 bool 值的一元函数是谓词(predicate)
- 返回 bool 值的二元函数是二元谓词(binary predicate)
算法
STL 包含很多处理容器的非成员函数,如 sort(), copy(), find(), random_shuffle(), set_union, set_intersection(), set_difference(), transform()。
对于算法函数设计,有两个主要的通用部分。首先,它们都使用模板来提供泛型;其次,它们都使用迭代器来提供访问容器中数据的通用表示。
可以使用 == 来比较不同类型的容器,如 deque 和 vector。因为容器重载的 == 运算符使用迭代器来比较内容。
STL 将算法库分成 4 组:
- 非修改式序列操作
- 修改式序列操作
- 排序和相关操作
- 通用数字运算
前三组在头文件 algorithm 中描述,第四组在头文件 numeric 中描述。
算法的通用特征:
STL 函数使用迭代器和迭代器区间,例如,copy() 函数的原型如下:
template<class InputIterator, class OutputIteerator>
OutputIterator copy(InputIterator first, InputIterator last, OutputIterator result);
标识符 InputIterator 和 OutputIterator 都是模板参数,类似于模板参数 T 和 U 一样。STL 文档使用模板参数名称来表示参数模型的概念,即上述声明告诉哦我们,区间参数必须是输入迭代器或更高阶别的迭代器,而指示结果存储位置的迭代器必须是输出迭代器或更高级别的迭代器。
- 就地算法和复制算法
对算法分类的方式之一是按照结果的位置进行分类。有些算法就地完成工作,有些则创建拷贝。例如,在 sort() 函数完成时,结果被存放在原始数据的位置上,因此 sort() 是就地算法;而 copy() 函数将结果发送到另一个位置,因此它是复制算法。
有些算法有两个版本:就地版本和复制版本。STL 的约定是,复制版本的名称将以 _copy 结尾。复制版本将接受一个额外的输出迭代器参数,该参数指定结果的放置位置。例如 replace() 函数的原型如下:
template<class ForwardIterator, class T>
void replace(ForwardIterator first, ForwardIterator last, const T& old_value, const T& new_value);
template<class InputItreator, class OutputIterator, class T>
Output Iterator replace_copy(InputIterator first, InputIterator last, OutputIterator result, const T& old_value, const T& new_value);
注意,replace_copy() 的返回类型为 OutputIterator。对于复制算法,统一的约定是:返回一个迭代器,该迭代器指向复制的最后一个值后面的一个位置。