文章目录
- *泛型算法*
- *10.1概述*
- *10.2初识泛型算法*
- *10.2.1只读算法*
- *算法和元素类型*
- *操作两个序列的算法*
- *10.2.2写容器元素的算法*
- *算法不检查写操作*
- *介绍`back_inserter`*
- *拷贝算法*
- *10.2.3重排容器元素的算法*
- *消除重复单词*
- *使用`unique`*
- *10.3定制操作*
- *10.3.1向算法传递函数*
- *谓词*
- *排序算法*
- *10.3.2`lambda`表达式*
- *介绍`lambda`*
- *向`lambda`传递参数*
- *使用捕获列表*
- *调用`find_if`*
- *`for_each`算法*
- *完整的`biggies`*
- *10.3.3`lambda`捕获和返回*
- *值捕获*
- *引用捕获*
- *隐式捕获*
- *可变`lambda`*
- *指定`lambda`返回类型*
- *10.3.4参数绑定*
- *标准库`bind`函数*
- *`bind`的参数*
- *绑定引用参数*
- *10.4再探迭代器*
- *10.4.1插入迭代器*
- *10.4.2`iostream`迭代器*
- *`istream_iterator`操作*
- *使用算法操作流迭代器*
- *`istream_iterator`允许使用懒惰求值*
- *`ostream_iterator`操作*
- *使用流迭代器处理类类型*
- *10.4.3反向迭代器*
- *反向迭代器和其他迭代器间的关系*
- *10.5泛型算法结构*
- *10.5.1五类迭代器*
- *迭代器类别*
- *10.5.2算法形参模式*
- *接受单个目标迭代器的算法*
- *接受第二个输入序列的算法*
- *10.5.3算法命名规范*
- *一些算法使用重载形式传递一个谓词*
- *`_if`版本的算法*
- *区分拷贝元素的版本和不拷贝的版本*
- *10.6特定容器算法*
- *`splice`成员*
- *链表特有的操作会改变容器*
泛型算法
10.1概述
大多数算法都定义在头文件
algorithm
中。标准库还在头文件numeric
中定义了一组数值泛型算法。一般情况下,这些算法并不直接操作容器,而是遍历由两个迭代器指定的一个元素范围来进行操作。
find
将范围中每个元素与给定值进行比较。传递给find
的前两个参数是表示元素范围的迭代器,第三个参数是一个值。它返回指向第一个等于给定值的元素的迭代器,如果范围中无匹配元素,则find
返回第二个参数来表示搜索失败。因此,可以通过比较返回值和第二个参数来判断搜索是否成功:
int val = 42; // 将查找的值
auto result = find(vec.cbegin(), vec.cend(), val); // 包头不包尾
// 报告结果
cout << "The value " << val
<< (result == vec.cend() ? " is not present" : " is present") << endl;
泛型算法本身不会执行容器的操作,它们只会运行于迭代器之上,执行迭代器的操作。这一特性带来了一个必要的编程假定:算法永远不会改变底层容器的大小。算法可能改变容器中保存的元素的值,也可能在容器内移动元素,但算法本身永远不会直接添加或删除元素。
10.2初识泛型算法
除了少数例外,标准库算法都对一个范围内的元素进行操作,将此元素范围称为输入范围。
10.2.1只读算法
算法和元素类型
accumulate
函数接受三个参数:前两个指出了需要求和的元素的范围,第三个参数是和的初值:int sum = accumulate(vec.cbegin(), vec.cend(), 0);
。
accumulate
的第三个参数的类型决定了函数中使用哪个加法运算符以及返回值的类型。额外注意,第三个参数不能是字符串字面值,原因在于,如果传递了一个字符串字面值,用于保存和的对象的类型将是const char*
,而其并没有加法运算符。
最佳实践:对于只读取而不改变元素的算法,通常最好使用cbein()
和cend()
。但是,如果计划使用算法返回的迭代器来改变元素的值,就需要使用begin
和end
的结果作为参数。
操作两个序列的算法
equal
用于确定两个序列是否保存相同的值。它将第一个序列中的每个元素与第二个序列中的对应元素进行比较。如果所有对应元素都相等,则返回true
,否则返回false
。
此算法接受三个迭代器:前两个表示第一个序列中的元素范围,第三个表示第二个序列的首元素。需要注意,equal
基于一个非常重要的假设:它假定第二个序列至少与第一个序列一样长。此算法要处理第一个序列中的每个元素,它假定每个元素在第二个序列中都有一个与之对应的元素。
那些只接受一个单一迭代器来表示第二个序列的算法,都假定第二个序列至少与第一个序列一样长。
10.2.2写容器元素的算法
一些算法将新值赋予序列中的元素。使用这类算法时,必须注意确保序列原大小至少不小于要求算法写入的元素数目。记住,算法不会执行容器操作,因此它们自己不可能改变容器的大小。
一些算法会自己向输入范围写入元素,这些算法本质上并不危险,它们最多写入与给定序列一样多的元素。
fill
接受一对迭代器表示一个范围,还接受一个值作为第三个参数。fill
将给定的这个值赋予输入序列中的每个元素:fill(vec.begin(), vec.end(), 0); // 将每个元素重置为0
。
关键概念:迭代器参数。用一个单一迭代器表示第二个序列的算法都假定第二个序列至少与第一个一样长。如果第二个序列是第一个序列的一个子集,则程序会产生一个严重错误——会试图访问第二个序列中末尾之后(不存在)的元素。
算法不检查写操作
一些算法接受一个迭代器来指出一个单独的目的位置。这些算法将新值赋予一个序列中的元素,该序列从目的位置迭代器指向的元素开始。
fill_n
接受一个单迭代器、一个计数值和一个值,它将给定值赋予迭代器指向的元素开始的指定个元素。fill_n
假定写入指定个元素是安全的。即:fill_n(dest, n, val)
假定dest指向一个元素,而从dest开始的序列至少包含n个元素:
vector<int> vec;
// 注意第二个参数是vec.size(),而不是指定的计数值
fill_n(vec.begin(), vec.size(), 0); // 将所有元素重置为0
一个初学者非常容易犯的错误是在一个空容器上调用
fill_n
(或类似的写元素的算法):fill_n(vec, 10, 0); // vec中并没有元素
。
向目的位置迭代器写入数据的算法假定目的位置足够大,能容纳要写入的元素。
介绍back_inserter
一种保证算法有足够元素空间来容纳输出数据的方法是使用一种向容器中添加元素的迭代器,即插入迭代器。
back_inserter
接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器。当通过此迭代器赋值时,赋值运算符会调用push_back
将一个具有给定值的元素添加到容器中。常常使用back_inserter
来创建一个迭代器,作为算法的目的位置来使用:
vector<int> vec;
fill_n(back_inserter(vec), 10, 0); // 将10个元素逐个赋予到back_inserter返回的迭代器,每次赋值都调用push_back
拷贝算法
拷贝(
copy
)算法接受三个迭代器,前两个表示一个输入范围,第三个表示目的序列的起始位置。此算法将输入范围中的元素拷贝到目的序列中。传递给copy
的目的序列至少要包含与输入序列一样多的元素,这一点很重要。该函数返回的是其目的位置迭代器(递增后)的值。
// 使用copy实现内置数组的拷贝
int a1[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int a2[sizeof(a1) / sizeof(*a1)]; // a2与a1大小一样
// ret指向拷贝到a2的尾元素之后的位置
auto ret = copy(begin(a1), end(a1), a2); // 把a1的内容拷贝给a2
多个算法都提供所谓的"拷贝"版本。这些算法计算新元素的值,但不会将它们放置在输入序列的末尾,而是创建一个新序列保存这些结果。
replace
读入一个序列,并将其中所有等于给定值的元素都改为另一个值:
replace(ilist.begin(), ilist.end(), 0, 42); // 将值为0的元素改为42
如果希望保留原序列不变,可以调用
replace_copy
。此算法接受额外第三个迭代器参数,指出调整后序列的保存位置:
replace_copy(ilist.cbegin(), ilist.cend(), back_inserter(ivec), 0, 42);
// ilist并未改变,ivec包含ilist的一份拷贝,不过原来在ilist中值为0的元素在ivec中都变为42
10.2.3重排容器元素的算法
某些算法会重排容器中元素的顺序,一个明显的例子是
sort
,它是利用元素类型的<
运算符来实现排序的。
消除重复单词
为了消除重复单词,首先将
vector
排序,使得重复的单词都相邻出现。一旦排序完毕,就可以使用另一个称为unique
的标准库算法来重排vector
,使得不重复的元素出现在vector
的开始部分。由于算法不能执行容器的操作,将使用vector
的erase
成员来完成真正的删除操作。
// 按字典排序并删除重复元素
void elimDups(vector<string> &words) {
sort(words.begin(), words.end());
// 返回指向不重复区域之后一个位置的迭代器
auto end_unique = unique(words.begin(), words.end());
words.erase(end_unique, words.end());
}
使用unique
unique
算法重排输入序列,将相邻的重复项"消除"(并不是真正的删除,只是覆盖相邻的重复元素,使得不重复元素出现在序列的开始部分),并返回一个指向最后一个不重复元素之后的位置的迭代器。此位置之后的元素仍然存在,但不知道它们的值是什么。
如果没有重复元素,则unique
会返回指向尾后元素的迭代器。
10.3定制操作
很多算法都会比较输入序列中的元素。默认情况下,这类算法使用元素类型的
<
或==
运算符完成比较。标准库还为这些算法定义了额外的版本,允许提供自己定义的操作来代替默认运算符。
10.3.1向算法传递函数
谓词
谓词是一个可调用的表达式,其返回结果是一个能用作条件的值。标准库算法所使用的谓词分为两类:一元谓词(意味着它们只接受单一参数)和二元谓词(意味着它们有两个参数)。接受谓词参数的算法对输入序列中的元素调用谓词。因此,元素类型必须能转换为谓词的参数类型:
bool isShorter(const string &s1, const string &s2) {
return s1.size() < s2.size();
}
sort(words.begin(), words.end(), isShorter);
排序算法
stable_sort
算法维持相等元素的原有顺序:
// 将words按字典序重排,并消除重复单词
elimDups(words);
// 按长度重新排序,长度相同的单词维持字典序
stable_sort(words.begin(), words.end(), isShorter);
10.3.2lambda
表达式
根据算法接受一元谓词还是二元谓词,传递给算法的谓词必须严格接受一个或两个参数。但是,有时希望进行的操作需要更多参数,超出了算法对谓词的限制。
介绍lambda
可以向一个算法传递任何类别的可调用对象。一个
lambda
表达式表示一个可调用的代码单元,
可以将其理解为一个未命名的内联函数:
[capture list] (parameter list) -> return type { function body }
其中,
capture list
(捕获列表)是一个lambda
所在函数中定义的局部变量的列表(通常为空)。
需要注意,lambda
必须使用尾置返回来指定返回类型,可以忽略参数列表和返回类型,但必须永远包含捕获列表和函数体。并且,其调用方式与普通函数的调用方式相同,都是使用调用运算符()
:
auto f = [] { return 42; };
cout << f() << endl;
如果忽略返回类型,
lambda
根据函数体中的代码推断出返回类型。如果函数体只是一个return
语句,则返回类型从返回的表达式的类型推断而来。否则,返回类型为void
。
向lambda
传递参数
与普通函数不同,
lambda
不能有默认实参。因此,一个lambda
调用的实参数目永远与形参数目相等。
stable_sort(words.begin(), words.end(), [](const string &a, const string &b) {
return a.size() < b.size();
});
空捕获列表表明此lambda不使用它所在函数中的任何局部变量。
使用捕获列表
虽然一个lambda可以出现在一个函数中,使用其局部变量,但它只能使用那些明确指明的变量。一个lambda通过将局部变量包含在其捕获列表中来指出将会使用这些变量。捕获列表指引lambda在其内部包含访问局部变量所需的信息。
[sz](const string &a) { return a.size() >= sz; };
调用find_if
对
find_if
的调用返回一个迭代器,指向第一个匹配的元素。如果这样的元素不存在,则返回尾后迭代器的一个拷贝。
// 这儿的words容器是按照单词长度进行升序排列了的
auto wc = find_if(words.begin(), words.end(), [sz] (const string &a) {
return a.size >= sz;
});
auto count = words.end() - wc; // 计算满足size >= sz的元素的数目
// 调用make_plural来输出"word"或"words"
cout << count << " " << make_plural(count, "words", "s")
<< " of length " << sz << " or longer" << endl;
for_each
算法
for_each
算法接受一个可调用对象,并对输入序列中每个元素调用此对象。
for_each(wc, words.end(), [] (const string &s) { cout << s << " "; });
cout << endl;
捕获列表只用于局部非
static
变量,lambda
可以直接使用局部static
变量和它所在函数之外声明的名字。例如,cout
定义在头文件iostream
中。因此,只要在该函数出现的作用域中包含了头文件iostream
,那么lambda就可以使用cout
。
完整的biggies
void biggies(vector<string> &words, vector<string>::size_type sz) {
// 将words按字典序排序,删除重复单词
elimDups(words);
// 按长度排序,长度相同的单词维持字典序
stable_sort(words.begin(), words.end(), [] (const string &a, const string &b) {
return a.size() < b.size();
});
// 获取一个迭代器,指向第一个满足size() >= sz的元素
auto wc = find_if(words.begin(), words.end(), [sz] (const string &a) {
return a.size() >= sz;
});
// 计算满足size() >= sz的元素的数目
auto count = words.end() - wc;
cout << count << " " << make_plural(count, "words", "s") << " of length " << sz << " or longer" << endl;
// 打印长度大于等于给定值的单词,每个单词后面接一个空格
for_each(wc, words.end(), [] (const string &s) {
cout << s << " ";
});
cout << endl;
}
10.3.3lambda
捕获和返回
当定义一个
lambda
时,编译器生成一个与lambda
对应的新的未命名的类类型。当向一个函数传递一个lambda
时,同时定义了一个新类型和该类型的一个对象:传递的参数就是此编译器生成的类类型的未命名对象。默认情况下,从lambda
生成的类都包含一个对应lambda
所捕获的变量的数据成员。类似任何普通类的数据成员,lambda
的数据成员也在lambda
对象创建时被初始化。
值捕获
与传值参数类似,采用值捕获的前提是变量可以拷贝。与参数不同,被捕获的变量的值是在
lambda
创建时拷贝,而不是调用时拷贝:
void fcn1() {
size_t v1 = 42;
auto f = [v1] { return v1; };
v1 = 0;
auto j = f(); // j为42;f保存了创建它时v1的拷贝
}
引用捕获
void fcn2() {
size_t v1 = 42;
auto f2 = [&v1] { return v1; };
v1 = 0;
auto j = f2(); // j为0;f2保存了v1的引用,而非拷贝
}
引用捕获与返回引用有着相同的问题和限制。如果采用引用方式捕获一个变量,就必须确保被引用的对象在
lambda
执行的时候是存在的。lambda
捕获的都是局部变量,这些变量在函数结束后就不复存在了。如果lambda
可能在函数结束后执行,捕获的引用指向的局部变量已经消失。但是,有时候引用捕获是必要的:
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
,则与函数不能返回一个局部变量的引用类似,此lambda
也不能包含引用捕获。
当以引用捕获一个变量时,必须保证在lambda
执行时变量是存在的。
一般来说,应该尽量减少捕获的数据量,来避免潜在的捕获导致的问题。而且,如果可能的话,应该避免捕获指针或引用。
隐式捕获
可以让编译器根据
lambda
体中的代码来推断要使用哪些变量。为了指示编译器推断捕获列表,应在捕获列表中写一个&
或=
。&
告诉编译器采用捕获引用方式,=
则表示采用值捕获方式:
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; });
}
当混合使用隐式捕获和显式捕获时,捕获列表中的第一个元素必须是一个
&
或=
。并且,显式捕获的变量必须使用与隐式捕获不同的方式,即,如果隐式捕获使用的是引用方式,则显式捕获命名变量必须采用值方式。
可变lambda
默认情况下,对于一个值被拷贝的变量,
lambda
不会改变其值。如果希望能改变一个被捕获的变量的值,就必须在参数列表首加上关键字mutable
。因此,可变lambda
能省略参数列表:
void fcn3() {
size_t v1 = 42; // 局部变量
// f可以改变它所捕获的变量的值
auto f = [v1] () mutable { return ++v1; }
v1 = 0;
auto j = f(); // j为43
}
一个引用捕获的变量是否(如往常一样)可以修改依赖于此引用指向的是一个
const
类型还是一个非const
类型。
指定lambda
返回类型
默认情况下,如果一个
lambda
体包含return
之外的任何语句,则编译器假定此lambda
返回void
。与其他返回void
的函数类似,被推断返回void
的lambda
不能返回值。
函数transform
接受三个迭代器和一个可调用对象。前两个迭代器表示输入序列,第三个迭代器表示目的位置。算法对输入序列中每个元素调用可调用对象,并将结果写到目的位置。如果目的位置迭代器与表示输入序列开始位置的迭代器是相同的,则transform
将输入序列中每个元素替换为可调用对象操作该元素得到的结果:
transform(vi.begin(), vi.end(), vi.begin(), [] (int i) {
return i < 0 ? -i : i;
});
// 错误:编译器推断这个版本的lambda返回类型为void,但它返回了一个int值
transform(vi.begin(), vi.end(), vi.begin(), [] (int i) {
if (i < 0) {
return -i;
} else {
return i;
}
});
// 显示定义返回类型
transform(vi.begin(), vi.end(), vi.begin(), [] (int i) -> int {
if (i < 0) {
return -i;
} else {
return i;
}
});
10.3.4参数绑定
对于那种只在一两个地方使用的简单操作,lambda表达式是最有用的。如果需要在很多地方使用相同的操作,通常应该定义一个函数。类似的,如果一个操作需要很多语句才能完成,通常使用函数更好。
如果lambda的捕获列表为空,通常可以用函数来代替它。例如,既可以用一个lambda,也可以用函数isShorter
来实现将vector
中的单词按长度排序。
但是,对于捕获局部变量的lambda,用函数来替换它就不是那么容易了。例如,用在find_if
调用中的lambda比较一个string
和一个给定大小。可以很容易地编写一个完成同样工作的函数:
bool check_size(const string &s, string::size_type sz) {
return s.size() >= sz;
}
但是,不能用这个函数作为
find_if
的一个参数,原因在于find_if
接受一个一元谓词,因此传递给find_if
的可调用对象必须接受单一参数。为了用check_size
来代替lambda,必须解决这类问题。
标准库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
的第一个参数,以此类推:
bool checkSize(const string &s, string::size_type sz) {
return s.size() >= sz;
}
// 此bind调用只有一个占位符,表示只接受单一参数。占位符出现在arg_list的第一个位置,
// 表示此参数对应checkSize的第一个参数,且参数类型为const string&。因此,调用check6
// 必须传递给它一个string类型的参数,check6会将此参数传递给checkSize
auto check6 = bind(checkSize, std::placeholders::_1, 6);
auto wc = find_if(words.begin(), words.end(), bind(checkSize, _1, sz));
bind
的参数
可以用
bind
修正参数的值,更一般的,可以用bind
绑定给定可调用对象中的参数或重新安排其顺序:
auto g = bind(f, a, b, _2, c, _1);
g(x, y); // 等价于f(a, b, y, c, x)
// 用bind重排参数顺序
sort(words.begin(), words.end(), isShorter);
sort(words.begin(), words.end(), bind(isShorter, _2, _1));
绑定引用参数
默认情况下,
bind
的那些不是占位符的参数被拷贝到bind
返回的可调用对象中。但是,与lambda
类似,有时对有些绑定的参数希望以引用方式传递,或是要绑定的参数的类型无法拷贝:
ostream &print(ostream &os, const string &s, char c) {
return os << s << c;
}
但是,不能直接用
bind
来代替对流对象的捕获。如果希望传递一个对象又不拷贝它,就必须使用标准库ref
函数:
for_each(words.begin(), words.end(), bind(print, ref(os), _1, ' '));
函数
ref
返回一个对象,包含给定的引用,此对象是可以拷贝的。标准库还有一个cref
函数,生成一个保存const
引用的类。
10.4再探迭代器
标准库还定义了额外几种迭代器:
- 插入迭代器:这些迭代器被绑定到一个容器上,可用来向容器插入元素。
- 流迭代器:这些迭代器被绑定到输入或输出流上,可用来遍历所关联的IO流。
- 反向迭代器:这些迭代器向后而不是向前移动。除了
forward_list
之外的标准库容器都有反向迭代器。- 移动迭代器:这些专用的迭代器不是拷贝其中的元素,而是移动它们。
10.4.1插入迭代器
插入器是一种迭代器适配器,它接受一个容器,生成一个迭代器,能实现向给定容器添加元素。当通过一个插入迭代器赋值时,该迭代器调用容器操作来向给定容器的指定位置插入一个元素。
插入器有三种类型,差异在于元素插入的位置:
back_inserter
创建一个使用push_back
的迭代器。front_inserter
创建一个使用push_front
的迭代器。inserter
创建一个使用insert
的迭代器。此函数接受第二个参数,这个参数必须是一个指向给定容器的迭代器。元素将被插入到给定迭代器所表示的元素之前。理解插入器的工作过程是很重要的:当调用
inserter(c, iter)
时,得到一个迭代器,接下来使用它时,会将元素插入到iter
原来所指向的元素之前的位置。即:如果it
是由inserter
生成的迭代器,则:
*it = val;
// 等价于
it = c.insert(it, val); // it指向新加入的元素
++it; // 递增it使它指向原来的元素
front_inserter
生成的迭代器的行为与inserter
生成的迭代器完全不一样。当使用front_inserter
时,元素总是插入到容器第一个元素之前:
list<int> lst = {1, 2, 3, 4};
list<int> lst2, lst3;
copy(lst.begin(), lst.end(), front_inserter(lst2)); // 调用push_front:4 3 2 1
copy(lst.begin(), lst.end(), inserter(lst3)); // 1 2 3 4
10.4.2iostream
迭代器
istream_iterator
操作
当创建一个流迭代器时,必须指定迭代器将要读写的对象类型。一个
istream_iterator
使用>>
来读取流。因此,istream_iterator
要读取的类型必须定义了输入运算符。当创建一个istream_iterator
时,可以将它绑定到一个流。当然,还可以默认初始化迭代器,这样就创建了一个可以当作尾后值使用的迭代器:
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); // 从cin读取int
istream_iterator<int> eof; // istream尾后迭代器
while (in_iter != eof) { // 当有数据可供读取时
// 后置递增运算读取流,返回迭代器的旧值
// 解引用迭代器,获得从流读取的前一个值
vec.push_back(*int_it++);
}
// 可以将程序重写为如下形式,体现了istream_iterator更有用的地方
vector<int> vec(in_iter, eof); // 从迭代器范围构造vec
使用算法操作流迭代器
由于算法使用迭代器操作来处理数据,而流迭代器又至少支持某些迭代器操作,因此至少可以用某些算法来操作流迭代器:
istream_iterator<int> in(cin), eof;
cout << accumulate(in, eof, 0) << endl;
istream_iterator
允许使用懒惰求值
当将一个
istream_iterator
绑定到一个流时,标准库并不保证迭代器立即从流读取数据。具体实现可以推迟从流中读取数据,直到使用迭代器时才真正读取。标准库中的实现所保证的是,在第一次解引用迭代器之前,从流中读取数据的操作已经完成了。对于大多数程序来说,立即读取还是推迟读取没什么差别。但是,如果创建了一个istream_iterator
,没有使用就销毁了,或者正在从两个不同的对象同步读取同一个流,那么何时读取可能就很重要了。
ostream_iterator
操作
可以对任何具有输出运算符(
<<
)的类型定义ostream_iterator
。当创建一个ostream_iterator
时,可以提供(可选的)第二参数,它是一个字符串,在输出每个元素后都会打印此字符串。此字符串必须是一个c风格字符串(即,一个字符串字面值常量或者一个指向以空字符结尾的字符数组的指针)。必须将ostream_iterator
绑定到一个指定的流,不允许空的或表示尾后位置的ostream_iterator
。
可以用
ostream_iterator
来输出值的序列:
ostream_iterator<int> out_iter(cout, " ");
for (auto e : vec) {
// 赋值语句实际上将元素写到cout。值得注意的是,当向out_iter赋值时,可以忽略解引用
// 和递增运算,即等价于:outer_iter = e;但是,为了与其他迭代器的使用保持一致,
// 更推荐下面的写法,如果想将此循环改为操作其他迭代器类型,修改起来非常容易。
*out_iter++ = e;
}
cout << endl;
可以通过调用
copy
来打印vec
中的元素,比编写循环更为简单:copy(vec.begin(), vec.end(), out_iter);
。
使用流迭代器处理类类型
istream_iterator<Sales_item> item_iter(cin), eof;
ostream_iterator<Sales_item> out_iter(cout, "\n");
// 将第一笔交易记录在sum中,并读取下一条记录。
Sales_item sum = *item_iter++;
while (item_iter != eof) {
// 如果当前交易记录(存在item_iter中)有着相同的ISBN号
if (item_iter->isbn() == sum.isbn()) {
sum += *item_iter++; // 将其加到sum上并读取下一条记录
} else {
out_iter = sum; // 输出sum当前值
sum = *item_iter++; // 读取下一条记录
}
}
out_iter = sum; // 记得打印最后一组记录的和
10.4.3反向迭代器
反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器。对于反向迭代器,递增以及递减操作的含义会颠倒过来。递增一个反向迭代器会移动到前一个元素;递减一个迭代器会移动到下一元素。
除了forward_list
之外,其他容器都支持反向迭代器。可以通过调用rbegin
、rend
、crbegin
和crend
成员函数来获得反向迭代器。这些成员函数返回指向容器尾元素和首元素之前一个位置的迭代器。
需要注意的是,流迭代器不支持递减运算,因为不可能在一个流中反向移动。
反向迭代器和其他迭代器间的关系
// 在一个逗号分隔的列表中查找最后一个元素
auto rcomma = find(line.crbegin(), line.crend(), ',');
// 错误:将逆序输出单词的字符,即:如果输入的是FIRST,MIDDLE,LAST则这条语句会打印TSAL
cout << string(line.crbegin(), rcomma) << endl;
// 由于使用的是反向迭代器,会反向处理相关元素。因此,会从crbegin开始反向打印
而现在希望的是正常顺序打印,这个时候需要调用
reverse_iterator
的base
成员函数来完成这一转换,此成员函数会返回其对应的普通迭代器:
// 正确:得到一个正向迭代器,从逗号开始读取字符直到line末尾
cout << string(rcomma.base(), line.cend()) << endl;
rcomma
和rcomma.base()
指向不同的元素,line.crbegin()
和line.cend()
也是如此。这些不同保证了元素范围无论是正向处理还是反向处理都是相同的。
从技术上讲,普通迭代器与反向迭代器的关系反映了左闭合区间的特性。关键点在于[line.crbegin(), rcomma)
和[rcomma.base(), line.cend())
指向line
中相同的元素范围。为了实现这一点,rcomma
和rcomma.base()
必须生成相邻位置而不是相同位置,crbegin()
和cend()
也是如此。
10.5泛型算法结构
10.5.1五类迭代器
迭代器是按它们所提供的操作来分类的,而这种分类形成了一种层次。除了输出迭代器之外,一个高层类别的迭代器支持低层类别迭代器的所有操作。
C++标准指明了泛型和数值算法的每个迭代器参数的最小类别。例如,find
算法在一个序列上进行一遍扫描,对元素进行只读操作,因此至少需要输入迭代器。replace
函数需要一对迭代器,至少是前向迭代器。其第三个迭代器表示目的位置,必须至少是输出迭代器。其他的例子类似。对每个迭代器参数来说,其能力必须与规定的最小类别至少相当。向算法传递一个能力更差的迭代器会产生错误。
对于向一个算法传递错误类别的迭代器的问题,很多编译器不会给出任何警告或提示。
迭代器类别
输入迭代器(类似于泛型算法中的输入范围,因此用来读取数据):可以读取序列中的元素。一个输入迭代器必须支持:
- 用于比较两个迭代器的相等和不相等运算符。
- 用于推进迭代器的前置和后置递增运算。
- 用于读取元素的解引用运算符:解引用只会出现在赋值运算符的右侧。
- 箭头运算符,等价于
(*it).member
,即,解引用迭代器,并提取对象的成员。输入迭代器只用于顺序访问。对于一个输入迭代器,
*it++
保证是有效的,但递增它可能导致所有其他指向流的迭代器失效。其结果就是,不能保证输入迭代器的状态可以保存下来并用来访问元素。因此,输入迭代器只能用于单遍扫描算法。算法find
和accumulate
要求输入迭代器;而istream_iterator
是一种输入迭代器。输出迭代器:可以看作输入迭代器功能上的补集——只写而不读元素。输出迭代器必须支持:
- 用于推进迭代器的前置和后置递增运算。
- 解引用运算符,只出现在赋值运算符的左侧(向一个已经解引用的输出迭代器赋值,就是将值写入它所指向的元素)。
只能向一个输出迭代器赋值一次。类似输入迭代器,输出迭代器只能用于单遍扫描算法。用作目的位置的迭代器通常都是输出迭代器。例如,
copy
函数的第三个参数就是输出迭代器。ostream_iterator
类型也是输出迭代器。前向迭代器:可以读写元素。这类迭代器只能在序列中沿一个方向移动。前向迭代器支持所有输入和输出迭代器的操作,而且可以多次读写同一个元素。因此,可以保存前向迭代器的状态,使用前向迭代器的算法可以对序列进行多遍扫描。算法
replace
要求前向迭代器,forward_list
上的迭代器是前向迭代器。双向迭代器:可以正向/反向读写序列中的元素。除了支持所有前向迭代器的操作之外,双向迭代器还支持前置和后置递减运算符。算法
reverse
要求双向迭代器,除了forward_list
以外,其他标准库都提供符合双向迭代器要求的迭代器。随机访问迭代器:提供在常量时间内访问序列中任意元素的能力。此类迭代器支持双向迭代器的所有功能,此外还支持迭代器的加减、比较运算:
- 用于比较两个迭代器相对位置的关系运算符(
<
、<=
、>
和>=
)。- 迭代器和一个整数值的加减运算,计算结果是迭代器在序列中前进(或后退)给定整数个元素后的位置。
- 用于两个迭代器上的减法运算符,得到两个迭代器的距离。
- 下标运算符
(iter[n])
,与*(iter[n])
等价。算法
sort
要求随机访问迭代器。array
、deque
、string
和vector
的迭代器都是随机访问迭代器,用于访问内置数组元素的指针也是。
10.5.2算法形参模式
在任何其他算法分类之上,还有一组参数规范。大多数算法具有以下几种形式之一:
// alg是算法的名字,beg和end表示算法所操作的输入范围。
alg(beg, end, other args);
alg(beg, end, dest, other args);
alg(beg, end, beg2, other args);
alg(beg, end, beg2, end2, other args);
接受单个目标迭代器的算法
向输出迭代器写入数据的算法都假定目标空间足够容纳写入的数据。
如果dest
是一个直接指向容器的迭代器,那么算法将输出数据写到容器中已存在的元素内。更常见的情况是,dest
被绑定到一个插入迭代器或者是一个ostream_iterator
。插入迭代器会将新元素添加到容器中,因而保证空间是足够的。ostream_iterator
会将数据写入到一个输出流,同样不管要写入多少个元素都没有问题。
接受第二个输入序列的算法
这些算法通常使用第二个范围中的元素与第一个输入范围结合来进行一些运算。
接受单独的beg2
的算法假定从beg2
开始的序列与beg
和end
所表示的范围至少一样大。
10.5.3算法命名规范
一些算法使用重载形式传递一个谓词
接受谓词参数来代替
<
或==
运算符的算法,以及那些不接受额外参数的算法,通常都是重载的函数。函数的一个版本用元素类型的运算符来比较元素;另一个版本接受一个额外谓词参数,来代替<
或==
:
unique(beg, end); // 使用==运算符比较元素
unique(beg, end, comp); // 使用comp比较元素
_if
版本的算法
接受一个元素值的算法通常有另一个不同名的(不是重载的)版本,该版本接受一个谓词代替元素值。接受谓词参数的算法都有附加的
_if
前缀:
find(beg, end, val); // 查找输入范围中val第一次出现的位置
find_if(beg, end, pred); // 查找第一个令pred为真的元素
区分拷贝元素的版本和不拷贝的版本
默认情况下,重排元素的算法将重排后的元素写回给定的输入序列中。这些算法还提供另一个版本,将元素写到一个指定的输出目的位置。写到额外目的空间的算法都在名字后面附加一个
_copy
:
reverse(beg, end); // 反转输入范围中元素的顺序
reverse_copy(beg, end, dest); // 将元素按逆序拷贝到dest
一些算法同时提供
_copy
和_if
版本。这些版本接受一个目的位置迭代器和一个谓词:
// 从v1中删除奇数元素
remove_if(v1.begin(), v1.end(), [](int i) { return i % 2; });
// 将偶数元素从v1拷贝到v2,v1不变
remove_copy_if(v1.begin(), v1.end(), back_inserter(v2), [](int i) { return i % 2; });
10.6特定容器算法
对于
list
和forward_list
,应该优先使用成员函数版本的算法而不是通用算法。
例如,通用版本的sort
要求随机访问迭代器,因此不能用于list
和forward_list
,因为这两个类型分别提供双向迭代器和前向迭代器。而另外一些算法需要交换输入序列中的元素。一个链表可以通过改变元素间的链接而不是真的交换它们的值来快速交换元素。因此,这些链表版本的算法的性能比对应的通用版本好得多。
splice
成员
链表类型还定义了
splice
算法,此算法是链表数据结构所特有的,因此不需要通用版本。
链表特有的操作会改变容器
多数链表特有的算法都与其通用版本很相似,但不完全相同。链表特有版本与通用版本的一个至关重要的区别是链表版本会改变底层的容器。