C++ Primer 总结索引 | 第十章:泛型算法

1、泛型算法:标准库并未给每个容器添加大量功能,而是提供了一组算法,这些算法中的大多数都 独立于任何特定的容器。这些算法是通用的:它们可用于 不同类型的容器 和 不同类型的元素

2、顺序容器只定义了很少的操作:在多数情况下,我们可以 添加和删除元素、访问首尾元素、确定容器是否为空 以及 获得指向首元素或尾元素之后位置的选代器

定义了一组泛型算法,称它们为“算法”,是因为它们实现了一些 经典算法的公共接口,如 排序和搜索:称它们是 “泛型的”,是因为它们 可以用于不同类型的元素 和 多种容器类型(不仅包括 标准库类型,如vector或list,还包括 内置的数组类型)

1、概述

1、大多数算法都定义在 头文件algorithm中(不特别提,基本都在)。标准库还在 头文件numeric 中定义了一组 数值泛型算法

这些算法 并不直接操作容器,而是遍历 由两个送代器指定的 一个元素范围 来进行操作。算法 遍历范围,对其中 每个元素进行一些处理。假定 有一个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将范围中 每个元素 与给定值进行比较。它返回指向 第一个等于给定值的元素 的迭代器。如果范围中 无匹配元素,则find返回 第二个参数 来表示搜索失败。因此,可以通过比较返回值 和 第二个参数 来判断搜索是否成功

由于find操作的是 迭代器,因此 可以用同样的find函数 在任何容器中查找值
例如,可以 用find在一个string的list中 查找一个给定值:

string val = "a value"; // 要查找的值
//此调用 在list中查找string元素
auto result = find(lst.cbegin(), lst.cend(), val);

指针就像 内置数组上的迭代器 一样,可以 用find 在数组中查找值:

int ia[] = {27, 210, 12, 47, 109, 83};
int val = 83;
int* result = find(begin(ia), end(ia), val);

使用了 标准库begin和end函数,来获得 指向ia中首元素和尾元素之后位置 的指针,并传递 给find

还可以在 序列的子范围中查找,只需 将指向子范围首元素和尾元素之后位置的迭代器(指针)传递给find。例如,下面的语句在i a[1]、ia[2] 和 ia[3] 中查找给定元素:

// 在从ia[1]开始,直至(但不包含)ia[4]的范围内 查找元素
auto result = find(ia + 1, ia + 4, val);

2、算法如何工作:
1、访问序列中的首元素
2、比较此元素与我们要查找的值
3、如果此元素与我们要查找的值匹配,find返回标识此元素的值
4、否则,find前进到下一个元素,重复执行步骤2和3
5、如果到达序列尾,find应停止
6、如果find到达序列末尾,它应该返回一个指出元素未找到的值。此值和步骤3返回的值必须具有相容的类型

这些步骤 都不依赖于容器所保存的元素类型。因此,只要 有一个迭代器 可用来访问元素,find就 完全不依赖于容器类型(甚至 无须理会 保存元素的是不是容器)

迭代器 令算法不依赖于容器,除了 第2步外,其他步骤 都可以用 迭代器操作来实现
但算法 依赖于元素类型的操作。虽然 迭代器的使用 令算法不依赖于容器类型,但 大多数算法都使用了一个(或多个元素类型上的操作。find 用元素类型的==运算符 完成每个元素与给定值的比较。其他算法 可能要求元素类型 支持 < 运算符。大多数算法 提供了一种方法,允许 使用自定义的操作 来代替默认的运算符

3、头文件algorithm中 定义了 一个名为count的函数,它类似find,接受 对迭代器 和 一个值 作为参数。count返回 给定值在序列中出现的次数

4、关键概念:算法永远不会执行容器的操作。它们只会 运行于选代器之上,执行送代器的操作。泛型算法 运行于迭代器之上 而不会执行容器操作,所以 算法永远不会改变底层容器的大小。算法 可能改变容器中保存的元素的值,也可能 在容器内移动元素,但 永远不会 直接添加 或 删除元素

5、标准库 定义了 一类特殊的迭代器,称为插入器(inserter)与普通迭代器 只能遍历 所绑定的容器相比,插入器 能做更多的事
情。当给这类迭代器 赋值时,会在底层的容器上 执行插入操作。因此,当一个算法操作 一个这样的迭代器时,迭代器 可以完成向容器添加元素的效果,但算法自身永远不会 做这样的操作

inserter是 C++ STL 中的一个 迭代器适配器,可以 在容器中插入元素 而不会覆盖现有元素。通常情况下,它与算法函数(例如 copy、transform 等)一起使用,允许将元素插入到目标容器的特定位置,而不会覆盖原有元素
inserter适配器的使用方式 如下:

#include <iostream>
#include <algorithm>
#include <vector>
#include <set>

int main() {
    std::vector<int> source = {1, 2, 3, 4, 5};
    std::set<int> destination;

    // 使用 inserter 将 source 中的元素插入到 destination 中
    std::copy(source.begin(), source.end(), std::inserter(destination, destination.begin()));

    // 打印 destination 中的元素
    for (auto num : destination) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}

std::inserter 适配器被用来在 destination 集合中插入元素,这样可以确保元素被插入到集合的正确位置 而不会破坏集合的有序性质。inserter 接受两个参数:目标容器和一个迭代器,该迭代器用于指示在目标容器中插入元素的位置
使用 std::inserter,需要包含 <iterator> 头文件

2、初识泛型算法

除了少数例外,标准库算法 都对一个范围内的元素 进行操作。将此元素范围称为 “输入范围”。接受输入范围的算法 总是使用前两个参数来表示此范围,两个参数分别是指 向要处理的第一个元素 和 尾元素之后位置的迭代器

理解算法的最基本的方法 就是了解它们是否读取元素、改变元素 或 是重排元素顺序

2.1 只读算法

1、一些算法 只会读取其输入范围内的元素,而 从不改变元素。如 find,count

只读算法还有 accumulate,它定义在 头文件numeric中。accumulate函数 接受三个参数,前两个指出了 需要求和的元素的范围,第三个参数是 和的初值。假定 vec是一个整数序列

//对vec中的元素 求和,和的初值是0
int sum = accumulate(vec.cbegin(), vec.cend(), 0); 

accumulate的第三个参数的类型 决定了函数中 使用哪个加法运算符 以及 返回值的类型

accumulate将 第三个参数作为 求和起点,这蕴含着一个编程假定:将元素类型加到和的类型上的操作 必须是可行的。即,序列中元素的类型 必须与 第三个参数匹配,或者 能够转换为第三个参数的类型。在上例中,vec中的元素可以是int,或者是double、long long 或 任何其他可以 加到int上的类型

由于string定义了 + 运算符,所以 可以通过 调用accumulate来 将vector中所有string元素 连接起来

string sum = accumulate(v.c begin(), v.cend(), string(""));

通过 第三个参数 显式地创建了 一个string。将空串当做一个字符串字面值 传递给第三个参数 是不可以的,会导致一个编译错误

//错误:const char*上没有定义 + 运算符
string sum = accumulate(v.cbegin(), v.cend(), "");

对于 只读取而不改变元素的算法,通常 最好使用cbegin() 和 cend()。如果 计划使用算法返回的迭代器 来改变元素的值,就需要使用 begin() 和 end() 的结果作为参数

2、操作两个序列的算法:另一个只读算法是equal,用于确定 两个序列是否保存相同的值,如果 所有对应元素都相等,则返回
true。此算法接受 三个迭代器:前两个(与以往一样)表示 第一个序列中的元素范围,第三个 表示 第二个序列的首元素

// roster2中的元素数目 应该至少与roster1一样多
equal(roster1.cbegin(), roster1.cend(), roster2.cbegin()); 

可以通过 调用equal来比较 两个不同类型的容器中的元素。而且,元素类型 也不必一样,只要 能用 == 来比较两个元素类型即可
例如,在此例中,roster1可以是vector<string>,而roster2是list<const char*>

只接受一个单一选代器来表示第二个序列的算法,都假定 第二个序列至少与第一个序列一样长
如果第二个序列是第一个序列的一个子集 则程序会产生一个严重错误——equal会试图 访问第二个序列中末尾之后(不存在)的元素

在调用equal 的例子中,如果两个名册中保存的都是C风格字符串而不是string,equal会比较 指针地址,而不是 字符串值,比较的结果与string类型的不一致

3、假定v是一个vector<double>,那么调用 accumulate(v.cbegin(), v.cend(), 0) 有何错误:结果默认是 int类型的,精度会降低。accumulate返回的类型 以 初始值类型为准

2.2 写容器元素的算法

1、将新值赋予序列中的元素,必须注意确保 序列原大小 至少不小于 要求算法写入的元素数目。算法 不会执行容器操作,因此它们自身 不可能改变容器的大小

一些算法会自己向输入范围 写入元素。这些算法本质上并不危险,它们最多写入 与给定序列一样多的元素
算法fill接受 一对迭代器表示一个范围,还接受一个值 作为第三个参数。fill将给定的这个值 赋予输入序列中的每个元素

fill(vec.begin(), vec.end(), 0); // 将每个元素重置为0
// 将容器的一个子序列设置为10
fill(vec.begin(), vec.begin() + vec.size() / 2, 10); 

fill 向给定输入序列中 写入数据,因此,只要 传递了一个有效的输入序列,写入操作 就是安全的

2、关键概念:迭代器参数
从两个序列中 读取元素。构成这两个序列的元素 可以来自于不同类型的容器。第一个序列 可能保存于一个vector中,而第二个序列 可能保存于一个list、deque、内置数组 或 其他容器中。两个序列中元素的类型 也不要求严格匹配。算法 要求的只是 能够比较两个序列中的元素。
例如,对equal算法,元素类型不要求相同 但是 必须能使用来比较来自两个序列中的元素

3、算法不检查写操作:这些算法 将新值赋予 一个序列中的元素,该序列 从目的位置迭代器指向的元素 开始
例如,函数fill_n接受一个单迭代器、一个计数值和一个值

// 使用vec,赋予它不同值
fill_n(vec.begin(), vec.size(), 0); // 将所有元素重置为0

函数fill_n假定 写入指定个元素是安全的。即,如下形式的调用

fill_n(dest, n, val)

fill_n假定 dest指向一个元素,而从dest开始的序列 至少包含n个元素

在一个空容器上调用fill_n(或类似的写元素的算法)

vector<int>vec; // 空向量   
fill_n(vec.begin(), 10, 0); // 灾难:修改vec中的10个(不存在)元素

这个调用是一场灾难,这条语句的结果是未定义的
向目的位置迭代器 写入数据的算法 假定目的位置足够大,能容纳要写入的元素

4、back_inserter:保证算法 有足够元素空间 来容纳输出数据的方法是 使用插入送代器(insert iterator)。插入迭代器 是一种向容器中 添加元素的迭代器。通常情况,当通过一个迭代器向容器元素赋值时,值 被赋予迭代器指向的元素。而当 通过一个插入迭代器赋值时,一个与赋值号右侧值相等的元素 被添加到容器中

使用back_inserter,它是 定义在头文件iterator中的一个函数
back_inserter接受一个 指向容器的引用,返回一个 与该容器绑定的插入迭代器
当通过此迭代器赋值时,赋值运算符会调用push_back将一个具有给定值的元素 添加到容器中:

vector<int> vec; // 空向量
auto it = back_inserter(vec); // 通过它赋值会将元素添加到vec中
*it = 42; // vec中现在有一个元素,值为42

使用back_inserter来 创建一个迭代器,作为 算法的目的位置 来使用

vector<int> vec; // 空向量
// 正确:back_inserter创建 一个插入迭代器,可用来向vec添加元素
fill_n(back_inserter(vec), 10, 0); // 添加10个元素到vec

在每步迭代中,fill_n向 给定序列的一个元素 赋值。由于传递的参数是 back_inserter返回的迭代器,因此 每次赋值都会在vec上调用push_back。最终,这条fill_n调用语句 向vec的末尾添加了10个元素,每个元素的值 都是0

编写程序,使用 fill_n 将一个序列中的 int 值都设置为 0

#include <iostream>
#include <numeric>
#include <vector>
#include <iterator>

using namespace std;

int main() {
	vector<int> v1(10, 1);
	fill_n(v1.begin(), v1.size(), 0); // 改变元素内容,v1.begin不能设成v1.cbegin()
	for (int i : v1) {
		cout << i << " ";
	}
	cout << endl;

	vector<int> v2;
	fill_n(back_inserter(v2), 10, 1);
	for (int i : v2) {
		cout << i << " ";
	}
	cout << endl;
	return 0;
}

5、拷贝算法:是另一个 向目的位置迭代器指向的输出序列中的元素 写入数据的算法。此算法 接受三个选代器,前两个表示一个输入范围,第三个表示 目的序列的起始位置。此算法 将输入范围中的元素 拷贝到目的序列中。传递给copy的目的序列至少要包含 与输入序列一样多的元素

vector<int> vec; list<int> lst; int i;
while (cin >> i)
	lst.push_back(i);
copy(lst.cbegin(), lst.cend(), vec.begin()); // 错误

修改最后一行:copy(lst.cbegin(), lst.cend(), back_inserter(vec));

用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

copy返回的是 其目的位置迭代器(递增后)的值。即,ret恰好指向 拷贝到a2的尾元素之后的位置

多个算法 都提供所谓的“拷贝”版本。这些算法 计算新元素的值,但不会 将它们放置在 输入序列的末尾,而是 创建一个新序列保存这些结果

replace算法 读入一个序列,并将 其中所有等于给定值的元素 都改为另一个值。此算法 接受4个参数:前两个是迭代器,表示输入序列,后两个 一个是要搜索的值,另一个是新值。它将所有 等于第一个值的元素 替换为第二个值:

//将所有值为0的元素 改为42
replace(ilst.begin(), ilst.end(), 0, 42); 

此调用 将序列中所有的0 都替换为42。如果 希望保留原序列不变,可以调用replace_copy。此算法接受额外第三个送代器参数,指出调整后序列的保存位置

//使用back_inserter按需要 增长目标序列
replace(ilst.cbegin(), ilst.cend(), 
	back_inserter(ivec),0, 42); 

调用后,ilst并未改变,ivec包含ist的一份拷贝,不过原来在ilst中值为0的元素 在i vec中都变为42

6、修改 下面程序的错误

vector<int> vec;
vec.reserve(10);
fill_n(vec.begin(), 10, 0);

与预期不符,vec并没有10个元素,且每个元素都为0。可以加上:vec.resize(10);
或改为:fill_n(back_inserter(v), 10, 0);

vec.reserve(10); 会预留足够的内存空间以容纳至少 10 个元素,但是 使用了 fill_n(vec.begin(), 10, 0); 来填充元素。问题在于,尽管 vector 的存储空间已经预留了,但它的 size 并没有相应地增加,因此 vec.begin() 并不指向任何元素,而是指向尚未分配的内存空间

reserve 是 C++ 标准库中用于容器预留存储空间的成员函数。它主要用于 vector、deque 和 string 等动态数组容器,允许在事先知道容器可能存储的元素数量时,预先分配存储空间,从而避免多次重新分配内存带来的性能开销
参数值 不必等于容器的实际大小,只需 大于或等于容器的当前大小

7、标准库算法不会改变它们所操作的容器的大小。为什么使用 back_inserter 不会使这一断言失效?
标准库算法从来不直接操作容器,它们只操作迭代器,从而间接访问容器。能不能插入和删除元素,不在于算法,而在于传递给它们的迭代器是否具有这样的能力

2.3 重排容器元素的算法

1、例子是sort。调用sort 会重排输入序列中的元素,使之有序,它是 利用元素类型的运算符 来实现排序的

2、为了 消除重复单词,首先 将vector排序,使得重复的单词 都相邻出现。一旦vector排序完毕,就可以使用 另一个称为unique的标准库算法 来重排vector,使得不重复的元素 出现在vector的开始部分。由于算法 不能执行容器的操作,将使用vector的 erase成员 来完成 真正的删除操作

void elimDups(vector<string> &words)
{
	// 按字典序 排序words,以便查找重复单词
	sort(words.begin(), words.end());
	// unique重排输入范围,使得 每个单词只出现一次
	// 排列在范围的前部,返回 指向不重复区域之后一个位置的迭代器
	auto end_unique = unique(words.begin(), words.end()); 
	// 使用向量操作 erase删除重复单词
	words.erase(end_unique, words.end());
}

3、使用unique:将删除打引号 是因为unique并不真的删除任何元素,它只是 覆盖相邻的重复元素,使得 不重复元素出现在序列开始部分。此位置之后的元素仍然存在,但 不知道它们的值是什么

4、认为算法不改变容器大小的原因?
泛型算法的一大优点是 “泛型”,也就是一个算法可用于多种不同的数据类型,算法与所操作的数据结构分离。这对编程效率的提升是非常巨大的

要做到算法与数据结构分离,重要的技术手段就是使用迭代器作为两者的桥梁。算法从不操作具体的容器,从而也就不存在与特定容器绑定,不适用于其他容器的问题。算法只操作迭代器,由迭代器真正实现对容器的访问。不同容器实现自己特定的迭代器(但不同迭代器是相容的),算法操作不同迭代器就实现了对不同容器的访问

比如 当向 fill_n 传递 back_inserter 时,虽然最终效果是向容器添加了新元素,但对 fill_n 来说,根本不知道这回事儿。它仍然像往常一样(通过迭代器)向元素赋予新值,只不过这次是通过 back_inserter 来赋值,而 back_inserter 选择将新值添加到了容器而已

3、定制操作

很多算法 都会比较 输入序列中的元素。默认情况下,这类算法 使用元素类型的 < 或 == 运算符 完成比较。标准库还为这些算法 定义了额外的版本,允许 提供自己定义的操作 来代替默认运算符

sort算法 默认使用元素类型的运算符。但可能 希望的排序顺序 与<所定义的顺序不同,或是 序列可能保存的是 未定义<运算符的元素类型(如Sales_data)。在这两种情况下,都需要 重载sort的默认行为

3.1 向算法传递函数

1、假定 希望在调用elimDups后 打印vector的内容。此外 还假定希望单词 按其长度排序,大小相同的 再按字典序排列。为了按长度重排 vector,将使用sort的 第二个版本,此版本 是重载过的,它接受第三个参数,此参数是 一个谓词

2、谓词 是一个可调用的表达式,其返回结果 是一个 能用作条件的值。标准库算法所使用的谓词 分为两类:一元谓词(意味着只接受单一参数)和 二元谓词(意味着 有两个参数)。接受谓词参数的算法 对输入序列中的元素 调用谓词。因此,元素类型必须 能转换为谓词的参数类型

接受一个 二元谓词参数 的sort版本 用这个谓词代替 < 来比较元素。此操作 必须在 输入序列中所有可能的元素值上 定义一个一致的序。可以 将isShorter传递给sort。这样做会将元素 按大小重新排序(返回值是bool)

// 比较函数,用来按长度排序单词
bool isShorter(const string &sl, const string &s2) 
	returns sl.size() < s2.size();
// 按长度由短至长排序words
sort(words.begin(), words.end(), isShorter);

3、排序算法:将words 按大小重排的同时,还希望 具有相同长度的元素 按字典序排列。为了保持相同长度的单词 按字典序排列,可以使用stable_sort算法。这种稳定排序算法 维持相等元素的原有顺序

elimDups(words); // 将words按字典序重排,并消除重复单词
// 按长度重新排序,长度相同的单词维持字典序
stable_sort(words.begin(), words.end(), isShorter);
for (const auto &S : words) // 无须拷贝字符串
	cout << S << ""; // 打印每个元素,以空格分隔
cout << endl;

4、编写名为compare_Isbn的函数,比较 两个 Sales_data 对象的 isbn() 成员。使用这个函数 排序一个保存Sales_data对象的vector

#include <iostream>
#include <string>
#include <vector>
#include "D:\VS\workspace\Primer8\Primer8\Sales_data.h"
#include <fstream>
#include <algorithm>

using namespace std;
bool compare(const Sales_data& s1, const Sales_data& s2) {
	return s1.isbn() < s2.isbn();
}

int main()
{
	ifstream ifs("D:/VS/workspace/Primer8/Primer8/data_8.txt"); // 要不两个反斜杠,要不一个正斜杠
	if (!ifs) {
		cerr << "loading file fails" << endl;
	}
	Sales_data s1;
	vector<Sales_data> vec;
	while (read(ifs, s1)) {
		vec.push_back(s1);
	}
	sort(vec.begin(), vec.end(), compare);
	for (Sales_data s : vec) {
		print(cout, s) << endl; // 返回ostream的好处:可以后面直接跟<<
	}
	return 0;
}

5、标准库 定义了名为 partition 的算法,它接受 一个谓词,对容器内容 进行划分,使得 谓词为true 的值 会排在容器的前半部分,而 使得谓词为 false 的值 会排在后半部分。算法 返回一个迭代器,指向 最后一个使谓词为 true 的元素之后的位置

bool noGreater5(string s)
{
	if (s.size() > 5)
		return false;
	else
		return true;
}

int main()
{
	vector<string> vec = { "abdh", "nfvkj", "udjfvksb" };
	vector<string>::iterator it = partition(vec.begin(), vec.end(), noGreater5);
	vec.erase(it);
	for (string s : vec) {
		cout << s << " ";
	}
	return 0;
}

3.2 lambda表达式

1、根据算法 接受一元谓词 还是 二元谓词,传递给算法的谓词 必须严格接受 一个或两个参数。但是,有时 希望进行的操作需要更多参数,超出了 算法对谓词的限制

如:求大于等于一个给定长度的单词 有多少,程序只打印大于等于给定长度的单词

void biggies(vector<string> &words, vector<string>::size_type sz)
{
	elimDups(words); // 将words按字典序排序,删除重复单词
	// 按长度排序,长度相同的单词 维持字典序
	stable_sort(words.begin(), words.end(), isShorter);
	// 获取一个选代器,指向 第一个满足 size() >= sz 的元素
	// 计算 满足 size >= sz 的元素的数目
	// 打印长度大于等于 给定值的单词,每个单词 后面接一个空格
}

在vector中 寻找第一个大于等于 给定长度的元素。一旦 找到了这个元素,根据其位置,就可以计算出 有多少元素的长度大于等于给定值
可以 使用标准库find_if算法 来查找第一个具有特定大小的元素。find_if算法 接受一对迭代器,表示一个范围。但与find不同的是,find_if的 第三个参数是 一个谓词。find_if算法 对输入序列中的每个元素调用 给定的这个谓词。它返回 第一个 使谓词返回非0值的元素,如果 不存在这样的元素 则返回尾迭代器

但是find_if接受一元谓词,没有任何办法 能传递给它 第二个参数来表示长度

2、lambda:可以 向一个算法 传递任何类别的可调用对象

对于一个对象 或 一个表达式,如果 可以对其使用调用运算符,则称它为 可调用的。即,如果 e是一个可调用的表达式,则 可以编写代码e(args),其中args是 一个逗号分隔的一个或多个参数的列表

使用过的 仅有的两种可调用对象是 函数 和 函数指针
1)可调用对象是 函数:“调用运算符” 通常指的是 函数调用运算符 ()。在C++中,可以使用()运算符来调用函数或函数对象。例如:
add(3, 4) 使用了函数调用运算符来 调用add函数,将参数3和4 传递给它,并将 返回值存储在result变量中

也可以重载函数调用运算符(),使得用户自定义类型的对象可以像函数一样被调用。例如:

#include <iostream>

class MyFunctor {
public:
    int operator()(int a, int b) {
        return a * b;
    }
};

int main() {
    MyFunctor functor;
    int result = functor(3, 4); // 使用函数调用运算符来调用对象
    std::cout << "Result: " << result << std::endl;

    return 0;
}

MyFunctor类 重载了 函数调用运算符(),使得 其对象能够像函数一样被调用

2)可调用对象是 函数指针:当函数指针作为可调用对象时,可以直接使用该指针来调用相应的函数

#include <iostream>

// 定义一个函数
int add(int a, int b) {
    return a + b;
}

int main() {
    // 声明一个函数指针,指向 add 函数
    int (*funcPtr)(int, int) = &add;

    // 使用函数指针来调用相应的函数
    int result = funcPtr(3, 4);
    std::cout << "Result: " << result << std::endl;

    return 0;
}

funcPtr是一个 指向 add 函数的函数指针。通过 将函数指针用作可调用对象,并传递 参数3和4,调用了add函数,并将 结果打印出来
需要注意的是,函数指针的类型 必须与相应的函数的类型完全匹配,包括参数列表和返回类型

还有其他两种可调用对象:重载了函数调用运算符的类, 以及 Iambda表达式
一个lambda表达式表示一个 可调用的代码单元。可以将其理解为 一个未命名的内联函数。一个lambda表达式 具有如下形式

[capture list](parameter list) -> return type { function body }

capture list(捕获列表)是 一个lambda所在函数中(就是包含lambda的那个函数) 定义的局部变量的列表(通常为空);return type、parameter list 和 function body 与任何普通函数一样,分别表示返回类型、参数列表 和 函数体

与普通函数不同,lambda必须使用 尾置返回 来指定返回类型
可以忽略 参数列表 和 返回类型,但必须 永远 包含捕获列表和函数体

auto f = [] { return 42; };

定义了 一个可调用对象f,它不接受参数,返回42

lambda的调用方式 与普通函数的调用方式相同,都是 使用调用运算符:

cout << f() << endl; // 打印42

在lambda中 忽略括号和参数列表 等价于指定一个空参数列表。在此例中,当调用时,参数列表是空的

如果忽略返回类型,lambda根据函数体中的代码推断出返回类型
如果函数体 有且只有 一个return语句,则 返回类型从返回的表达式的类型推断而来。否则,返回类型为void

3、向lambda传递参数
与普通函数不同,lambda不能有 默认参数。因此,一个lambda调用的实参数目 永远与形参数目相等

编写一个与isShorter函数完成相同功能的lambda

[](const string &a,const string &b)
	{ return a.size() < b.size(); }

空捕获列表 表明此lambda不使用它所在函数中的 任何局部变量。可以使用 此lambda来调用stable_sort

// 按长度排序,长度相同的单词维持字典序
stable_sort(words.begin(), words.end(),
	[] (const string &a, const string &b)
	{ return a.size() < b.size(); } );

4、使用捕获列表:捕获列表 指引lambda在其内部 包含 访问 包含其的函数内的局部变量 所需的信息

[sz] (const string &a)
	{ return a.size() >= sz; };

lambda以一对[]开始,可以在其中 提供一个 以逗号分隔的名字列表,这些名字都是 它所在函数中定义的
由于此lambda 捕获sz,因此lambda的函数体 可以使用sz
一个lambda 只有在其捕获列表中 捕获一个它所在函数中的局部变量,才能 在函数体中使用该变量

5、调用find_if:可以查找 第一个长度大于等于sz 的元素

//获取一个迭代器,指向 第一个满足size() >= sz的元素
auto wc = find_if(words.begin(), words.end(),
	[sz](const string &a)
	{ return a.size() >= sz; }); 

对find_if的调用 返回一个迭代器,指向 第一个长度不小于 给定参数sz的元素。如果这样的元素 不存在,则返回words.end() 的一个拷贝
使用find_if返回的迭代器 来计算 从它开始到words的末尾一共有多少个元素:auto count = words.end() - wc;

6、for_each算法:使用for_each算法。此算法 接受一个可调用对象,并对 输入序列中每个元素 调用此对象

// 打印长度大于等于 给定值的单词,每个单词后面接一个空格
for_each(wc, words.end(),
	[](const string &s){ cout << s << " "; });

此lambda中的捕获列表为空,但其函数体中 还是使用了两个名字:s和cout,前者是 它自己的参数
只 对lambda所在函数中定义的(非static)变量 使用捕获列表。一个lambda可以直接 使用定义在当前函数之外的名字。在本例中,cout不是定义在biggies中的局部名字,而是定义在 头文件iostream中。因此,只要在biggies出现的作用域中 包含了头文件io stream,lambda就可以 使用cout

捕获列表 只用于局部非static变量,lambda可以 直接使用局部static变量 和 在它所在函数之外声明的名字

7、编写一个 lambda ,接受两个int,返回它们的和

#include <iostream>

using namespace std;

int main()
{
	auto add = [](int a, int b) -> int { return a + b; }; // lambda返回值不是int,最后别忘了分号
	cout << add(1, 2) << endl; // 调用
}

编写一个 lambda ,捕获它所在函数的 int,并接受一个 int参数。lambda 应该返回捕获的 int 和 int 参数的和

#include <iostream>

using namespace std;

auto sum(int a) {
	// return [a](int b) {return a + b; }; // 不能直接return
	auto res = [a](int b) {return a + b; };
	auto r = res(2); // 还是不能传给int
	return r; // 不能返回int
}

int main()
{
	cout << sum(3) << endl;
	return 0;
}

使用 lambda 编写你自己版本的 biggies

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>

using namespace std;

void elimDups(vector<string>& vec) {
	sort(vec.begin(), vec.end());
	auto unip = unique(vec.begin(), vec.end());
	vec.erase(unip, vec.end()); // 不加就只删一个
}

string make_pl(const string& s, int count, const string& ending) { // 不能用字面值常量传递给string &,要const
	return count > 1 ? s + ending : s;
}

void biggest(vector<string>& words, vector<string>::size_type sz) {
	elimDups(words);
	
	for_each(words.begin(), words.end(), [](const string& s) { cout << s << " "; }); // 用法
	
	cout << endl;
	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; }); 
	auto wc = partition(words.begin(), words.end(), [sz](const string& a) { return a.size() < sz; });
	// 跟find_if不一样。满足条件的在前半部分
	
	int count = words.end() - wc;
	cout << make_pl("word", count, "s") + ": " << endl;
	for (auto it = wc; it != words.end(); it++) {
		cout << *it << " ";
	}
}

int main()
{
	vector<string> words = {"aaa", "dsvdfb", "sgrff", "sdg", "dsgv", "aaa", "sgrff"};
	biggest(words, 5);
	return 0;
}

unique的用法

#include <iostream>
#include <algorithm>
#include <vector>

int main() {
    std::vector<int> vec = { 1, 1, 2, 2, 3, 3, 4, 5, 5, 6 };
    auto last = std::unique(vec.begin(), vec.end()); 
    // 执行完成之后,vec数组为{1,2,3,4,5,6,4,5,5,6} 4,5,5,6在原位置上还有

    vec.erase(last, vec.end());
    for (const auto& elem : vec) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;
    return 0;
}

3.3 lambda 捕获和返回

1、定义一个lambda时,编译器 生成一个与lambda对应的新的(未命名的)类类型

默认情况下,从lambda生成的类 都包含 一个对应该lambda所捕获的变量的数据成员。类似 任何普通类的数据成员,lambda的数据成员 也在lambda对象创建时被初始化

2、值捕获:类似 参数传递,变量的捕获方式 也可以是值或引用。与传值参数类似,采用 值捕获的前提是 变量可以拷贝。与参数不同,被捕获的变量的值 是在lambda 创建时 拷贝,而不是 调用时拷贝

void fcn1()
{
	size_t v1 = 42; // 局部变量
	// 将v1拷贝到 名为f的可调用对象
	auto f = [v1] { return v1; };
	v1 = 0;
	auto j = f(); // j为42; f保存了 我们创建它时v1的拷贝
}

被捕获变量的值 是在lambda创建时 拷贝,因此 随后对其修改 不会影响到lambda内对应的值

3、引用捕获:定义lambda时 可以采用 引用方式捕获变量

void fcn2()
{
	size_t v1 = 42; // 局部变量
	// 对象f2包含v1的引用
	auto f2 = [&v1]{ return v1; }; 
	v1 = 0; 
	auto j = f2(); // j为0;f2保存v1的引用,而非拷贝
}

在lambda函数体内 使用此变量时,实际上使用的是 引用所绑定的对象。当lambda返回v1时,它返回的是 v1指向的对象的值

引用捕获 与返回引用 有着相同的问题和限制。如果 采用引用方式 捕获一个变量,就必须确保 被引用的对象在lambda执行的时候是存在的
lambda捕获的都是 局部变量,这些变量 在函数结束后就不复存在了。如果lambda可能在 函数结束后执行,捕获的引用 指向的局部变量已经消失
例:可能希望biggies函数接受一个ostream的引用,用来 输出数据,并接受一个字符作为分隔符

void biggies(vector<string> &words,
				vector<string>::size_type sz,
				ostream &os = cout, char c = ' ')
{
	//与之前例子一样的重排words的代码
	//打印count的语句改为打印到os
	for_each(words.begin(), words.end(), 
			[&os, c](const string &s) { os << s << c; ));

不能拷贝ostream对象,捕获os的唯一方法就是 捕获其引用(或指向os的指针)

也可以 从一个函数返回lambda。函数可以直接返回 一个可调用对象,或者返回 一个类对象,如果函数 返回一个lambda,则与 函数不能返回一个局部变量的引用 类似,此lambda也不能 包含引用捕获。因为 以引用方式捕获一个变量时,必须保证在lambda执行时变量是存在的

4、一个lambda捕获 从lambda被创建(即,定义lambda的代码执行时)到lambda自身执行(可能有多次执行) 这段时间内保存的相关信息。必须确保在lambda执行时,绑定到 迭代器、指针或引用的对象 仍然存在。应该 尽量减少捕获的数据量,避免捕获指针或引用

5、隐式捕获:让编译器 根据lambda体中的代码 来推断 要使用哪些变量。为了 指示编译器推断捕获列表,应 在捕获列表中
写一个&或=。&告诉编译器采用捕获引用方式,=则表示采用值捕获方式
例如,我们可以重写 传递给find_if的lambda

//sz为 隐式捕获,值捕获方式
wc = find_if(words.begin(), words.end(),
			[=](const string&s)
			{ returns.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; });
}

混合使用隐式捕获和显式捕获时,捕获列表中的 第一个 元素必须是一个&或=。此符号 指定了默认捕获方式为 引用或值
当混合使用 隐式捕获和显式捕获时,显式捕获的变量 必须使用 与隐式捕获不同的方式

6、lambda 捕获列表

符号解释
[ ]空捕获列表。lambda不能使用所在函数中的变量。一个lambda 只有捕获变量后 才能使用它们
[names]names是 一个逗号分隔的名字列表,这些名字 都是lambda所在函数的局部变量。默认情况下,捕获列表中的变量 都被拷贝。名字前如果使用了&,则采用 引用捕获方式
[&]隐式捕获列表,采用 引用捕获方式。lambda体中所使用的来自所在函数的实体 都采用 引用方式使用
[=]隐式捕获列表,采用 值捕获方式。lambda体 将拷贝 所使用的来自所在函数的实体的值
[&, identifier_list]identifier_list是一个逗号分隔的列表,包含0个或多个来自所在函数的变量。这些变量 采用值捕获方式,而 任何隐式捕获的变量 都采用引用方式捕获。identifier_list中的名字 前面不能使用&
[=, identifier_list]identifier_list中的变量 都采用引用方式捕获,而任何隐式捕获的变量 都采用值方式捕获。identifier_list中的名字 不能包括this,且这些名字之前必须使用 &

7、可变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类型

void fcn4()
{
	size_t v1 = 42; // 局部变量
	// v1是一个 非const变量的引用
	// 可以通过f2中的引用 来改变它
	auto f2 = [&v1] { return ++vl; };
	v1 = 0;
	auto j = f2(); // j为1
}

8、指定lambda返回类型:默认情况下,如果 一个lambda体 包含return之外的任何语句,则 编译器 假定此lambda返回void

可以 使用标准库transform算法 和 一个lambda 来将一个序列中的每个负数 替换为其绝对值

transform(vi.begin(), vi.end(), vi.begin(), 
		[](int i) { return i < 0 ? -i : i; });

前两个迭代器 表示输入序列,第三个迭代器 表示目的位置。算法对输入序列中 每个元素 调用可调用对象,并将结果写到目的
位置。当输入迭代器 和 目的迭代器 相同时,transform 将输入序列中 每个元素 替换为 可调用对象操作该元素得到的结果

lambda体 是 单一的return语句,返回 一个条件表达式的结果。无须指定 返回类型,因为可以根据条件运算符的类型 推断出来
如果我们将程序改写为看起来是等价的if语句,就会产生编译错误

// 错误:不能推断lambda的返回类型
transform(vi.begin(), vi.end(), vi.begin(), 
		[](int i) {if (i < 0) return -i; else return i; });

编译器 推断这个版本的lambda 返回类型为void,但它返回了一个int值
需要为一个lambda定义返回类型时,必须使用尾置返回类型

transform(vi.begin(), vi.end(), vi.begin(), 
		[](int i) -> int
		{ if (i < 0) return -i; else return i; });

9、编写一个 lambda,捕获一个局部 int 变量,并递减变量值,直至它变为0。一旦变量变为0,再调用lambda应该不再递减变量。lambda应该返回一个bool值,指出捕获的变量是否为0

#include <iostream>
#include <algorithm>

using namespace std;

int main()
{
	int i = 5;
	auto f = [&i]()->bool
	{
		if (i == 0)
			return false;
		else {
			i--;
			return true;
		}
	};
	while (f()) {
		cout << i << endl;
	}
	return 0;
}

3.4 参数绑定

1、只在 一两个地方 使用的简单操作,lambda表达式 是最有用的。如果 需要在很多地方 使用相同的操作,通常应该 定义一个函数,而不是 多次编写相同的lambda表达式。类似的,如果 一个操作需要很多语句才能完成,通常使用函数更好

如果lambda的捕获列表为空,通常可以用函数来代替它

bool check_size(const string &s, string::size_type sz) {
	return s.size() >= sz;
}

不能用这个函数作为find if的一个参数。find_if接受一个 一元谓词,因此传递 给find_if的可调用对象 必须接受单一参数。biggies传递给 find_if的lambda使用捕获列表 来保存sz。为了用check_size来代替 此lambda,必须 解决 如何向sz形参传递一个参数的问题

2、标准库bind函数:可以解决向check_size传递一个长度参数的问题,它定义在 头文件functional中。可以 将bind函数看作一个通用的 函数适配器,它接受 一个可调用对象,生成 一个新的可调用对象 来“适应”原对象的参数列表
调用bind的一般形式为:

auto newCallable = bind(callable, arg_list);

newCallable 本身是 一个可调用对象,arg_list是 一个逗号分隔的参数列表,对应给定的 callable的参数。即,当我们调用new Callable时,newCallable 会调用 callable,并传递给它arg list中的参数
arg_list 中的参数可能包含 形如n的名字,其中n是一个整数。这些参数是“占位符”,表示newCallable的参数,它们占据了 传递给newCallable的参数的“位置”。数值n表示 生成的可调用对象中 参数的位置:_1 为newCallable的第一个参数,_2 为第二个参数,依此类推

3、绑定check_size的 sz参数:将使用bind 生成一个调用check_size的对象

// check6 是一个可调用对象,接受一个string类型的参数
// 并用此string 和 值6 来调用check_size
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 b1 = check6(s); // check6(s) 会调用 check_size(s, 6)

find_if调用的 check_size的版本

auto wc = find_if(words.begin(), words.end(),
				bind(check_size, _1, sz));

此bind调用 生成一个可调用对象,将check_size的第二个参数 绑定到sz的值。当find_if 对words中的string 调用这个对象时,这些对象会 调用check_size,将给定的string 和 sz 传递给它

4、使用placeholders名字:名字 _n 都定义在 一个名为placeholders的命名空间中,而这个命名空间本身定义在 std命名空间中。为了使用这些名字,两个命名空间 都要写上。与 其他例子类似,对bind的调用代码 假定之前已经恰当地使用了using声
明。例如,_1对应的using声明为:

using std::placeholders::_1;

说明 要使用的名字 _1 定义在命名空间 placeholders中,而此命名空间又定义在 命名空间std中。对 每个占位符名字,都必须提供 一个单独的using声明。可以使用 另外一种不同形式的using语句,而不是分别声明每个占位符:using namespace namespace_name;
希望 所有来自namespace_name的名字 都可以在我们的程序中直接使用

using namespace std::placeholders;

使得由 placeholders定义的所有名字 都可用。与 bind函数一样,placeholders命名空间 也定义在 functional头文件中

5、bind的参数:如前文所述,我们可以用bind修正参数的值。更一般的,可以 用bind绑定给定可调用对象中的参数 或 重新安排其顺序

// g是一个 有两个参数的可调用对象
auto g = bind(f, a, b, _2, c, _1);

这个新的可调用对象 将它自己的参数 作为第三个 和 第五个参数 传递给f。f的 第一个、第二个和第四个参数 分别被绑定到 给定的值a、b和c上
即,对 g的调用会调用f,用g的参数 代替占位符,再加上 绑定的参数a、b和c。例如,调用g(X, Y) 会调用 f(a, b, Y, c, X)

6、用bind重排参数顺序:可以 用bind颠倒 isShorter 的含义:

// 按单词长度 由短至长排序
sort(words.begin(), words.end(), isShorter);
// 按单词长度 由长至短排序
sort(words.begin(), words.end(), bind(isShorter, _2, _1));

7、绑定引用参数:bind的 那些不是占位符的参数 被拷贝到bind返回的 可调用对象中。但是,与lambda类似,有时 对有些绑定的参数 我们希望以引用方式传递,或是 要绑定参数的类型 无法拷贝

例如,为了 替换一个引用方式 捕获ostream的lambda:

// os是一个局部变量,引用一个输出流
// c是一个局部变量,类型为char
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的捕获

// 错误:不能拷贝os
for_each(words.begin(), words.end(), bind(print, os, _1, ' '));

原因在于 bind拷贝其参数,而我们 不能拷贝一个ostream。如果我们 希望传递给bind一个对象 而又不拷贝它,就必须使用标准库 ref函数

for_each(words.begin(), words.end(),
		bind(print, ref(os), _1, ' '));

函数ref 返回一个对象,包含 给定的引用,此对象是 可以拷贝的。标准库中 还有一个cref函数,生成一个保存const引用的类。与bind一样,函数 ref 和 cref 也定义在头文件 functional中

8、重写统计长度小于等于6的单词数量的程序,使用函数代替 lambda

#include <functional>
#include <vector>
#include <string>
#include <iostream>
#include <algorithm>

using namespace std;

bool check(const string& s, int n) {
	return s.size() >= n;
}

int main()
{
	vector<string> vec = {"ufkbnbk", "dnksvkfn", "nv"};
	cout << count_if(vec.begin(), vec.end(), bind(check, placeholders::_1, 6)) << endl;
	return 0;
}

9、bind 接受的参数个数:假设 要绑定的函数有n个参数,绑定取n + 1个参数。另外一个是 函数本身的绑定

10、给定一个string,使用 bind 和 check_size 在一个 int 的vector 中查找第一个大于string长度的值

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <functional>

using namespace std;

bool check_size(string& s, int sz)
{
	return s.size() < sz;
}

int main()
{
	vector<int> vi = { 1,2,3,4,5,6 };
	string s("aaaa");

	auto iter = find_if(vi.begin(), vi.end(), bind(check_size, s, placeholders::_1));

	cout << *iter << endl;

	return 0;
}

在3.2 7第二个程序 中,编写了一个使用partition 的biggies版本。使用 check_size 和 bind 重写此函数

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <functional>

using namespace std;

void elimDups(vector<string>& vec) {
	sort(vec.begin(), vec.end());
	auto unip = unique(vec.begin(), vec.end());
	vec.erase(unip, vec.end()); 
}

string make_pl(const string& s, int count, const string& ending) {
	return count > 1 ? s + ending : s;
}

void printS(const string& s) {
	cout << s << " ";
}

bool isShorter(const string& s, int n) {
	return s.size() < n;
}

void biggest(vector<string>& words, vector<string>::size_type sz) {
	elimDups(words);
	for_each(words.begin(), words.end(), printS); // 不是所有谓词函数都需要返回值(bool)
	cout << endl;
	stable_sort(words.begin(), words.end(), [](const string& a, const string& b) { return a.size() < b.size(); });
	auto wc = partition(words.begin(), words.end(), bind(isShorter, placeholders::_1, sz));
	int count = words.end() - wc;
	cout << make_pl("word", count, "s") + ": " << endl;
	for (auto it = wc; it != words.end(); it++) {
		cout << *it << " ";
	}
}

int main()
{
	vector<string> words = {"aaa", "dsvdfb", "sgrff", "sdg", "dsgv", "aaa", "sgrff"};
	biggest(words, 5);
	return 0;
}

4、再探迭代器

为每个容器定义的迭代器 之外,标准库 在头文件iterator中还定义了 额外几种迭代器
1)插入迭代器:这些迭代器 被绑定到一个容器上,可用来向容器插入元素
2)流迭代器:这些迭代器 被绑定到输入或输出流上,可用来遍历 所关联的IO流
3)反向迭代器:这些迭代器 向后而不是向前移动。除了 forward_list之外的 标准库容器 都有反向迭代器
4)移动选代器:这些专用的迭代器 不是拷贝其中的元素,而是移动它们

4.1 插入迭代器

1、插入器是一种 迭代器适配器,它接受一个容器,生成 一个迭代器,能实现 向给定容器添加元素。当我们 通过一个插入迭代器进行赋值时,该迭代器 调用容器操作 来向给定容器的指定位置 插入一个元素

2、插入迭代器操作

代码解释
it = t在it指定的 当前位置 插入值t。假定c是 it绑定的容器,依赖于 插入迭代器的不同种类,此赋值会 分别调用 c.push_back(t)、c.push_front(t) 或c.insert(t, p),其中 p为传递给inserter的 迭代器位置
*it, ++it, it++这些操作存在,但不对it做任何事情。每个操作 都返回it

3、插入器有三种类型,差异在于元素插入的位置
1)back_inserter 创建一个 使用push_back的迭代器
2)front_inserter 创建一个 使用push_front的迭代器
3)inserter 创建一个 使用insert的迭代器。此函数接受 第二个参数,这个参数必须是 一个指向给定容器的迭代器。元素将被插入到 给定迭代器 所表示的元素 之前

只有 在容器支持push_front的情况下,我们才可以使用 front_inserter

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

当调用 front_inserter© 时,我们 得到一个 插入迭代器,接下来 会调用push_front。当每个元素 被插入到容器c中时,它变为 c 的 新的首元素。因此,front_inserter 生成的迭代器 会将插入的元素序列的顺序 颠倒过来,而 inserter 和 back_inserter 则不会

4、除了 unique 之外,标准库还定义了名为 unique_copy 的函数,它接受第三个迭代器,表示拷贝不重复元素的目的位置
使用 unique_copy 将一个vector中不重复的元素 拷贝到 一个初始化为空的list中(可以 不同容器类型,泛型)

#include <iostream>
#include <vector>
#include <list>
#include <algorithm>

using namespace std;

int main()
{
	vector<int> vec = { 1,2,2,3,3,5 };
	list<int> li;
	// unique_copy(vec.begin(), vec.end(), li.begin()); // 报错,list为空呢
	unique_copy(vec.begin(), vec.end(), inserter(li, li.begin()));
	for (int i : li) {
		cout << i << " ";
	}
	return 0;
}

一个vector 中保存 1 到 9,将其拷贝到三个其他容器中。分别使用inserter、back_inserter 和 front_inserter 将元素添加到三个容器中

#include <iostream>
#include <vector>
#include <list>
#include <algorithm>

using namespace std;

int main()
{
	vector<int> v1 = { 1,2,3,4,5,6,7,8,9 };
	list<int> l2, l3;
	vector<int> l1;

	copy(v1.begin(), v1.end(), back_inserter(l1)); // 用法
	for (const auto i : l1)
		cout << i << " ";
	cout << endl;

	copy(v1.begin(), v1.end(), front_inserter(l2)); // 用法
	for (const auto i : l2)
		cout << i << " ";
	cout << endl;

	copy(v1.begin(), v1.end(), inserter(l3, l3.begin())); // 用法
	for (const auto i : l3)
		cout << i << " ";
	cout << endl;

	return 0;
}

4.2 iostream迭代器

1、虽然 iostream类型 不是容器,但 标准库定义了 可以用于这些IO类型对象的迭代器。istream_iterator 读取输入流,ostream _iterator 向一个输出流 写数据。这些迭代器 将它们对应的流 当作一个特定类型的元素序列 来处理。通过 使用流迭代器,我们可以 用泛型算法 从流对象读取数据 以及 向其写入数据

2、istream_iterator 操作:当 创建一个流迭代器时,必须 指定迭代器 将要读写的对象类型。一个istream_iterator 使用 >> 来读取流。因此,istream_iterator 要读取的类型 必须定义了 输入运算符。当 创建一个 istream_iterator 时,我们 可以将它绑定到一个流
当然,我们还可以 默认初始化迭代器,这样就 创建了一个可以当作尾后值使用的 迭代器

// 从cin读取int
istream_iterator<int> int_it(cin);
//尾后迭代器
istream_iterator<int> int_eof;

ifstream in("afile");
istream_iterator<string> str_it(in); // 从"a file"读取字符串

用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++);

对于一个 绑定到流的迭代器,一旦 其关联的流 遇到文件尾 或 遇到IO错误,迭代器的值 就与尾后迭代器相等

后置递增运算 会从流中读取下一个值,向前推进,但 返回的是 迭代器的旧值。选代器的旧值 包含了从流中读取的前一个值,对迭代器进行 解引用就能获得此值

将程序 重写为如下形式,这体现了istream_iterator 更有用的地方

istream_iterator<int> in_iter(cin), eof; // 从cin读取int
vector<int> vec(in_iter, eof); 			 // 从选代器范围构造vec

用一对 表示元素范围的迭代器 来构造vec。这两个迭代器是istream_iterator,这意味着 元素范围是 通过从关联的流中读取数据获得的。这个构造函数 从cin中读取数据,直至 遇到文件尾 或者 遇到一个不是int的数据 为止

istream_iterator 操作

代码解释
istream_iterator<T> in(is);in从输入流is 读取类型为T的值
istream_iterator<T> end; 读取类型为T的值的istream_iterator迭代器,表示尾后位置
in1 == in2 in1 != in2in1 和 in2 必须 读取相同类型。如果它们 都是尾后迭代器,或 绑定到 相同的输入,则两者相等
*in返回从流中读取的值
in->mem与(*in).mem 的含义相同
++in, in++使用元素类型所定义的 >> 运算符 从输入流中 读取下一个值。与以往一样,前置版本 返回一个指向 递增后迭代器的引用,后置版本 返回旧值

3、使用 算法操作流迭代器:可以 用一对istream_iterator 来调用 accumulate

istream_iterator<int> in(cin), eof;
cout << accumulate(in, eof, 0) << endl;

此调用会计算出从标准输入读取的值的和。如果输入为
23 109 45 89 6 34 12 90 34 23 56 23 8 89 23
则输出为664

4、istream_iterator 允许使用 懒惰求值:当我们 将一个istream_iterator 绑定到一个流时,标准库 并不保证 迭代器立即从
流读取数据。具体实现 可以推迟从流中读取数据,直到 我们使用迭代器时 才真正读取。标准库中的实现 所保证的是,在我们第一次 解引用迭代器之前,从流中读取数据的操作 已经完成了
如果我们创建了一个 istream_iterator,没有使用 就销毁了,或者 我们正在从两个不同的对象同步读取 同一个流,那么何时读取可能就很重要了

5、ostream_iterator操作:可以 对任何具有输出运算符(<<运算符)的类型 定义ostream_iterator。当创建一个ostream_iterator时,我们可以提供(可选的)第二参数,它是一个字符串,在输出每个元素后都会打印此字符串。此字符串必须是一个C风格字符串(即,一个字符串字面常量 或者 一个指向以空字符结尾的字符数组的指针)。必须将ostream_iterator绑定到 一个指定的流,不允许空的 或 表示尾后位置的ostream_iterator(与 istream_iterator区别)

ostream_iterator操作

代码解释
ostream_iterator<T> out(os)out将类型为T的值写到 输出流os中
ostream_iterator<T> out(os, d)out将类型为T的值写到 输出流os中,每个值后面 都输出一个d。d指向一个空字符结尾的字符数组
out = val用<<运算符 将val写入到out所绑定的 ostream中。val的类型 必须与out可写的类型兼容(赋值写出)
*out, ++out, out++这些运算符 是存在的,但不对 out做任何事情。每个运算符 都返回out

可以用 ostream_iterator 来输出 值的序列:

ostream_iterator<int> out_iter(cout, " ");
for (auto e :vec)
	*out_iter++ = e; // 赋值语句实际上将元素写到 cout
cout << endl;

此程序 将vec中的每个元素 写到cout,每个元素后 加一个空格。每次向 out_iter 赋值时,写操作 就会被提交

当我们向 out_iter 赋值时,可以 忽略 解引用 和 递增运算。即,循环 可以 重写成下面的样子:

for (auto e :vec)
	out_iter = e; // 赋值语句 将元素写到cout
cout << endl;

运算符* 和 ++ 实际上对 ostream_iterator 对象 不做任何事情,因此 忽略它们 对我们的程序没有任何影响。但是,推荐第一种形式。在这种写法中,流迭代器的使用 与 其他迭代器的使用 保持一致。如果想 将此循环改为 操作其他迭代器类型,修改起来非常容易。而且,对于读者来说,此循环的行为也更为清晰

可以通过调用copy来打印vec中的元素,这比编写循环更为简单:

copy(vec.begin(), vec.end(), out_iter);
cout << endl;

6、使用流迭代器 处理类类型:可以 为任何定义了 输入运算符(>>)的类型 创建istream_iterator对象。类似的,只要 类型有输出运算符(<<),我们 就可以 为其定义ostream_iterator。由于Sales_item 既有输入运算符 也有输出运算符,因此 可以使用IO迭代器 重写1.6节 中的书店程序:

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;

7、使用流迭代器读取一个文本文件,存入一个vector中的string里

#include <fstream>
#include <iostream>
#include <iterator>
#include <vector>
#include <string>
#include <algorithm>

using namespace std;

int main()
{
	ifstream ifs("10_29data.txt");
	if (!ifs) cout << "loading data fails." << endl;
	istream_iterator<string> iis(ifs), eof;
	vector<string> vec(iis, eof); // 注意用法
	ostream_iterator<string> ois(cout, " ");
	copy(vec.begin(), vec.end(), ois);
	cout << endl;
	return 0;
}

8、重写1.6节中的书店程序,使用一个vector保存交易记录,使用不同算法完成处理。使用 sort 和10.3.1节中的 compareIsbn 函数来排序交易记录,然后使用 find 和 accumulate 求和

Sales_item.h
需要对原来的文件 加上小于运算符定义的重载

/*
 * This file contains code from "C++ Primer, Fifth Edition", by Stanley B.
 * Lippman, Josee Lajoie, and Barbara E. Moo, and is covered under the
 * copyright and warranty notices given in that book:
 *
 * "Copyright (c) 2013 by Objectwrite, Inc., Josee Lajoie, and Barbara E. Moo."
 *
 *
 * "The authors and publisher have taken care in the preparation of this book,
 * but make no expressed or implied warranty of any kind and assume no
 * responsibility for errors or omissions. No liability is assumed for
 * incidental or consequential damages in connection with or arising out of the
 * use of the information or programs contained herein."
 *
 * Permission is granted for this code to be used for educational purposes in
 * association with the book, given proper citation if and when posted or
 * reproduced.Any commercial use of this code requires the explicit written
 * permission of the publisher, Addison-Wesley Professional, a division of
 * Pearson Education, Inc. Send your request for permission, stating clearly
 * what code you would like to use, and in what specific way, to the following
 * address:
 *
 *     Pearson Education, Inc.
 *     Rights and Permissions Department
 *     One Lake Street
 *     Upper Saddle River, NJ  07458
 *     Fax: (201) 236-3290
*/

/* This file defines the Sales_item class used in chapter 1.
 * The code used in this file will be explained in
 * Chapter 7 (Classes) and Chapter 14 (Overloaded Operators)
 * Readers shouldn't try to understand the code in this file
 * until they have read those chapters.
*/

#ifndef SALESITEM_H
// we're here only if SALESITEM_H has not yet been defined 
#define SALESITEM_H

// Definition of Sales_item class and related functions goes here
#include <iostream>
#include <string>

class Sales_item {
    // these declarations are explained section 7.2.1, p. 270 
    // and in chapter 14, pages 557, 558, 561
    friend std::istream& operator>>(std::istream&, Sales_item&);
    friend std::ostream& operator<<(std::ostream&, const Sales_item&);
    friend bool operator<(const Sales_item&, const Sales_item&);
    friend bool
        operator==(const Sales_item&, const Sales_item&);
public:
    // constructors are explained in section 7.1.4, pages 262 - 265
    // default constructor needed to initialize members of built-in type
    Sales_item() : units_sold(0), revenue(0.0) { }
    Sales_item(const std::string& book) :
        bookNo(book), units_sold(0), revenue(0.0) { }
    Sales_item(std::istream& is) { is >> *this; }
public:
    // operations on Sales_item objects
    // member binary operator: left-hand operand bound to implicit this pointer
    Sales_item& operator+=(const Sales_item&);

    // operations on Sales_item objects
    std::string isbn() const { return bookNo; }
    double avg_price() const;
    // private members as before
private:
    std::string bookNo;      // implicitly initialized to the empty string
    unsigned units_sold;
    double revenue;
};

// used in chapter 10
inline
bool compareIsbn(const Sales_item& lhs, const Sales_item& rhs)
{
    return lhs.isbn() < rhs.isbn();
}

// nonmember binary operator: must declare a parameter for each operand
Sales_item operator+(const Sales_item&, const Sales_item&);

inline bool
operator==(const Sales_item& lhs, const Sales_item& rhs)
{
    // must be made a friend of Sales_item
    return lhs.units_sold == rhs.units_sold &&
        lhs.revenue == rhs.revenue &&
        lhs.isbn() == rhs.isbn();
}

inline bool
operator!=(const Sales_item& lhs, const Sales_item& rhs)
{
    return !(lhs == rhs); // != defined in terms of operator==
}

inline bool
operator<(const Sales_item & lhs, const Sales_item & rhs) // 需要加上小于运算符定义的重载
{
    return lhs.isbn() < rhs.isbn(); 
}

// assumes that both objects refer to the same ISBN
Sales_item& Sales_item::operator+=(const Sales_item& rhs)
{
    units_sold += rhs.units_sold;
    revenue += rhs.revenue;
    return *this;
}

// assumes that both objects refer to the same ISBN
Sales_item
operator+(const Sales_item& lhs, const Sales_item& rhs)
{
    Sales_item ret(lhs);  // copy (|lhs|) into a local object that we'll return
    ret += rhs;           // add in the contents of (|rhs|) 
    return ret;           // return (|ret|) by value
}

std::istream&
operator>>(std::istream& in, Sales_item& s)
{
    double price;
    in >> s.bookNo >> s.units_sold >> price;
    // check that the inputs succeeded
    if (in)
        s.revenue = s.units_sold * price;
    else
        s = Sales_item();  // input failed: reset object to default state
    return in;
}

std::ostream&
operator<<(std::ostream& out, const Sales_item& s)
{
    out << s.isbn() << " " << s.units_sold << " "
        << s.revenue << " " << s.avg_price();
    return out;
}

double Sales_item::avg_price() const
{
    if (units_sold)
        return revenue / units_sold;
    else
        return 0;
}
#endif

10.32.cpp
end始终是 当前段的尾部,beg是 当前段的开头

#include <iostream>
#include <iterator>
#include <fstream>
#include <vector>
#include <algorithm>
#include "Sales_item.h"
 // Sales_item.h对运算符进行了重载,所以可以直接当成一个类型,cin,find_if,accumulate和内置类型一样用
#include <numeric>

using namespace std;

int main()
{
	istream_iterator<Sales_item> iis(cin), eof;
	vector<Sales_item> vec(iis, eof); // 直到iis也变成eof为止
	sort(vec.begin(), vec.end(), compareIsbn);
 
	// 重点,end始终是当前accumulate的终点之后一位,也就是下一个beg指向的位置;beg是当前accumulate的起点
	for (auto beg = vec.begin(), end = beg; beg != vec.end(); beg = end) {
		end = find_if(beg, vec.end(), [beg](Sales_item& s) { return beg->isbn() != s.isbn(); }); // find_if这里是!=
		cout << accumulate(beg, end, Sales_item(beg->isbn())) << endl;
		// 以只有ISBN号的其他元素都是0的作为初值
	}
	cout << endl;
	return 0;
}

运行结果:
运行结果
9、接受三个参数:一个输入文件和两个输出文件的文件名。输入文件保存的应该是整数。使用 istream_iterator 读取输入文件。使用 ostream_iterator 将奇数写入第一个输入文件,每个值后面都跟一个空格。将偶数写入第二个输出文件,每个值都独占一行

#include <iostream>
#include <fstream>
#include <iterator>
#include <string>

using namespace std;

int main()
{
	string inputFile = "10_33_input.txt";
	string outputFile1 = "10_33_output.txt", outputFile2 = "10_33_output2.txt";
	ifstream ifs(inputFile);
	ofstream ofs(outputFile1), ofs2(outputFile2);
	istream_iterator<int> iii(ifs), eof;
	ostream_iterator<int> oii(ofs, " "), oii2(ofs2, " "); // 读写文件i/ostream对象+迭代器
	while (iii != eof) {
		if (*iii % 2 == 0) {
			oii = *iii++; // 写出操作
		}
		else {
			oii2 = *iii++;
		}
	}
	return 0;
}

另一种实现:main传入参数 文件名信息;copy_if

#include <iostream>
#include <fstream>
#include <iterator>
#include <algorithm>
#include <vector>

using namespace std;

int main(int argc, char **argv) // main后面参数的类型要带
{
	if (argc != 4) {
		cerr << "function parameters ERROR" << endl;
		return -1;
	}
	cout << "Running " << argv[0] << endl;
	ifstream ifs(argv[1]);
	ofstream ofs(argv[2]), ofs2(argv[3]);
	istream_iterator<int> iii(ifs), eof;
	ostream_iterator<int> oii(ofs, " "), oii2(ofs2, " ");

	// copy_if(iii, eof, oii, [](int i) { return (i % 2); });
	// copy_if(iii, eof, oii, [](int i) { return !(i % 2); }); 
	// 错了,这样第二个就啥也读不进去,必须整vector记录
	vector<int> vec(iii, eof);
	copy_if(iii, eof, oii, [](int i) { return (i % 2); }); // 奇数
	copy_if(vec.begin(), vec.end(), oii, [](int i) { return !(i % 2); });
	cout << "Complete " << argv[0] << endl;
	return 0;
}

运行结果:
运行结果

附:argc,argv:main传入参数 复习 / copy_if函数

1)在C++中,argv 是一个指向字符指针数组的指针,它是 main 函数的参数之一。argv 代表命令行参数,它是一个数组,每个元素是一个指向以 null 结尾的 C 字符串的指针,表示一个命令行参数。argc 表示命令行参数的数量,其中包括程序名称本身

因此,argv 和 argc 的类型分别为:
argv:char**,指向字符指针数组的指针,即指向 C 字符串的指针数组。
argc:int,表示命令行参数的数量

2)copy_if 函数用于从一个范围中复制满足特定条件的元素到另一个范围
它需要四个参数:
1)要复制的范围的起始迭代器
2)要复制的范围的结束迭代器
3)指向新范围的起始位置的迭代器
4)谓词(predicate)函数,用于确定哪些元素应该被复制

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> source = {1, 2, 3, 4, 5};
    std::vector<int> destination;

    // 使用 copy_if 复制 source 中大于 2 的元素到 destination
    std::copy_if(source.begin(), source.end(), std::back_inserter(destination),
                 [](int x) { return x > 2; });

    // 输出 destination 中的元素
    for (int num : destination) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}

copy_if函数将 source 中大于 2 的元素复制到 destination 中。要注意的是,copy_if 不会自动调整 destination 的大小,所以需要使用 std::back_inserter 插入迭代器来自动调整大小

4.3 反向迭代器

1、在容器中 从尾元素向首元素 反向移动的迭代器。对于反向迭代器,递增(以及递减)操作的含义 会颠倒过来。递增一个反向迭代器(++it)会移动到前一个元素;递减一个选代器(–it)会移动到下一个元素

2、除了 forward_list 之外,其他容器 都支持反向迭代器。我们 可以通过调用rbegin、rend、crbegin 和 crend成员函数 来获得反向迭代器。这些成员函数 返回指向容器尾元素 和 首元素 之前一个位置的迭代器。与普通迭代器一样,反向迭代器也有const 和 非const版本

下面的循环 是一个使用反向迭代器的例子。按逆序打印vec中的元素:

vector<int> vec = {0,1,2,3,4,5,6,7,8,9};
// 从尾元素到首元素的反向迭代器
for (auto r_iter = vec.crbegin();    // 将r_iter绑定到尾元素
			r_iter != vec.crend();   // crend指向首元素之前的位置
			++r_iter)				 // 实际是递减,移动到前一个元素
	cout << *r_iter << endl; 		 //打印9,8.7...0

颠倒递增 和 递减运算符的含义 可能看起来 令人混淆,但这样做 使我们可以用算法 透明地 向前 或 向后处理容器。例如,可以通过 向sort传递 一对反向迭代器 来将vector 整理为 递减序

sort(vec.begin(), vec.end());   //按“正常序”排序vec
// 按逆序排序:将最小元素放在vec的末尾
sort(vec.rbegin(), vec.rend());

3、反向迭代器需要递减运算符:除了 forward_list 之外,标准容器上的 其他迭代器都 既支持 递增运算 又 支持 递减运算。但是,流迭代器 不支持 递减运算,因为 不可能在一个流中 反向移动。因此,不可能从一个forward_list 或 一个流迭代器 创建反向迭代器

4、反向迭代器 和 其他迭代器间 的关系:有一个 名为line的string,保存着 一个逗号分隔的单词列表,我们希望打印 line中的第一个单词

// 在一个逗号分隔的列表中 查找第一个元素
auto comma = find(line.cbegin(), line.cend(), ",");
cout << string(line.cbegin(), comma) << endl;

如果line中有逗号,那么comma将指向这个逗号

如果希望打印 最后一个单词,可以改用 反向迭代器

// 在一个逗号分隔的列表中 查找最后一个元素
auto rcomma = find(line.crbegin(), line.crend(), ',');

将crbegin() 和 crend() 传递给 find, find 将从line的最后一个字符开始 向前搜索。如果line中有逗号,则rcomma 指向最后一个逗号

//错误:将逆序输出单词的字符
cout << string(line.crbegin(), rcomma) << endl;

使用的是反向选代器,会反向处理string。我们不能 直接使用rcomma。因为它是一个 反向迭代器

需要做的是,将rcomma转换回 一个普通迭代器,能在 line中正向移动。我们通过 调用reverse_iterator 的 base成员函数 来完成 这一转换,此成员函数会 返回其对应的普通迭代器:

// 正确:得到一个正向迭代器,从逗号开始 读取字符直到line末尾
cout << string(rcomma.base(), line.cend()) << endl;

反向迭代器 和 其他迭代器间 的关系
rcomma 和 rcomma.base() 指向 不同的元素,line.crbegin() 和 line.cend() 也是如此
普通迭代器 与 反向迭代器的关系 反映了 左闭合区间 的特性

5、使用 reverse_iterator / 普通迭代器 逆序打印一个vector

#include <iostream>
#include <iterator>
#include <vector>

using namespace std;

int main()
{
	vector<int> vec = { 1, 2, 3, 4, 5, 6 };
	for (auto it = vec.crbegin(); it != vec.crend(); it++)
		cout << *it << " ";
	cout << endl;
	auto iter = vec.cend();
	while (iter != vec.cbegin())
		cout << *(--iter) << " "; // 要先减再解指针
	cout << endl;
	return 0;
}

6、使用 find 在一个 int 的list 中查找最后一个值为0的元素

#include <iostream>
#include <iterator>
#include <list>

using namespace std;

int main()
{
	list<int> li = { 1,2,3,4,4,0 };
	auto it = find(li.crbegin(), li.crend(), 0);
	cout << distance(it, li.crend()) << endl; // 运行结果是6,因为运算符也一样逆过来了
	cout << distance(li.cbegin(), it.base()) << endl; // 等价代码,逆向和正向的结果相同
	// cout << distance(it, li.end()) << endl; 报错,只能同类型的迭代器(正逆,常量与否)
	return 0;
}

7、给定一个包含10 个元素的vector,将位置3到7之间的元素按逆序拷贝到一个list中

#include <iostream>
#include <iterator>
#include <list>
#include <vector>

using namespace std;

int main()
{
	vector<int> vec = { 1,2,3,4,5,6,7,8,9,10 };
	list<int> li(vec.rbegin() + 3, vec.rbegin() + 8); // 末尾不是+7,因为左闭右开
	for (int i : li)
		cout << i << " ";
	cout << endl;
	list<int> li2(vec.cbegin() + 2, vec.cbegin() + 7); // 等价代码
	for (auto it = li2.crbegin(); it != li2.crend(); it++) {
		cout << *it << " ";
	}
	return 0;
}

5、泛型算法结构

1、任何算法的最基本的特性 是它 要求其迭代器提供哪些操作。某些算法,如find,只要求 通过迭代器访问元素、递增迭代器 以及 比较两个迭代器是否相等 这些能力。其他一些算法,如sort,还要求读、写 和 随机访问元素的能力。算法所要求的迭代器操作可以分为 5个迭代器类别

迭代器类别

迭代器类别解释
输入迭代器只读,不写;单遍扫描,只能递增
输出迭代器只写,不读;单遍扫描,只能递增
前向迭代器可读写;多遍扫描,只能递增
双向迭代器可读写;多遍扫描,可递增递减
随机访问迭代器可读写,多遍扫描,支持全部迭代器运算

第二种算法分类的方式 是按照是否读、写 或是 重排序列中的元素来分类

5.1 5类迭代器

1、迭代器 也定义了 一组公共操作。一些操作 所有迭代器都支持,另外一些只有 特定类别的迭代器才支持。例如,ostream _iterator 只支持 递增、解引用 和 赋值。vector、string 和 deque 的迭代器 除了这些操作外,还支持 递减、关系 和 算术运算

而这种分类 形成了一种层次。除了输出迭代器之外,一个高层类别的迭代器 支持低层类别迭代器的所有操作

C++标准 指明了 泛型和数值算法的 每个迭代器参数的最小类别。例如,find算法 在一个序列上 进行一遍扫描,对元素进行只读操作,因此至少需要输入迭代器。replace函数 需要一对迭代器,至少是 前向迭代器。类似的,replace_copy的前两个迭代器参
数 也要求至少是前向选代器。其第三个迭代器 表示 目的位置,必须至少是 输出迭代器
对每个迭代器参数 来说,其能力必须 与规定的最小类别至少相当。向算法传递一个能力更差的迭代器 会产生错误

对于 向一个算法传递错误类别的迭代器的问题,很多编译器 不会给出任何警告 或 提示

2、迭代器类别:
1)输入送代器:可以读取 序列中的元素。一个输入迭代器必须支持

  • 用于 比较两个迭代器的相等 和 不相等 运算符(==、!=)
  • 用于 推进迭代器的前置和后置递增运算(++)
  • 用于 读取元素的解引用运算符(*):解引用 只会出现在赋值运算符的右侧
  • 箭头运算符(->),等价于(*it).member,即,解引用迭代器,并 提取对象的成员

输入迭代器 只用于顺序访问。对于 一个输入迭代器,*it++ 保证是有效的,但递增它可能导致所有其他指向流的选代器失效。其结果就是,不能保证 输入迭代器的状态 可以保存下来 并 用来访问元素。因此,输入迭代器 只能用于 单遍扫描算法
算法find 和 accumulate 要求 输入迭代器;而istream_iterator 是 一种输入迭代器

递增它可能导致所有其他指向流的选代器失效:

对于某些特定类型的输入流,如文件或标准输入流,这可能会导致当前元素不再可访问,并且可能影响到指向同一流的所有其他迭代器。这是由于输入迭代器的以下特性造成的:
1)单遍扫描:输入迭代器仅支持对数据的单向和单遍扫描。当你递增迭代器以读取下一个元素时,当前元素可能会从底层数据源中被消耗掉(例如,从标准输入读取数据)。这意味着,一旦迭代器前进,当前位置的数据就无法再通过迭代器访问了

2)无回溯:与双向或随机访问迭代器不同,输入迭代器不支持回溯。一旦你递增了迭代器,就没有办法再返回到前一个元素。这是因为输入迭代器的设计主要是为了从如IO流这样的序列中读取数据,而这类数据源通常不支持回溯

3)流状态变化:当你从输入流(如文件流或标准输入流)中读取数据时,流的状态会随着数据的读取而变化。例如,文件指针会向前移动。如果有多个迭代器指向同一个输入流,一旦其中一个迭代器递增并从流中读取数据,流的当前位置会改变,这会影响到所有其他迭代器,因为它们现在指向了流的新位置。在某些情况下,这可能导致其他迭代器失效,尤其是在它们期望访问的数据已经被读取并且因为迭代器递增而被流状态改变时

4)不保证多迭代器有效性:标准库通常不保证在对输入流使用多个迭代器时它们的有效性。这是因为输入迭代器的设计假设它们是顺序且独立使用的,不考虑并发或重复访问同一数据元素的情况。

总之,当递增一个指向输入流的迭代器时,应该假定所有其他指向同一流的迭代器都可能失效,因为流的状态已经改变,且输入迭代器的设计并不支持在数据被消费后再次访问它

2)输出迭代器:可以看作 输入迭代器 功能上的补集 —— 只写而不读元素
输出迭代器 必须支持

  • 用于 推进迭代器的前置 和 后置递增运算(++)
  • 解引用运算符(*),只出现在 赋值运算符的左侧(向一个 已经解引用的 输出迭代器 赋值,就是 将值写入它所指向的元素)

只能 向一个输出迭代器 赋值一次。类似 输入迭代器,输出迭代器 只能 用于单遍扫描算法。用作 目的位置的迭代器 通常 都是输出迭代器
例如,copy函数的第三个参数 就是输出迭代器。ostream_iterator 类型 也是输出迭代器

3)前向迭代器:可以读写元素。这类迭代器 只能在序列中 沿一个方向移动
前向迭代器 支持 所有输入 和 输出迭代器的操作,而且 可以多次读写 同一个元素。因此,我们可以保存 前向迭代器的状态,使用 前向迭代器的算法 可以对序列进行多遍扫描
算法replace 要求 前向迭代器,forward_list 上的 迭代器是 前向迭代器

4)双向迭代器:可以正向 / 反向 读写序列中的元素。除了 支持所有前向迭代器 的操作之外,双向迭代器 还支持前置 和 后置递减运算符(- -)
算法reverse要求双向迭代器,除了 forward_list 之外,其他标准库 都提供符合 双向迭代器要求的迭代器

5)随机访问迭代器:提供 在常量时间内 访问序列中任意元素的能力。此类迭代器 支持双向迭代器的 所有功能,还有

  • 用于 比较两个迭代器 相对位置的关系运算符(<、<=、>和>=)
  • 迭代器 和 一个整数值的加减运算(+、+=、-和-=),计算结果是 迭代器在序列中前进(或后退)给定整数个元素后的位置
  • 用于 两个迭代器上的减法运算符(- -),得到 两个迭代器的距离
  • 下标运算符 iter[n],与 *(iter[n]) 等价

算法sort 要求 随机访问迭代器。array、deque、string 和 vector 的迭代器都是 随机访问迭代器,用于 访问内置数组元素的指针也是

3、列出 5个迭代器类别,以及 每类迭代器所支持的操作
输入迭代器:==、!=、++、*、->;
输出迭代器:++、*;
前项迭代器:==、!=、++、*、->;
双向迭代器:==、!=、++、--、*、->;
随机访问迭代器:==、!=、++、--、*、->、<、<=、>、>=、+、+=、-、-=、-、iter[n]、*(iter[n])

4、list 上的迭代器是 双向迭代器,vector 上的迭代器是 随机访问迭代器
注:vector 在内存中 是连续存储的,所以才可以 用随机访问迭代器。list 链表 在内存中不是连续存储的

5、copy 要求前两个参数 至少是输入迭代器,表示 一个输入范围。它读取这个范围中的元素,写入到第三个参数 表示的输出序列中,因此 第三个参数至少是输出迭代器
reverse 要 反向处理序列,因此它要求 两个参数至少是 双向迭代器
unique 顺序扫描元素,覆盖重复元素,因此要求 两个参数至少是 前向迭代器。“至少” 意味着能力更强的迭代器是可接受的

5.2 算法形参模式

1、在任何其他算法分类之上,还有一组 参数规范。大多数算法具有如下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);

alg是 算法的名字,beg 和 end 表示算法所操作的输入范围。dest、beg2 和 end2,都迭代器参数,分别承担 指定目的位置 和 第二个范围的角色

2、接受单个目标选代器的算法:dest参数 是 一个表示算法可以写入的 目的位置的 迭代器
向 输出迭代器 写入数据的算法 都假定 目标空间足够容纳写入的数据

如果dest是 一个直接指向容器的迭代器,那么 算法将输出数据 写到容器中已存在的 元素内。更常见的情况是,dest 被绑定到一个插入迭代器 或是 一个ostream_iterator。插入迭代器 会将新元素添加到 容器中,因而保证 空间是足够的。ostream_iterator 会将 数据写入到一个输出流,同样 不管要写入多少个元素都没有问题

3、接受 第二个输入序列的算法:接受单独的beg2 或是 接受beg2和end2的算法 用这些迭代器 表示 第二个输入范围
这类算法 接受两个完整指定的范围:[beg, end)表示的范围 和 [beg2, end2) 表示的 第二个范围

只接受单独的beg2(不接受end2)的算法将 beg2作为 第二个输入范围中的首元素。此范围的结束位置 未指定,这些算法假定从beg2开始的范围 与beg 和 end所表示的范围 至少一样大

接受单独beg2的算法 假定从beg2开始的序列 与beg和end所表示的范围 至少一样大

5.3 算法命名规范

1、除了 参数规范,算法还遵循 一套命名和重载规范。这些规范处理 诸如:如何提供一个操作 代替 默认的<或==运算符 以及 算法是将输出数据写入输入序列 还是 一个分离的目的位置等问题

2、一些算法 使用重载形式 传递一个谓词:接受谓词参数来代替 <或=运算符 的算法,以及那些 不接受额外参数的算法,通常都
是 重载的函数。函数的一个版本 用元素类型的运算符 来比较元素;另一个版本 接受一个额外谓词参数,来代替 <或==

unique(beg, end); 		// 使用==运算符比较元素
unique(beg, end, comp); // 使用comp比较元素

两个调用 都重新整理给定序列,将相邻的重复元素删除

3、_if版本的算法:接受一个元素值的算法 通常有 另一个不同名的(不是重载的)版本,该版本接受一个谓词 代替 元素值。接受谓词参数的算法 都有附加的if前缀

find(beg, end, val);  	 // 查找输入范围中val第一次出现的位置
find_if(beg, end, pred); // 查找第一个令p red为真的元素

这两个算法 都在输入范围中 查找特定元素 第一次出现的位置
这两个算法 提供了命名上差异的版本,而非重载版本,因为两个版本的算法 都接受相同数目的参数,因此可能产生重载歧义

4、区分拷贝元素的版本 和 不拷贝的版本:默认情况下,重排元素的算法 将重排后的元素 写回给定的输入序列中。这些算法还提供 另一个版本,将元素 写到一个指定的输出目的位置
写到额外目的空间的算法 都在名字后面 附加一个 _copy

reverse(beg, end); 				// 反转输入范围中元素的顺序
reverse_copy(beg, end, dest); 	// 将元素按逆序拷贝到dest

一些算法 同时提供copy 和 _if 版本。这些版本 接受一个目的位置迭代器 和 一个谓词:

// 从V1中 删除奇数元素
remove_if(vl.begin(), vl.end(),
					[](int i) { return i % 2; });
// 将偶数元素 从v1拷贝到v2;v1不变
remove_copy_if(vl.begin(), vl.end(), back_inserter(v2),
				[](int i) { return i % 2; });

5、仅根据算法和参数的名字,描述下面每个标准库算法执行什么操作

replace(beg, end, old_val, new_val);
replace_if(beg, end, pred, new_val);
replace_copy(beg, end, dest, old_val, new_val);
replace_copy_if(beg, end, dest, pred, new_val);

将范围 [beg, end) 值等于 old_val 的元素替换为 new_val
将范围 [beg, end) 满足 谓词 pred 的元素替换为 new_val
将范围 [beg, end) 的元素 拷贝到目的序列 dest 中,将其中值等于 old_val 的元素替换为 new_val
将范围 [beg, end) 的元素 拷贝到目的序列 dest 中,将其中满足谓词 pred 的元素替换为 new_val

6、特定容器算法

1、链表类型list 和 forward_list 定义了 几个成员函数形式的算法,定义了独有的sort、merge(有序合并)、remove、reverse和
unique。通用版本的sort 要求随机访问迭代器,因此 不能用于list 和 forward list,因为 这两个类型 分别提供双向迭代器 和 前向迭代器

2、链表类型 定义的其他算法的通用版本 可以用于链表,但代价太高。这些算法需要 交换输入序列中的元素。一个链表 可以通过改变元素间的链接 而不是真的交换它们的值 来快速“交换”元素。因此,这些链表版本的算法的性能 比对应的通用版本好得多

对于list 和 forward_list,应该 优先使用成员函数版本的算法 而不是通用算法

3、list 和 forward_list 成员函数版本的算法,这些操作 都返回void

函数解释
lst.merge(lst2), lst.merge(lst2, comp)将来自lst2的元素 合并入lst。lst 和 lst2 都必须是有序的。元素将从lst2中删除。在合并之后,lst2变为空。第一个版本 使用 < 运算符; 第二个版本 使用给定的比较操作
lst.remove(val), lst.remove_if(pred)调用 erase 删除掉 与给定值相等(==)或 令一元谓词为真的 每个元素
lst.reverse()反转lst中元素的顺序
lst.sort(), lst.sort(comp)使用<或给定比较操作 排序元素
lst.unique(), lst.unique(pred)调用erase 删除同一个值 的连续拷贝。第一个版本 使用==;第二个版本 使用给定的二元谓词

4、splice成员:链表类型还定义了splice算法,此算法是链表数据结构所特有的,把指定的元素 移动到 指定位置(list是之前,forward_list 是之后)

list 和 forward_list 的splice成员函数的参数,lst.splice(args) 或 flst.splice_after(args)

参数解释
(p, lst2)p是一个指向lst中元素的迭代器,或 一个指向 flst 首前位置的迭代器。函数 将lst2的所有元素 移动到 lst 中 p 之前的位置 或是 flst 中 p 之后的位置。将元素从 lst2 中删除。lst2的类型 必须与lst 或 flst 相同,且不能是 同一个链表
(p, lst2, p2)p2是一个指向lst2中位置的 有效的迭代器。将p2指向的元素移动到lst中,或 将p2之后的元素 移动到flst中。lst2可以是 与lst 或 fist 相同的链表
(p, lst2, b, e)b和e 必须表示 lst2中 的合法范围。将给定范围中的元素从 lst2 移动到 lst 或 flst。lst2 与 lst(或flst)可以是 相同的链表,但p不能指向给定范围中元素

5、链表特有的操作 会改变容器:链表 特有版本 与 通用版本间的 一个至关重要的区别是 链表版本会改变底层的容器。例如,remove的链表版本 会删除指定的元素。unique的链表版本 会删除第二个 和 后继的重复元素
类似的,merge和splice会销毁其参数

例如,通用版本的merge 将合并的序列写到 一个给定的目的迭代器:两个输入序列是不变的
而链表版本的merge函数 会销毁 给定的链表——元素从参数指定的链表中删除,被合并到 调用merge的链表对象中

6、去除重复单词(注意list的操作)+ 将 nums 它中的每个元素平方后输出到标准输出,每个数之间用空格分隔(泛型算法+输出迭代器+lambda)

#include <iostream>
#include <list>
#include <string>
#include <iterator>
#include <algorithm>
#include <vector>

using namespace std;

void elimDups(list<string>& ls) {
	ls.sort(); // 注意list的操作
	ls.unique();
}

int main()
{
	// 去除重复单词
	list<string> ls = { "the", "quick", "red", "fox", "jumps",
									 "over", "the", "slow", "red", "turtle" };
	elimDups(ls);
	ostream_iterator<string> ois(cout, " ");
	copy(ls.begin(), ls.end(), ois);
	cout << endl;

	// 将 nums 它中的每个元素平方后输出到标准输出,每个数之间用空格分隔:泛型算法+输出迭代器+lambda
	vector<int> nums = { 1, 2, 3, 4, 5 };
	transform(nums.begin(), nums.end(),
		ostream_iterator<int>(cout, " "),
		[](int x) { return x * x; }); 
	return 0;
}

术语表

1、泛型算法:类型无关的算法

2、谓词:返回 可以转换为bool类型 的值的函数。泛型算法 通常用来 检测元素。标准库使用的 谓词 是一元(接受一个参数)或 二元的(接受两个参数)

3、ref:标准库函数,从一个指向不能拷贝的类型的对象的引用 生成一个可拷贝的对象

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值