【C++】【C++ Primer】10-泛型算法
- 1 概述
- 2 初识泛型算法
- 3 定制操作
- 4 再探迭代器
- 7 练习
- 7.1 编写程序,读取int序列存入vector中,打印有多少个元素的值等于给定值。
- 7.2 重做上一题,读取string序列存入list中。
- 7.3 用accumulate求一个vector\中元素之和。
- 7.4 假定v是一个vector\,那么调用accumulate(v.cbegin(), v.cend(), 0)有何错误?
- 7.5 如果roster1和roster2中保存的都是C风格字符串,执行equal(roster1.cbegin(), roster1.cend(), roster2.cbegin());会发生什么?
- 7.6 编写程序,使用fill_n将一个序列中的int值都设置为0。
- 7.7 下面程序是否有错误?如有请改正。
- (a)vector\ vec;
- list\ lst;
- int I;
- while (cin >> i) {
- lst.push_back(i);
- }
- copy(lst.cbegin(), lst.cend(), vec.begin());
- (b)vector\ vec;
- vec.reserve(10);
- fill_n(vec.begin(), 10, 0);
- 7.8 我们说“标准库算法不会改变它们所操作容器的大小”,为什么使用back_inserter不会使这一断言失效?
1 概述
标准库定义了一组泛型算法,来实现查找、替换、删除、排序等功能。之所以称之为“算法”,是因为它们实现了经典算法的公共接口。之所以称之为“泛型”,是因为它们可以用于不同元素的多种容器类型(甚至可以用于数组等其他类型的序列)。
大多数算法都定义在头文件algorithm中,头文件numeric中也定义了一组数组泛型算法。
1.1 泛型算法的原理
泛型算法不直接操作容器,而是遍历由两个迭代器指定的元素范围,对其中每个元素进行一些处理。
1.2 标准库算法find——查找容器中是否包含特定值
1.2.1 参数说明
- 参数一:元素范围起始迭代器;
- 参数二:元素范围终止迭代器;
- 参数三:待查找的数值。
1.2.2 返回值说明
find返回第一个等于给定值的元素的迭代器。如果在给定范围内没有待查找数值,则返回第二个参数,表示查找失败。因此可以对比返回值和第二个参数来判断是否查找成功。
int val = 42;
auto result = find(vec.begin, vec.end(), val);
cout << "The value " << val << (result == vec.cend() ? " is not present" : "is present") << endl;
1.2.3 find函数的适用性
find操作迭代器,所以可以在任何容器中查找值。由于指针就像内置数组的迭代器一样,所以也可以用find在数组中查找值:
int ia[] = {27, 210, 12, 47, 109, 83};
int val = 83;
int *result = find(begin(ia), end(ia), val);
1.2.4 算法永远不会执行容器的操作
泛型算法运行在迭代器之上,执行迭代器的操作,不会执行容器的操作。这就带来了一个编程假定——算法永远不会改变底层容器的大小。算法可能改变容器中保存的元素的值,也可能在容器中移动元素,但永远不会直接添加或删除元素。
2 初识泛型算法
除了少数例外,标准库算法都对一个范围内的元素进行操作,由算法的前两个参数来表示。我们将这个元素范围称为“输入范围”。
2.1 只读算法
只读算法只读取其输入范围内的元素,却不改变元素。
2.1.1 count函数
count函数定义在头文件algorithm中。
count函数接受一对迭代器和一个值作为参数,count返回给定值在序列中出现的次数。
int val = 5;
vector<int> vec = {1, 2, 3, 4, 5, 6, 5};
cout << "Num of 5 is " << count(vec.begin(), vec.end(), val) << endl;
2.1.2 accumulate函数
accumulate函数定义在头文件numeric中。
accumulate函数接受三个参数,前两个指出需要求和的元素范围,第三个参数是和的初值。第三个参数的类型决定了函数中使用哪个加法运算符以及返回值的类型。
vector<int> vec = {1, 2, 3, 4};
int sum1 = accumulate(vec.begin(), vec.end(), 0); // sum1为10
int sum2 = accumulate(vec.begin(), vec.end(), 5); // sum2为15
2.1.3 算法和元素类型
accumulate将第三个参数作为求和起点,这蕴含着一个编程假定:序列中元素的类型必须与第三个参数匹配,或者能够转换为第三个参数的类型。
string sum = accumulate(v.cbegin(), v.cend()), string("")); // 正确
string sum = accumulate(v.cbegin(), v.cend(), ""); // 错误。类型是const char *,没有+运算符
2.1.4 equal函数
equal算法用于确定两个序列是否保存相同的值。它将第一个序列中的每个元素与第二个序列中的对应元素进行比较。如果所有元素都相等则返回true,否则返回false。
equal算法接受三个迭代器:前两个表示第一个序列中的元素范围,第三个表示第二个序列的首元素。
由于equal利用迭代器完成操作,所以可以通过equal来比较两个不同类型的容器中的元素,元素类型也不必一样,只要能用==来比较两个元素类型即可。譬如以下代码:
vector<string> vec = {string("aaa"), string("bbb"), string("ccc")};
list<const char *> lst = {"aaa", "bbb", "ccc"};
cout << equal(vec.cbegin(), vec.cend(), lst.cbegin()) << endl;
2.1.5 操作两个序列的算法
操作两个序列的算法之间的区别在于如何传递第二个序列。
类似equal这种只接受单一迭代器来表示第二个序列的算法,都假定第二个序列至少与第一个序列一样长。简而言之,要确保第一个序列中的每个元素在第二个序列中都有对应元素。
2.2 写容器元素的算法
一些算法将新值赋予序列中的元素。使用这类算法时,必须确保序列原大小至少不小于要求算法写入的元素数目。因为算法不执行容器操作,所以算法自身无法改变容器的大小。
注意,泛型算法对容器的要求并不是有足够空间,而是有足够元素。或者说,不是要求capacity足够大,而是要求size足够大。
vector<int> vec;
vec.reserve(10); // capacity足够,但元素不够多,所以fill_n失败
vec.resize(10); // size足够,fill_n成功
fill_n(vec.begin(), vec.capacity(), 6);
2.2.1 fill函数
fill算法接受三个参数。前两个参数是一对迭代器,表示元素范围。第三个参数是将要赋予输入序列每个元素的数值。
vector<int> vec(10);
fill(vec.begin(), vec.end(), 6);
2.2.2 算法不检查写操作
一些算法只接受一个目的迭代器,这些算法将新值赋给一个序列中的元素,该序列就从目的迭代器开始。由于仅指定了开始位置,所以向目的位置迭代器写入数据的算法假定目的位置足够大,能容纳要写入的元素。
算法本身不检查空间是否够大,这一点要编程者自己确保。
2.2.3 fill_n函数
fill_n算法接受三个参数。第一个参数是目标迭代器,第二个参数是计数值,第三个参数是将要赋给元素的数值。
fill_n算法将给定值赋给以目标迭代器所指元素开始的指定个元素。
vector<int> vec(10);
fill_n(vec, 5, 6);
2.2.4 back_inserter
一种保证算法有足够元素空间来容纳输出数据的方式是使用插入迭代器(insert iterator)。
插入迭代器是一种向容器中添加元素的迭代器。通过普通迭代器向容器元素赋值时,值被赋予迭代器指向的元素。通过插入迭代器赋值时,一个与赋值号右侧值相等的元素被添加到容器中。
back_inserter是定义在头文件iterator中的函数。它接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器。通过此迭代器赋值时,赋值运算符会调用push_back将一个具有给定值的元素添加到容器中:
vector<int> vec;
auto it = back_inserter(vec);
*it = 42; // 向vec中添加一个新元素,其值为42
通常使用back_inserter创建迭代器,作为算法的目的位置来使用。
vector<int> vec;
fill_n(back_inserter(vec), 10, 0);
2.2.5 copy算法
copy算法将输入范围中的元素拷贝到目的序列中。传递给copy的目的序列至少要包含与输入序列一样多的元素。
copy算法接受三个迭代器,前两个表示一个输入范围,第三个表示目的序列的起始位置。
copy返回的是其目的位置迭代器递增后的值(指向拷贝到目标序列的尾元素之后的位置)。
用copy实现内置数组拷贝:
int a1[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int a2[sizeof(a1) / sizeof(*a1)];
auto ret = copy(begin(a1), end(a1), a2);
很多算法都提供了copy版本。copy版本计算新元素的值,创建一个新序列保存这些结果,并不改变原来的序列。
2.2.6 replace算法和replace_copy算法
replace算法读入一个序列,将其中所有等于给定值的元素都改为另一个值。
replace算法接受4个参数,前两个是迭代器,表示输入序列。第三个参数是要搜索的值,第四个参数是新值。
replace(ilst.begin(), ilst.end(), 0, 42); // 将所有值为0的元素改为42
如果希望原序列不变,可以调用replace_copy。该算法新增的第三个参数指出调整后序列的保存位置。
replace_copy(ilst.begin(), ilst.end(), back_inserter(ivec), 0, 42);
2.3 重排容器元素的算法
某些算法会重排容器中元素的顺序。
2.3.1 sort算法
sort算法利用元素类型的<运算符重排输入序列中的元素,使之有序。
vector<string> vec = {"the", "quick", "red", "fox", "jumps", "over", "the", "slow", "red", "turtle"};
sort(vec.begin(), vec.end());
print_string_vector(vec); // 输出fox jumps over quick red red slow the the turtle
2.3.2 unique算法
unique算法重排输入序列,覆盖相邻的重复元素,使得不重复元素出现在序列的开始部分。unique返回的迭代器指向最后一个不重复元素之后的位置。此位置之后的元素仍然存在,但不知道它们的值是什么。
vector<string> vec = {"the", "quick", "red", "fox", "jumps", "over", "the", "slow", "red", "turtle"};
sort(vec.begin(), vec.end());
print_string_vector(vec); // 输出fox jumps over quick red red slow the the turtle
auto end_unique = unique(vec.begin(), vec.end()); // vec.begin()到vec.end_unique()就是不重复的元素
2.3.3 erase容器操作
unique算法只是将不重复的元素推到容器的头部,并没有真的删除元素。为了删除无用元素,要使用容器操作。
vector<string> vec = {"the", "quick", "red", "fox", "jumps", "over", "the", "slow", "red", "turtle"};
sort(vec.begin(), vec.end());
print_string_vector(vec); // 输出fox jumps over quick red red slow the the turtle
auto end_unique = unique(vec.begin(), vec.end()); // vec.begin()到vec.end_unique()就是不重复的元素
vec.erase(end_unique, vec.end()); // 删除无用元素
3 定制操作
3.1 向算法传递函数
很多算法都会比较输入序列中的元素。默认情况下,这类算法使用元素类型的<运算符或<=运算符完成比较。但我们也可以使用自定义的操作来代替默认运算符。
3.1.1 谓词
谓词是一个可调用的表达式,其返回结果是一个能用作条件的值。接受谓词参数的算法对输入序列中的元素调用谓词。因此,元素类型必须能转换为谓词的参数类型。
标准库算法所使用的谓词分为两类:
- 一元谓词:只接收一个参数
- 二元谓词:接收两个参数
3.1.2 sort的谓词版本
sort有一个重载版本,它的第三个参数是二元谓词。
bool isShorter(const string &s1, const string &s2)
{
return s1.size() < s2.size();
}
sort(vec.begin(), vec.end(), isShorter);
3.1.3 stable_sort算法
stable_sort算法在排序时维持相等元素的原有顺序。
我们可以先将单词按字典序排序,然后再调用stable_sort,这样就可以将单词按长度排序,且相同长度的单词按字典序排序。
sort(vec.begin(), vec.end());
auto end_unique = unique(vec.begin(), vec.end());
vec.erase(end_unique, vec.end());
stable_sort(vec.begin(), vec.end(), isShorter);
3.1.4 find_if算法
find_if用于查找第一个具有特定大小的元素。
find_if算法前两个参数是一对迭代器,表示一个范围。第三个参数是一个谓词。find_if对输入序列中的每个元素调用谓词,返回第一个使谓词返回非0值的元素。如果没有这样的元素,则返回尾迭代器。
3.2 lambda表达式
根据算法接受一元谓词还是二元谓词,我们传递给算法的谓词必须严格接受一个或两个参数。但有时我们需要更多参数,超出了算法对谓词的限制。为了解决此问题,需要使用另外一些语言特性。
3.2.1 可调用对象
对于一个对象或一个表达式,如果可以对其使用调用运算符,则称它是可调用的。换言之,如果e是一个可调用的表达式,则我们可以编写代码e(args),其中args是一个逗号分隔的一个或多个参数的列表。我们可以向一个算法传递任何类别的可调用对象。
可调用对象共有四种:
- 函数
- 函数指针
- 重载函数调用运算符的类
- lambda表达式
3.2.2 lambda的格式
一个lambda表达式表示一个可调用的代码单元,可将其理解为未命名的内联函数。
与函数类似,lambda表达式具有返回类型、参数列表、函数体。与函数不同之处在于,lambda可以定义在函数内部。
[capture list](parameter list) -> return type { function body }
lambda必须使用尾置返回来指定返回类型。
lambda可以忽略参数列表和返回类型,但必须包含捕获列表和函数体。忽略参数列表等价于指定空的参数列表。忽略返回类型,lambda表达式会根据函数体中的代码推断出返回类型。如果函数体只有一个return语句,返回类型会从返回的表达式的类型推断而来。否则,返回类型为void。
auto f = [] {return 42;}
cout << f() << endl;
3.2.3 向lambda传递参数
调用lambda时,用给定的实参来初始化lambda的形参。但和普通函数不同,lambda不能有默认参数。因此,lambda调用的实参数目必须和形参数目相等。
stable_sort的lambda版本如下:
stable_sort(words.begin(), words.end(),
[] (const string &a, const string &b)
{ return a.size() < b.size() });
3.2.4 使用捕获列表
从作用域的角度来看,如果将lambda定义在函数中,应当能使用定义在函数中的局部变量。但lambda只能使用在其捕获列表中明确指出的局部变量。
捕获列表仅用于局部非static变量。lambda可以直接使用局部static变量和它所在函数之外声明的名字。
3.2.5 for_each算法
for_each算法接受一个可调用对象,并对输入序列中每个元素调用此对象。
void biggies(vector<string> &words, vector<string>::size_type sz)
{
elimDups(words); // 将words按字典序排序,删除重复单词
stable_sort(words.begin(), words.end(),
[](const string &a, const string &b)
{ return a.size() < b.size() }); // 按长度排序,长度相同的单词维持字典序
auto wc = find_if(words.begin(), words.end(), [sz](const string &a)
{return a.size() >= sz}); // 获取一个迭代器,指向第一个满足size() >= sz的元素
auto count = words.end() - wc; // 计算size >=sz的元素数目
cout << count << " " << make_plural(count, "word", "s")
<< " of length " << sz << " or longer" << endl;
for_each(wc, words.end(), [](const string &s) {cout << s << " "});
cout << endl;
}
3.3 lambda捕获和返回
定义一个lambda时,编译器生成一个与lambda对应的新的未命名的类类型。
- 向一个函数传递一个lambda时,同时定义了一个新类型和该类型的一个对象,传递的参数就是此编译器生成的类类型的未命名对象。
- 使用auto定义一个用lambda初始化的变量时,定义了一个从lambda生成的类型的对象。
默认情况下,从lambda生成的类都包含一个对应该lambda所捕获的变量的数据成员。类似普通类的数据成员,lambda的数据成员也在lambda对象创建时被初始化。
变量捕获的方式可以是值或引用。
3.3.1 值捕获
采用值捕获的前提是变量可以拷贝。与参数不同,被捕获的变量值是在lambda创建时拷贝,而不是调用时拷贝。因此,随后对其修改不会影响lambda内对应的值。
void func()
{
size_t v1 = 42;
auto f = [v1]{return v1;}
v1 = 0;
auto j = f(); // j为42
}
3.3.2 引用捕获
定义lambda时,可以用引用方式捕获变量。在lambda函数体内使用该变量时,实际使用的是引用绑定的对象。
void func()
{
size_t v1 = 42;
auto f = [&v1] { return v1; }
v1 = 0;
auto j = f(); // j为0
}
引用捕获和返回引用有着相同的问题和限制。如果采用引用方式捕获对象,就必须确保被引用对象在lambda执行时是存在的。lambda捕获的都是局部变量,这些变量在函数结束后就不存在了。如果lambda可能在函数结束后执行,捕获的引用指向的局部变量就已经消失了。
引用捕获有时是必要的。例如如果希望函数捕获ostream,由于不能拷贝ostream对象,所以捕获os的唯一方法就是捕获其引用或指针。
void biggies(vector<string> &words,
vector<string>::size_type sz,
ostream &os = cout, char c = ' ')
{
for_each(words.begin(), words.end(),
[&os, c](const string &s) { os << s << c; });
}
当我们向函数传递lambda时,lambda会立即执行。在此情况下,以引用方式捕获os没有问题,因为for_each执行时,biggies中的os变量是存在的。
我们可以从一个函数返回lambda。函数可以直接返回一个可调用对象,或者返回一个类对象。如果函数返回一个lambda,则此lambda不能包含引用捕获。
3.3.3 隐式捕获
除了显式列举我们希望捕获的变量,还可以让编译器根据lambda函数体来推断要使用哪些变量。为了指示编译器推断捕获列表,应在捕获列表中写一个&告诉编译器使用引用捕获方式,或写一个=告诉编译器采用值捕获方式。
// sz为隐式捕获,值捕获方式
wc = find_if(words.begin(), words.end(),
[=](const string &s)
{return s.size() > sz;});
如果希望对部分变量采用值捕获,对其他变量采用引用捕获,可以混合使用隐式捕获和显式捕获。此时捕获列表中的第一个元素必须是&或=,指定默认捕获方式为引用捕获或值捕获。
混合使用隐式捕获和显式捕获时,显式捕获的变量必须使用和隐式捕获不同的方式。如果隐式捕获是引用捕获,则显式捕获只能使用值捕获,反之亦然。
void biggies(vector<string> &words,
vector<string>::size_type sz,
ostream &os = cout, char c = ' ')
{
// os隐式捕获,引用捕获方式。c显式捕获,值捕获方式
for_each(words.begin(), words.end(),
[&, c](const string &s) { os << s << c; });
// os显式捕获,引用捕获方式。c隐式捕获,值捕获方式
for_each(words.begin(), words.end(),
[=, &os](const string &s) { os << s << c; });
}
表1 lambda捕获列表 | |
---|---|
[] | 空捕获列表。lambda不能使用其所在函数中定义的非static变量,必须捕获之后才能使用。 |
[names] | names是用逗号分隔的名字列表,这些名字都是定义在lambda所在函数中的非static局部变量。默认情况下,捕获列表中的变量都被拷贝。名字前如果添加&,则是引用捕获方式。 |
[&] | 隐式捕获列表,采用引用捕获方式。lambda函数体中使用的来自所在函数的实体都采用引用方式使用。 |
[=] | 隐式捕获列表,均采用值捕获方式。lambda函数体将拷贝所使用的的来自所在函数的实体的值 |
[&, identifier_list] | identifier_list是一个以逗号分隔的列表,包含0个或多个来自所在函数的变量。这些变量采用值捕获方式,而任何隐式捕获的变量都采用引用方式捕获。identifier_list中的名字前面不能使用& |
[=, identifier_list] | identifier_list中的变量都采用引用方式捕获,而任何隐式捕获的变量都采用值方式捕获。identifier_list中的名字不能包括this,且这些名字之前必须使用& |
3.3.4 尽量保持lambda的变量捕获简单化
一个lambda捕获从lambda创建(即lambda定义代码执行时)到lambda执行(可能多次执行)这段时间内保存的相关信息。确保lambda每次执行时这些都有预期的意义是编程者的责任。
如果我们捕获一个指针或迭代器,或采用引用捕获,就必须确保在lambda执行时,绑定到迭代器、指针或引用的对象仍然存在。而且需要保证对象具有预期的值,因为从lambda定义到执行的这段时间内,可能有代码改变绑定的对象的值。
通常来说,应当尽量减少捕获的数据量,且应尽量避免捕获指针或引用。
3.3.5 可变lambda
默认情况下,对于值捕获的变量,lambda不能改变其值。如果希望改变值捕获的变量,必须在参数列表后加上muteble关键字。可变lambda的参数列表可以为空,但不能省略()。
int main()
{
int i = 10;
auto f = [i]() mutable {return ++i;};
int j = f();
cout << "j: " << j << endl;
return 0;
}
对于引用捕获的变量,是否能修改其值依赖于该引用绑定的对象是否是const类型。
3.3.6 指定lambda返回类型
如果lambda体只有return语句,可以推算出返回类型,无需指定。
如果lambda体除return之外还有其他语句,则编译器假定该lambda返回void。被推断返回void的lambda不能返回值。此时如果需要返回值,则应使用尾置返回类型明确指定。
3.3.7 transform算法
transform算法接受三个迭代器和一个可调用对象。前两个迭代器表示输入序列,第三个迭代器表示目的位置。算法对输入序列中的每个元素调用可调用对象,并将结果写到目的位置。
目的位置和输入序列开始位置的迭代器可以是同一个,此时transform将输入序列中的每个元素都替换成可调用对象处理后的结果。
transform(vi.begin(), vi.end(), vi.begin(),
[](int i) { return i < 0 ? -i : i; });
3.4 参数绑定
lambda适用于仅在一两个地方使用的简单操作。如果需要在多处使用,或者语句很多,则还是使用函数更好。
如果lambda的捕获列表为空,通常可以用函数代替。对于捕获局部变量的lambda,用函数来替换就不大容易了。这主要是受到一元谓词、二元谓词的参数个数限制。
为了便于理解,我们以find_if为例分析。lambda版本代码如下:
wc = find_if(words.begin(), words.end(),
[=](const string &s)
{return s.size() > sz;});
实现lambda相同功能的函数如下:
bool check_size(const string &s, string::size_type sz)
{
return s.size() > sz;
}
尽管用函数实现相同功能很简单,但find_if接受一元谓词,所以check_size不能作为find_if的参数。
3.4.1 标准库bind函数
为了解决check_size函数的传参问题,要引入标准库函数bind,它定义在头文件functional中。
可以将bind函数看作通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。
调用bind的一般形式如下:
auto newCallable = bind(callable, arg_list);
newCallable是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。简而言之,我们调用newCallable时,newCallable会调用callable,并将arg_list中的参数传递给它。
arg_list中的参数可能包含形如_n的名字,n是一个整数。这些参数是占位符,表示newCallable的参数。占位符占据了传递给newCallable的参数的位置。数值n表示生成的可调用对象中参数的位置,_1是newCallable的第一个参数,_2是第二个参数,以此类推。
我们接着上一小节的例子来分析。使用bind,可以生成一个调用check_size的对象:
auto new_check = bind(check_size, _1, 6); // 此处将传给sz的数值写成了字面值常量6
// 以下两条语句等效
new_check(s);
check_size(s, 6);
占位符_1放在arg_list的首位,对应check_size的第一个参数,类型为const string &。因此,在调用new_check时,要传入const string &类型的参数。
综上,可以将find_if的调用改为以下版本:
auto wc = find_if(words.begin(), words.end()
, bind(check_size, _1, sz));
3.4.2 使用placeholders名字
名字_n都定义在名为placeholders的命名空间中,placeholders本身定义在std命名空间(functional头文件)。为了使用这些名字,两个命名空间都要写上。所以在以上bind代码调用之前,要添加using声明:
using std::placeholders::_1;
如果对每个占位符都单独声明,不仅繁琐,也容易出错。可以使用以下形式声明,所有来自namespace_name的名字都可以在程序中直接使用:
using namespace namespace_name;
3.4.3 bind的参数
在bind绑定时,可以调整arg_list的顺序。譬如以下代码:
auto g = bind(f, a, b, _2, c, _1);
// 以下两行代码等价
g(X, Y);
f(a, b, Y, c, X);
3.4.4 绑定引用参数
默认情况下,bind的那些不是占位符的参数被拷贝到bind返回的可调用对象中。但是,有些参数希望以引用方式传递,或者该类型的参数无法拷贝(如os)。
我们用以下代码为例进行分析:
// 用引用方式捕获ostream的lambda
for_each(words.begin(), words.end(),
[&os, c](const string &s) { os << s << c; });
以下函数实现相同功能:
ostream &print(ostream &os, const string &s, char c)
{
return os << s << c;
}
但是,不能直接用bind来代替对os的捕获:
for_each(words.begin(), words.end(), bind(print, os, _1, ' ')); // 错误,不能拷贝os
如果想向bind传递一个对象而又不拷贝它,就必须使用标准库ref函数。ref返回一个对象,包含给定的引用,这个对象是可以拷贝的。标准库中还有cref函数,生成一个保存const引用的类。函数ref和cref也定义在头文件functional中。
for_each(words.begin(), words.end(),
bind(print, ref(os), _1, ' '));
4 再探迭代器
除了为每个容器定义的迭代器,标准库在头文件iterator中还定义了以下几种迭代器:
- 插入迭代器(insert iterator):这些迭代器被绑定到一个容器上,可用于向容器插入元素。
- 流迭代器(stream iterator):这些迭代器被绑定到输入或输出流上,可用于遍历所关联的IO流。
- 反向迭代器(reverse iterator):这些迭代器向后而非向前移动。除了forward_list之外的标准库容器都有反向迭代器。
- 移动迭代器(move iterator):这些专用的迭代器不是拷贝其中的元素,而是移动它们。
4.1 插入迭代器
插入器是一种迭代器适配器,它接受一个容器,生成一个迭代器,可以向给定容器添加元素。通过插入迭代器进行赋值时,该迭代器调用容器操作来向给定容器的指定位置插入一个元素。
表2 插入迭代器操作 | |
---|---|
it = t | 在it指定的位置插入值t。根据插入迭代器的不同种类,实际会调用push_back(t)、push_front(t)、insert(t, p)等。p是传递给inserter的迭代器位置 |
*it, ++it, it++ | 这些操作不会对it做任何事情,每个操作都返回it |
插入器有三种类型,差异在于元素插入的位置:
- back_inserter:创建一个使用push_back的迭代器;
- front_inserter:创建一个使用push_front的迭代器;
- inserter:创建一个使用insert的迭代器。此函数接受第二个参数,这个参数必须是指向给定容器的迭代器,元素将被插入到给定迭代器所表示的元素之前。
调用inserter(c, iter)时,得到一个迭代器。使用它时会把元素插入到iter原来所指向的元素之前的位置。
auto it = inserter(c, c.begin());
// 以下两段代码效果等价
*it = val;
it = c.insert(it, val); // it指向新加入的元素
++it; // 递增it使它指向原来的元素
调用front_inserter时,元素总是插入到容器的首元素之前。即使传递给inserter的位置原来指向第一个元素,只要在此元素之前插入一个元素,该元素就不再是首元素了。
list<int> lst = {1, 2, 3, 4};
list<int> lst2, lst3;
copy(lst.cbegin(), lst.cend(), front_inserter(lst2)); // 拷贝完成后,lst2包含4 3 2 1
copy(lst.cbegin(), lst.cend(), inserter(lst3, lst3.begin()));
4.2 iostream迭代器
istream_iterator读取输入流,ostream_iterator向输出流写数据。这些迭代器将它们对应的流当做一个特定类型的元素序列来处理。通过使用流迭代器,可以用泛型算法来读写流对象。
4.2.1 istream_iterator
创建流迭代器时,必须指定迭代器将要读写的对象类型。istream_iterator使用>>来读取流,所以istream_iterator要读取的类型必须定义了输入运算符。
创建istream_iterator时,可以将其绑定到一个流。还可以默认初始化迭代器,该迭代器可以作为尾后值使用。对于绑定到流的迭代器,一旦其关联的流遇到文件尾或遇到IO错误,迭代器的值就与尾后迭代器相等。
istream_iterator<int> int_it(cin); // 从cin读取int数据
istream_iterator<int> int_eof; // 默认初始化,得到尾后迭代器
ifstream in("afile");
istream_iterator<string> str_it(in); // 从afile读取字符串
以下代码用istream_iterator从标准输入读取数据,存入vector。
istream_iterator<int> in_iter(cin);
istream_iterator<int> eof;
while (in_iter != eof) {
vec.push_back(*in_iter++);
}
还可以用istream_iterator来构造vector:
istream_iterator<int> in_iter(cin), eof;
vector<int> vec(in_iter, eof);
表3 istream_iterator操作 | |
---|---|
istream_iterator in(is); | in从输入流is读取类型为T的值 |
istream_iterator end; | 读取类型为T的值的istream_iterator迭代器,表示尾后位置 |
in1 == in2 | in1和in2必须读取相同类型。如果它们都是尾后迭代器,或绑定到相同的输入,则两者相等 |
in1 != in2 | |
*in | 返回从流中读取的值 |
in->mem | 与(*it).mem相同 |
++in, in++ | 使用元素类型定义的>>运算符从输入流中读取下一个值。 |
4.2.2 使用算法操作流迭代器
前文反复强调,算法使用迭代器来处理数据。流迭代器支持部分迭代器操作,所以至少可以用某些算法来操作流迭代器。后续我们再讲解如何分辨哪些算法可以用于流迭代器,目前先看一个使用istream_iterator来调用accumulate的示例:
istream_iterator<int> in(cin), eof;
cout << accumulate(in, eof, 0) << endl;
4.2.3 istream_iterator允许使用懒惰求值
将一个istream_iterator绑定到一个流时,标准库并不保证迭代器立即从流读取数据。具体实现可以推迟从流中读取数据,直到使用迭代器时才真正读取。标准库中的实现保证的是,在第一次解引用迭代器之前,从流中读取数据的操作已经完成了。
对于大多数程序来说,立即读取还是推迟读取并没有什么差别。但如果我们创建了一个istream_iterator,没有使用就销毁了,或者我们正在从两个不同的对象同步读取同一个流,何时读取可能就很重要了。
4.2.4 ostream_iterator操作
可以对任何具有输出运算符(<<)