一般情况下,这些算法并不直接操作容器,而是遍历由两个迭代器指定的一个元素范围来进行操作。通常情况下,算法遍历范围,对其中每个元素进行一些处理。例如,假定我们有一个int的vector,希望知道vector中是否包含一个特定值。回答这个问题最方便的方法是调用标准库算法find:
int val = 42;
// 如果在vec中找到想要的元素,则返回结果指向他,否则返回结果为vec.cend()
auto result = find(vec.cbegin(), vec.cend(), val);
cout << "The value" << val
<< (result == vec.cend()
? "is not present" : " is present") << endl;
传递给find
的前两个参数是表示元素范围的迭代器,第三个参数是一个值。find
将范围中每个元素与给定值进行比较。如果在vec中找到想要的元素,则返回结果指向他,否则返回结果为cend()
由于find操作的是迭代器,因此我们可以用同样的find函数在任何容器中查找值。
类似的,由于指针就像内置数组上的迭代器一样, 我们可以用find在数组中查找值:
int ia[] = {
0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int val = 42;
int* result = find(begin(ia), end(ia), val);
auto res = find(ia + 1, ia + 5, val);
算法如何工作
算法不依赖于容器所保存的元素类型。因此,只要有一个迭代器可用来访问元素,find就完全不依赖于容器类型(甚至无须理会保存元素的是不是容器)。
迭代器零算法不依赖于容器,但算法依赖于元素类型
虽然迭代器的使用令算法不依赖于容器类型,但大多数算法都使用了一个(或多个)元素类型上的操作。例如,find用元素类型的==运算符完成每个元素与给定值的比较。其他算法可能要求元素类型支持<运算符。不过,我们将会看到,大多数算法提供了一种方法,允许我们使用自定义的操作来代替默认的运算符。
除了少数例外,标准库算法都对一-个范围内的元素进行操作。我们将此元素范围称为“输入范围”。接受输入范围的算法总是使用前两个参数来表示此范围,两个参数分别是指向要处理的第一个元素和尾元素之后位置的迭代器。
虽然大多数算法遍历输入范围的方式相似,但它们使用范围中元素的方式不同。理解算法的最基本的方法就是了解它们是否读取元素、改变元素或是重排元素顺序。
一.些算法只会读取其输入范围内的元素,而从不改变元素。如find
、count
。
accumulate
,定义在头文件numeric
中。accumulate函数接受三个参数,前两个指出了需要求和的元素的范围,第三个参数是和的初值:
int sum = accumulate(vec.cbegin(), vec.cend(), 0);
【Note】第三个参数的类型决定了函数中使用哪个加法运算符以及返回值的类型
算法和元素类型
accumulate将第三个参数作为求和起点,这蕴含着一个编程假定:将元素类型加到和的类型上的操作必须是可行的。
由于string定义了+运算符,我们可以通过电泳accumulate来讲vector中所有string元素连接起来:
string sum = accumulate(v.cbeing(), v.cend(), string(""));
string sum2 = accumulate(v.cbeing(), v.cend(), "");
// 将空串当做一个字符串字面值传递给第三个参数是不可以的,会导致编译错误。
操作两个序列的算法
equal
用于确定两个序列是否保存相同的值。它将第一个序列中的每个元素与第二个序列中的对应元素进行比较。如果所有对应元素都相等,则返回true,否则返回false。此算法接受三个迭代器:前两个(与以往一样)表示第一个序列中的元素范围,第三个表示第二个序列的首元素:
// 序列2的元素数目应该至少与序列1一样多
equal(roster1.cbegin(), roster1.cend(), roster2.cbegin());
由于equal利用迭代器完成操作,因此我们可以通过调用equal来比较两个不同类型的容器中的元素。而且,元素类型也不必一样,只要我们能用==来比较两个元素类型即可。例如,在此例中,rosterl可以是vector,而roster2是list
迭代器参数
一些算法从两个序列中读取元素。构成这两个序列的元素可以来自于不同类型的容器。例如,第一个序列可能保存于一个vector中,而第二个序列可能保存于一个list、deque、内置数组或其他容器中。而且,两个序列中元素的类型也不要求严格匹配。算法要求的只是能够比较两个序列中的元素。例如,对equal算法,元素类型不要求相同,但是我们必须能使用一来比较来自两个序列中的元素。
操作两个序列的算法之间的区别在于我们如何传递第二个序列。一些算法,例如equal,接受三个迭代器:前两个表示第一个序列的范围,第三个表示第二个序列中的首元素。其他算法接受四个迭代器:前两个表示第一个序列的元素范围,后两个表示第二个序列的范围。
用一个单一迭代器表示第二个序列的算法都假定第二个序列至少与第一个一样长。确保算法不会试图访问第二个序列中不存在的元素是程序员的责任。例如,算法equal会将其第一个序列中的每个元素与第二个序列中的对应元素进行比较。如果第二个序列是第一个序列的一个子集,则程序会产生一个严重错误。equal会试图访问第二个序列中末尾之后(不存在)的元素。
一些算法将新值赋予序列中的元素。当我们使用这类算法时,必须注意确保序列原大小大于等于我们要求算法写入的元素数目。记住,算法不会执行容器操作,因此它们自身不可能改变容器的大小。
一些算法会自己向输入范围写入元素。这些算法本质上并不危险,它们最多写入与给定序列一样多的元素。
例如,算法fill
接受一对迭代器表示一个范围,还接受一个值作为第三个参数。fill将给定的这个值赋予输入序列中的每个元素。
fill(vec.begin(), vec.end(), 0); // 范围内元素都置为0
算法不检查写操作
一些算法接受一个迭代器来指出一个单独的目的位置。这些算法将新值赋予一个序列中的元素,该序列从目的位置迭代器指向的元素开始。例如,函数fill_n
接受一个单迭代器、一个计数值和一个值。它将给定值赋予迭代器指向的元素开始的指定个元素:
vector<int> vec; // 空向量
fill_n(vec.begin(), 10, 0); // 错误:修给不存在的元素
vec中并没有元素——它是空的。这条语句的结果是未定义的。
向目的位置迭代器写入数据的算法假定目的位置足够大,能容纳要写入的元素。
插入迭代器back_inserter
一种保证算法有足够元素空间来容纳输出数据的方法是使用插入迭代器。插入迭代器是一种向容器中添加元素的迭代器。
通常情况,当我们通过一个迭代器向容器元素赋值时,值被赋予迭代器指向的元素。而当我们通过一个插入迭代器赋值时,一个与赋值号右侧值相等的元素被添加到容器中。
back_inserter
是定义在头文件iterator
的一个函数
back_inserter接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器。当我们通过此迭代器赋值时,赋值运算符会调用push_back
将一个具有给定值的元素添加到容器中:
vector<int> vec; // 空向量
auto it = back_inserter(vec); // 通过它赋值会将元素添加到vec中
*it = 42;
我们常常使用bac_inserter来创建一个 迭代器,作为算法的目的位置来使用。例如:
vector<int> vec;
// 正确:back_inserter创建一个插入迭代器,可用来向vec添加元素
fill_n(back_inserter(vec), 10, 0);
在每步迭代中,fill_n向给定序列的一个元素赋值。由于我们传递的参数是back_inserter返回的迭代器,因此每次赋值都会在vec上调用push_back。最终,这条fill_n 调用语句向vec的末尾添加了10 个元素,每个元素的值都是0
拷贝算法
拷贝(copy)算法是另一个向目的位置迭代器指向的输出序列中的元素写入数据的算法。此算法接受三个迭代器,前两个表示一个输入范围,第三个表示目的序列的起始位置。此算法将输入范围中的元素拷贝到目的序列中。传递给copy的目的序列至少要包含与输入序列一样多的元素,这一点很重要。
内置数组的拷贝:
int a1[] = {
0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int a2[sizeof(a1) / sizeof(*a1)]; // a1、a2大小一致
auto ret = copy(begin(a1), end(a1), a2);
copy返回的是其目的位置迭代器(递增后)的值。即,ret恰好指向拷贝到a2的尾元素之后的位置。
多个算法都提供所谓的“拷贝”版本。这些算法计算新元素的值,但不会将它们放置在输入序列的末尾,而是创建一个新序列保存这些结果。
例如,replace
算法读入一个序列,并将其中所有等于给定值的元素都改为另一个值。此算法接受4个参数:前两个是迭代器,表示输入序列,后两个一个是要搜索的值,另一个是新值。它将所有等于第一个值的元素替换为第二个值:
// 将所有值为0的元素都改为42
replace(ilist.begin(), ilist.end(), 42);
此调用将序列中所有的0都替换为42。 如果我们希望保留原序列不变,可以调用replace_copy
。 此算法接受额外第三个迭代器参数,指出调整后序列的保存位置:
// 使用back_inserter按需要增长目标序列
replace_copy(ilist.cbegin(), ilist.cend(), back_inserter(ivec), 0, 42);
此调用后,ilist并未改变,ivec包含ilist的一份拷贝,不过原来在ilist中值为0的元素在ivec中都变为42。
如sort
消除重复元素
为了消除重复元素,首先将vector排序,使得重复的元素都相邻出现。一旦vector排序完毕,我们就可以使用另一个称为unique
的标准库算法来重排vector,使得不重复的元素出现在vector的开始部分。由于算法不能执行容器的操作,我们将使用vector的erase成员来完成真正的删除操作:
void elimDups(vector<string> &words){
sort(words.begin(), words.end());
// unique重排输入范围,是的每个单词只出现一次排列在前部,返回指向不重复区域之后一个未知的迭代器
auto end_unique = unique(words.begin(), words.end());
words.erase(end_unique, words.end());
}
使用unique
unique
算法重排输入序列,将相邻的重复项“消除”,并返回一个指向不重复值范围末尾的迭代器。
很多算法都会比较输入序列中的元素。默认情况下,这类算法使用元素类型的<或==运算符完成比较。标准库还为这些算法定义了额外的版本,允许我们提供自己定义的操作来代替默认运算符。
例如,sort算法默认使用元素类型的<运算符。但可能我们希望的排序顺序与<所定义的顺序不同,或是我们的序列可能保存的是未定义<运算符的元素类型(如Sales_data)。 在这两种情况下,都需要重载sort的默认行为。
作为一个例子,假定希望在调用elimDups后打印vector的内容。此外还假定希望单词按其长度排序,大小相同的再按字典序排列。为了按长度重排vector,我们将使用sort的第二个版本,此版本是重载过的,它接受第三个参数,此参数是一个谓词(predicate)。
谓词
谓词是一个可调用的表达式,其返回结果是一个能用作条件的值。标准库算法所使用的谓词分为两类:一元谓词(unary predicate,意味着它们只接受单一参数)和二元谓词(binary predicate, 意味着它们有两个参数)。接受谓词参数的算法对输入序列中的元素调用谓词。因此,元素类型必须能转换为谓词的参数类型。
接受一个二元谓词参数的sort版本用这个谓词代替<来比较元素。这样做会将元素按大小重新排序:
// 比较函数
bool isShorter(const string &s1, const string &s2) {
return s1.size() < s2.size();
}
// 按长度由短至长排序
sort(words.begin(), words.end(), isShorter);
排序算法
在我们将words按大小重排的同时,还希望具有相同长度的元素按字典序排列。为了保持相同长度的单词按字典序排列,可以使用stable_sort
算法。这种稳定排序算法维持相等元素的原有顺序。
通常情况下,我们不关心有序序列中相等元素的相对顺序,它们毕竟是相等的。但是,在本例中,我们定义的“相等”关系表示“具有相同长度”。而具有相同长度的元素,如果看其内容,其实还是各不相同的。通过调用stable_sort,可以保持等长元素间的字典序:
elimDups(words); // 将words字典序重排,并消除重复单词
// 按长度重新排序,长度相同的单词维持字典序
stable_sort(words.begin(), words.end(), isShorter);
根据算法接受一元谓词还是二元谓词,我们传递给算法的谓词必须严格接受一个或两个参数。但是,有时我们希望进行的操作需要更多参数,超出了算法对谓词的限制。如果在编写划分序列的谓词时,可以不必为每个可能的大小都编写一个独立的谓词,显然更有实际价值。
一个相关的例子是, 我们将修改之前的程序,求大于等于一个给定长度的单词有多少。我们还会修改输出,使程序只打印大于等于给定长度的单词。
void biggies(vector<string> &words, vector<string>::size_type sz){
elimDups(words);
stable_sort(words.begin(), words.end(), isShorter);
// 获取一个迭代器,指向第一个满足size() >= sz 的元素
// 计算满足size() >= sz 的元素的数目
// 打印长度大于等于给定制的单词,每个单词后面接一个空格
}
我们的新问题是在vector中寻找第一个大于等于给定长度的元素。一旦找到了这个元素,根据其位置,就可以计算出有多少元素的长度大于等于给定值。
我们可以使用标准库find_if
算法来查找第一个具有特定大小的元素。类似find,find_if算法接受一对迭代器,表示一个范围。但与find不同的是,find_if的第三个参数是一个谓词。find_if算法对输入序列中的每个元素调用给定的这个谓词。它返回第一个使谓词返回非0值的元素,如果不存在这样的元素,则返回尾迭代器。
编写一个函数,令其接受一个string和一个长度,并返回一个bool值表示该string的长度是否大于给定长度,是一件很容易的事情。但是,find_if接受一元谓词-我们传递给find_if的任何函数都必须严格接受 一个参数,以便能用来自输入序列的一个元素调用它。没有任何办法能传递给它第二个参数来表示长度。为了解决此问题,需要使用另外一些语言特性。
lambda
一个lambda表达式表示一个可调用的代码单元。我们可以将其理解为一个未命名的内联函数。与任何函数类似,一个lambda 具有一个返回类型、一个参数列表和一个函数体。但与函数不同,lambda可能定义在函数内部。一个lambda表达式具有如下形式:
[capture list] (parameter list) -> return type {function body}
其中,capturelist(捕获列表)是一个lambda所在函数中定义的局部变量的列表(通常为空);return type、parameter list和function body与任何普通函数一样,分别表示返回类型、参数列表和函数体。但是,与普通函数不同,lambda必须使用尾置返回来指定返回类型。
我们可以忽略参数列表和返回类型,但必须永远包含捕获列表和函数体
auto f = [] {
return 42;};
此例中,我们定义了一个可调用对象f,它不接受参数,返回42。
lambda的调用方式与普通函数的调用方式相同,都是使用调用运算符:
cout << f() << endl;
向lamdba传递参数
作为一个带参数的lambda的例子,我们可以编写一个与isShorter函数完成相同功能的lambda:
[] (const string &a, const string &b) {
return a.size() < b.size(); };
如下所示,可以使用次lambda来滴爱哦用stable_sort
:
stable_sort(words.begin(), words.end,
[] (const string &a, const string &b) {
return a.size() < b.size(); });
当stable_sort需要比较两个元素时,他就会调用这个lambda表达式
使用捕获列表
虽然一个lambda可以出现在一个函数中,使用其局部变量,但它只能使用那些明确指明的变量。一个lambda通过将局部变量包含在其捕获列表中来指出将会使用这些变量。捕获列表指引lambda在其内部包含访问局部变量所需的信息。
在本例中,我们的lambda会捕获sz,并只有单一的string参数。其函数体会将string的大小与捕获的sz的值进行比较:
[sz] (const string &a) {
return a.size() >= sz; };
lambda以一对[]开始,我们可以在其中提供一个以逗号分隔的名字列表,这些名字都是它所在函数中定义的。
由于此lambda捕获sz,因此lambda的函数体可以使用sz。lambda不捕获words,因此不能访问此变量。如果我们给lambda提供一个空捕获列表,则代码会编译错误
【Note】一个lambda只有在其捕获列表中捕获一个它所在函数中的局部变量,才能在函数体中使用该变量。
调用find_if
使用此lambda,我们就可以查找第一个长度大于等于sz的元素:
// 获取一个迭代器,指向第一个满足size() >= sz的元素
auto wc = find_if(words.begin(), words.end(),
[sz] (const string &a) {
return a.size() >. sz; });
这里对find_if的调用返回一个迭代器,指向第一个长度不小于给定参数sz的元素。如果这样的元素不存在,则返回words.end()的一个拷贝。
计算从他开始到末尾总共有多少个元素:
auto count = words.end() - wc;
cout << count << " " << make_plural(count, "word", "s")
<< " of length " << sz << " or longer" << endl;
for_each算法
问题的最后一部分是打印words中长度大于等于sz的元素。为了达到这一目的,我们可以使用for_each 算法。此算法接受一个可调用对象,并对输入序列中每个元素调用此对象:
for_each(wc, words.end(), [](const string &s) {
cout << s << " "; });
cout << endl;
完整的biggies
void biggies(vector<string> &words, vector<string>::size_type sz) {
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 s.size() >= sz; });
// 计算满足size() >= sz的元素个数
auto count = words.end() - wc;
cout << count << " " << make_plural(count, "word", "s")
<< " of length " << sz << " or longer" << endl;
// 打印长度大于等于给定值的单词,每个单词后面接一个空格
for_each(wc, word.end(),
[] (const string &s) {
cout << s << " "; });
cout << endl;
}
当定义一个lambda时,编译器生成一个与lambda对应的新的(未命名的)类类型。
可以这样理解,当向一个函数传递一个lambda时,同时定义了一个新类型和该类型的一个对象,传递的参数就是此编译器生成的类类型的未命名对象。类似的,当使用auto定义- -个用lambda初始化的变量时,定义了一个从lambda生成的类型的对象。
默认情况下,从lambda生成的类都包含一个对应该lambda所捕获的变量的数据成员。类似任何普通类的数据成员,lambda的数据成员也在lambda对象创建时被初始化。
值捕获
类似参数传递,变量的捕获方式也可以是值或引用。到目前为止,我们的lambda采用值捕获的方式。与传值参数类似,采用值捕获的前提是变量可以拷贝。与参数不同,被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝:
void fcn1(){
size_t v1 = 42; // 局部变量
// 将v1拷贝到名为f的可调用对象
auto f = [v1] {
return v1; };
v1 = 0;
auto j = f(); // j为42;f保存了我们擦混见他时v1的拷贝
}
引用捕获
void fcn2(){
size_t v1 = 42;
// f2包含v1的引用
auto f2 = [&v1] {
return v1; };
v1 = 0;
auto j = f2(); // j为0;f2保存v1的引用而非拷贝
}
如果不能拷贝对象,则捕获唯一方法就是捕获其引用
隐式捕获
除了显式列出我们希望使用的来自所在函数的变量之外,还可以让编译器根据lambda体中的代码来推断我们要使用哪些变量。为了指示编译器推断捕获列表,应在捕获列表中写一个&
或=
。&告诉编译器采用捕获引用方式,=则表示采用值捕获方式。例如,我们可以重写传递给find_if的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 = ' '){
// ...
// os为隐式捕获,引用捕获方式;c为显式捕获,值捕获方式
for_each(words.begin(), words.end(),
[&, c] (const string &s) {
os << s << c; });
// c为隐式捕获,值捕获方式;os为显式捕获,引用捕获方式
for_each(words.begin(), words.end(),
[=, &os] (const string &s) {
os << s << c; });
}
当我们混合使用隐式捕获和显式捕获时,捕获列表中的第一个元素必须是一个&或=。此符号指定了默认捕获方式为引用或值。
可变lambda
默认情况下,对于一个值被拷贝的变量,lambda不会改变其值。如果我们希望能改变一个被捕获的变量的值,就必须在参数列表首加上关键字mutable
。因此,可变lambda能省略参数列表:
void fcn3(){
size_t v1 = 42;
auto f = [v1] () mutable {
return ++v1; };
v1 = 0;
auto j = f(); // 43;
}
指定lambda返回类型
默认情况下,如果一个 lambda体包含return之外的任何语句,则编译器假定此lambda返回void。与其他返回void的函数类似,被推断返回void的lambda不能返回值。
下面给出了一个简单的例子,我们可以使用标准库transform
算法和一个lambda来将一个序列中的每个负数替换为其绝对值。
函数transform
接受三个迭代器和一个可调用对象。前两个迭代器表示输入序列,第三个迭代器表示目的位置。算法对输入序列中每个元素调用可调用对象,并将结果写到目的位置。如本例所示,目的位置迭代器与表示输入序列开始位置的迭代器可以是相同的。当输入迭代器和目的迭代器相同时,transform将输入序列中每个元素替换为可调用对象操作该元素得到的结果。
transform(vi.begin(), vi.end(), vi.begin(),
[] (int i) -> int
{
if (i < 0) return -i; else return i; });
bool check_size(const string &s, string::size_type sz){
return s.size() >= sz;
}
标准库bind函数
我们可以解决向check_size传递一个长度参数的问题,方法是使用一个新的名为bind的标准库函数,它定义在头文件functional中。可以将bind
函数看作一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。
auto newCallable = bind(callable, arg_list)
其中,newCallable本身是一个可调用对象,arg_list 是一个逗号分隔的参数列表,对应给定的callable的参数。即,当我们调用newCallable时,newCallable会调用callable,并传递给它arg_list 中的参数。
arg_list 中的参数可能包含形如_n
的名字,其中n是一个整数。这些参数是“占位符”,表示newallable的参数,它们占据了传递给newCallable的参数的“位置”。数值n表示生成的可调用对象中参数的位置:_1为newCallable的第一个参数,_2为第二个参数,依此类推。
绑定check_size的sz参数
auto check6 = bind(check_size, _1, 6);
此bind调用只有一个占位符,表示check6只接受单一参数。占位符出现在arg_list的第一个位置,表示check6的此参数对应check_size的第一个参数。 此参数是一个const string&。因此,调用check6必须传递给它一个string类型的参数,check6会将此参数传递给check_size
string s = "hello";
bool bl = check6(s); // check6(s)会调用check_size(s, 6);
使用bind,我们可以将原来基于lambda的find_if调用:
auto wc = find_if(words.begin(), words.end(),
[sz] (const string &a));
替换为如下使用check_size的版本:
auto wc = find_if(words.begin(), words.end(),
bind(check_size, _1, sz));
使用placeholders名字
名字n都定义在一个名为placeholders
的命名空间中,而这个命名空间本身定义在std
命名空间中。为了使用这些名字,两个命名空间都要写上。与我们的其他例子类似,对bind
的调用代码假定之前已经恰当地使用了using
声明。例如,_1
对应的using声明为:
using namespace std::placeholders;
使得由placeholders定义的所有名字都可用。与bind函数一样,placeholders命名空间也定义在functional
头文件中。
bind的参数
如前文所述,我们可以用bind修正参数的值。更一般的,可以用bind绑定给定可调用对象中的参数或重新安排其顺序。例如,假定f是一个可调用对象,它有5个参数,则下面对bind的调用:
// g是一个有两个参数的可调用对象
auto g = bind(f, a, b, _2, c, _1);
实际上,这个bind调用会将g(_1, _2)
映射为f(a, b, _2, c, _1)
例如调用g(X, Y)
会调用f(a, b, Y, c, X)
用bind重排参数顺序
我们用bind颠倒isShorter的含义:
// 按单词长度由短至长排序
sort(words.begin(), words.end(), isShorter);
// 按单词长度由长自短排序
sort(words,begin(), words.end(), bind(isShorter, _2, _1));
绑定引用参数
默认情况下,bind的那些不是占位符的参数被拷贝到bind返回的可调用对象中。
但是,与 lambda类似,有时对有些绑定的参数我们希望以引用方式传递,或是要绑定参数的类型无法拷贝。
除了为每个容器定义的迭代器之外,标准库在头文件iterator
中还定义了额外几种迭代器。这些迭代器包括以下几种:
forward_list
之外的标准库容器都有反向迭代器。插入器是一种迭代器适配器,它接受一个容器,生成一个迭代器,能实现向给定容器添加元素。当我们通过一个插入迭代器进行赋值时,该迭代器调用容器操作来向给定容器的指定位置插入一个元素。
插入迭代器有三种类型,差异在于元素插入的位置:
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
时,元素总是插入到容器第一个元素之前。即使我们传递给inserter的位置原来指向第一个元素,只要我们在此元素之前插入一个新元素,此元素就不再是容器的首元素了:
list<int> lst = {
1, 2, 3, 4};
list<int> lst2, lst3; // 空list
// 拷贝完成之后,lst2包含4 3 2 1
copy(lst.cbegin(), lst.cend(), front_inserter(lst2));
// 拷贝完成之后,lst3包含1 2 3 4
copy(lst.cbegin(), lst.cend(), inserter(lst3, lst3.begin()));
虽然iostream类型不是容器,但标准库定义了可以用于这些IO类型对象的迭代器。istream_iterator
读取输入流,ostream_iterator
向一个输出流写数据。这些迭代器将它们对应的流当作一个特定类型的元素序列来处理。通过使用流迭代器,我们可以用泛型算法从流对象读取数据以及向其写入数据。
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(*in_iter++);
for_each(vec.cbegin(), vec.cend(),
[](const int num) {
cout << num << " "; });
cout << endl;
我们可以重写程序如下,这体现了istream_iterator更有用的地方:
istream_iterator<int> in_iter(cin), eof;
vector<int> vec(in_iter, eof); // 从迭代器范围构造vec
我们可以用一对istream_iterator
来调用accumulate
:
istream_iterator<int> in(cin), eof;
cout << accumulate(in, eof, 0) << endl;
此调用会从标准输入读取的值的和。
istream_iterator允许使用懒惰求值
当我们将一个istream_iterator
绑定到一个流时,标准库并不保证迭代器立即从流读取数据。具体实现可以推迟从流中读取数据,直到我们使用迭代器时才真正读取。
标准库中的实现所保证的是,在我们第一次解引用迭代器之前,从流中读取数据的操作已经完成了。对于大多数程序来说,立即读取还是推迟读取没什么差别。但是,如果我们创建了一个istream_iterator
,没有使用就销毁了,或者我们正在从两个不同的对象同步造取同一个流,那么何时读取可能就很重要了。
ostream_iterator操作
ostream_iterator<int> out_iter(cout, " ");
for (auto e : vec)
*out_iter++ = e; // 赋值语句实际上将元素写到cout
cout << endl;
使用copy
打印vec的元素:
copy(vec.begin(), vec.end(), out_iter);
cout << endl;
使用流迭代器处理类类型
反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器。对于反向迭代器,递增、递减操作的含义会颠倒过来。
除了forward_list
之外,其他容器都支持反向迭代器。我们可以通过调用rbegin、rend、crbegin和crend成员函数来获得反向迭代器。这些成员函数返回指向容器尾元素和首元素之前以个位置的迭代器。与普通迭代器一样,反向迭代器也有const和非const版本。
反向迭代器需要递减运算符
除了forward_list
之外,标准容器上的其他迭代器都既支持递增运算又支持递减运算。
但是,流迭代器不支持递减运算,因为不可能在一个流中反向移动。
因此,不可能从一个forward_list或一个流迭代器创建反向迭代器。
反向迭代器与其他迭代器的关系
假定有一个名为line的string,保存着一个逗号分隔的单词列表,我们希望打印line中的第一个单词。使用find
可以很容易地完成这一任务:
auto comma = find(line.cbegin(), line.cend(), ',');
cout << string(line.cbegin(), comma) << endl;
如果希望打印最后一个单词,可以使用反向迭代器:
auto rcomma = find(line.crbegin(), line.crend(), ',');
cout << string(line.crbegin(), rcomma) << endl; // 错误:将逆序输出单词
cout << string(line.base(), line.cend()) << endl; // 正确
我们使用的是反向迭代器,会反向处理string。因此,上述输出语句从crbegin开始反向打印line中内容。而我们希望按正常顺序打印从rcomma开始到line末尾间的字符。但是,我们不能直接使用rcomma。因为它是一个反向迭代器,意味着它会反向朝着string的开始位置移动。需要做的是,将rcomma转换回一个普通迭代器,能在line中正向移动。我们通过调用reverse_ iterator
的base
成员函数来完成这一转换,此成员函数会返回其对应的普通迭代器
类似容器,迭代器也定义了一组公共操作。一些操作所有迭代器都支持,另外一些只有特定类别的迭代器才支持。例如,ostream_iterator 只支持递增、解引用和赋值。vector、string 和deque的迭代器除了这些操作外,还支持递减、关系和算术运算。
大多数算法具有如下4种形式之一:
alg(beg, end, other args);
alg(beg, end, dest, other args);
alg(beg, end, beg2, other args);
alg(beg, end, beg2, end2, other args);
一些算法使用重载形式传递一个谓词
unique(beg, end);
unique(beg, end, comp);
_if版本算法
接受一个元素值的算法通常有另一个不同名的(不是重载的)版本,该版本接受一个谓词代替元素值。接受谓词参数的算法都有附加的_ if
前缀:
find(beg, end, val);
find_if(beg, end, pred); // 查找第一个令pred为真的元素
这两个算法提供了命名上差异的版本,而非重载版本,因为两个版本的算法都接受相同数目的参数。因此可能产生重载歧义,虽然很罕见,但为了避免任何可能的歧义,标准库选择提供不同名字的版本而不是重载。
区分拷贝元素的版本和不拷贝元素的版本
默认情况下,重排元素的算法将重排后的元素写回给定的输入序列中。这些算法还提供另一个版本,将元素写到一个指定的输出目的位置。如我们所见,写到额外目的空间的算法都在名字后面附加一个_copy
:
reverse(beg, end);
reverse(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; });
两个算法都调用了lambda来确定元素是否为奇数。在第一个调用中,我们从输入序列中将奇数元素删除。在第二个调用中,我们将偶数元素从输入范围拷贝到v2中。
与其他容器不同,链表类型list
和forward_list
定义了几个成员函数形式的算法。特别是,它们定义了独有的sort
、merge
、remove
、reverse
和unique
。通用版本的sort要求随机访问迭代器,因此不能用于list和forward_list,因为这两个类型分别提供双向迭代器和前向迭代器。
splice成员
多数链表特有的算法都与其通用版本很相似,但不完全相同。链表特有版本与通用版本间的一个至关重要的区别是链表版本会改变底层的容器。例如,remove
的链表版本会删除指定的元素。unique
的链表版本会删除第二个和后继的重复元素。
类似的,merge
和splice
会销毁其参数。例如,通用版本的merge将合并的序列写到一个给定的目的迭代器;两个输入序列是不变的。而链表版本的merge函数会销毁给定的链表——元素从参数指定的链表中删除,被合并到调用merge的链表对象中。在merge之后,来自两个链表中的元素仍然存在,但它们都已在同一个链表中。