C++Primer——第10章(泛型算法)

10.1 泛型算法概述

本章提到的泛型算法:
find
InputIt find( InputIt first, InputIt last, const T& value );
first,last ,value分别为 两个标记范围的迭代器、目标查找值。 返回值:如果找到给定目标值,返回对应的迭代器,否则返回第二个参数,即标记结尾的迭代器。
accumulate
accumulate( InputIt first, InputIt last, T init );
first,last,init分别为 要求和的元素范围、和的初值。 返回值:给定和的初值 与给定范围中的元素 的和。
equal
bool equal ( InputIt1 first1, InputIt1 last1,InputIt2 first2 );
first1,last1,first分别为 标记第一个范围的两个迭代器、第二个范围的起始迭代器。 返回值:如果两个范围内 所有对应元素都相等则返回true,否则返回false。
fill
void fill( ForwardIt first, ForwardIt last, const T& value );
first,last,value分别为 标记要修改的元素范围的两个迭代器、给定修改后的值。 
fill_n
void fill_n( OutputIt first, Size count, const T& value );
first,count,value分别为 标记要修改的元素范围起始的迭代器、要修改的元素个数、要赋的值。
copy
OutputIt copy( InputIt first, InputIt last, OutputIt d_first );
first,last,d_first分别为 标记要复制的元素范围的两个迭代器、目标范围的起始迭代器。
replace
void replace( ForwardIt first, ForwardIt last, const T& old_value, const T& new_value );
first,last,old_value,new_value分别为 标记要处理的元素范围的两个迭代器、要被替换的元素值、用作替换的新值。
replace_copy
OutputIt replace_copy( InputIt first, InputIt last, OutputIt d_first, const T& old_value, const T& new_value );
first,last,d_first、old_value,new_value分别为 标记要复制的元素范围的两个迭代器目标范围的起始迭代器、要被替换的元素值、用作替换的新值。
sort
void sort( RandomIt first, RandomIt last );
fitst,last 为标记要排序的元素范围的两个迭代器。
unique
ForwardIt unique( ForwardIt first, ForwardIt last );
fitst,last 为标记要处理的元素范围的两个迭代器重排输入序列,并将相邻的重复项“消除。 返回值:指向不重复值范围末尾的迭代器。
stable_sort
void stable_sort( RandomIt first, RandomIt last );
对first、last标记的范围内的元素进行"稳定排序"(保留 "相等元素" 的原始相对位置)。
find_if
InputIt find_if( InputIt first, InputIt last,UnaryPredicate p );
first、last、p分别为 标记要查找元素范围的两个迭代器、谓词。查找第一个满足特定要求的元素。 返回值:第一个使谓词返回非0值的元素;如果不存在这样的元素,则返回尾迭代器end()。
for_each
UnaryFunction for_each( InputIt first, InputIt last, UnaryFunction f );
first、last、f分别为 标记要查找元素范围的两个迭代器、函数对象。此算法接受一个函数对象,并对输入序列中的每个元素 调用此对象。
partition
BidirIt partition( BidirIt first, BidirIt last, UnaryPredicate p );
first、last、p标记要重排序元素范围的两个迭代器、谓词。对容器内容进行划分,使得谓词p为true的值 会排在容器的前半部分,而使谓词p为false 的值会排在后半部分。 返回值:指向最后一个使谓词为true的元素之后的位置的迭代器。
transform
OutputIt transform( InputIt first1, InputIt last1, OutputIt d_first, UnaryOperation unary_op );
first,last,d_first,unary_op分别为 标记输入序列两个迭代器、目标序列的起始迭代器、一元函数。算法对输入序列中的每个元素调用unary_op,并将结果写到目标序列 中的对应目标位置。 返回值:指向目标序列中最后一个元素 的后一个位置的迭代器。
因为它们实现共同的操作,所以称之为“算法”;而“泛型” 指的是它们可以操作在多种容器类型上。
泛型算法本身 不执行容器操作,只是单独依赖 迭代器和迭代器操作实现。
  • 头文件: #include <algorithm>(算法库) 或者 #include <numeric>(数值库)
  • 必要的编程假定:算法永远不会改变底层容器的大小。算法可能改变容器中保存的元素的值,也可能在容器内移动元素,但不能直接添加或者删除元素。
InputIt find( InputIt first, InputIt last, const T& value );
first,last ,value分别为 两个标记范围的迭代器、目标查找值。 返回值:如果找到给定目标值,返回对应的迭代器,否则返回第二个参数,即标记结尾的迭代器。

10.2 初识泛型算法

标准库提供了超过100个算法,但这些算法 有一致的结构。
理解算法的最基本的方法是 了解它们是否读取元素、改变元素、重排元素顺序。
注:算法不会执行容器操作,因此它们"自身"不可能改变容器的大小。

(1) 只读算法:
只读取范围中的元素,但不改变元素。
T accumulate( InputIt first, InputIt last, T init );
first,last,init分别为 要求和的元素范围、和的初值。 返回值:给定和的初值 与给定范围中的元素 的和。
bool equal( InputIt1 first1, InputIt1 last1,InputIt2 first2 );
first1,last1,first分别为 标记第一个范围的两个迭代器、第二个范围的起始迭代器。 返回值:如果两个范围内 所有对应元素都相等则返回true,否则返回false。
int main() {
	std::vector<int> vec1{ 1,2,3,4 };
	std::vector<int> vec2{ 1,2,3,4 };
	std::vector<int> vec3{ 1,2,3 };

	int counts = std::accumulate(vec1.begin(), vec1.end(), 5);			    // 15

	bool result = std::equal(vec1.begin(), vec1.end(), vec2.begin());      // true
	bool result1 = std::equal(vec1.begin(), vec1.end(), vec3.begin());     // 错误: equal只传递一个迭代器vec3.begin()来表示 比较的第二个范围,但此算法要处理第一个序列中的每个元素,它假定每个元素在第二个序列中都有一个与之对应的元素。
}
注:那些只接受一个单一迭代器 来表示第二个序列的算法,都假定第二个序列至少与第一个序列一样长。
建议:因为只需要读取元素,所以通常最好使用 cbegin和cend。

(2) 写容器元素的算法:
这些算法将新值赋予序列中的元素。
void fill( ForwardIt first, ForwardIt last, const T& value );
first,last,value分别为 标记要修改的元素范围的两个迭代器、给定修改后的值。 
void fill_n( OutputIt first, Size count, const T& value );
first,count,value分别为 标记要修改的元素范围起始的迭代器、要修改的元素个数、要赋的值。
OutputIt copy( InputIt first, InputIt last, OutputIt d_first );
first,last,d_first分别为 标记要复制的元素范围的两个迭代器、目标范围的起始迭代器。
void replace( ForwardIt first, ForwardIt last, const T& old_value, const T& new_value );
first,last,old_value,new_value分别为 标记要处理的元素范围的两个迭代器、要被替换的元素值、用作替换的新值。
OutputIt replace_copy( InputIt first, InputIt last, OutputIt d_first, const T& old_value, const T& new_value );
first,last,d_first、old_value,new_value分别为 标记要复制的元素范围的两个迭代器目标范围的起始迭代器、要被替换的元素值、用作替换的新值。
int main() {
	
	std::vector<int> vec1{ 1,2,3,4 };
	std::vector<int> vec2{ 1,2,3,4 };
	std::vector<int> vec3{ 1,2,3,4 };
	std::vector<int> vec4{ 0,0,0,0 };
	std::vector<int> vec5{ 0,0,0,0 };
	std::vector<int> vec6{ 0,0,0,0 };

	std::fill(vec2.begin(), vec2.begin() + 2, 0);         // vec2={0,0,3,4} (范围内两个元素 被替换成0)
	std::fill_n(vec3.begin(), 2, 0);                      // vec3={0,0,3,4} (从vec.begin()开始的两个元素 被替换成0)
	std::copy(vec1.begin(), vec1.end(), vec4.begin());    // vec4={1,2,3,4} (范围内的元素 复制到另一个范围)
	std::replace(vec5.begin(), vec5.end(), 0, 1);         // vec5={1,1,1,1} (范围内的元素 值为0的都被替换成1)
	std::replace_copy(vec1.begin(), vec1.end(), vec6.begin(), 2, 1);     // vec5={1,1,3,4} (范围内的元素被 复制到从vec6.begin()起始的范围,复制过程中 原范围内元素 值为2的都替换成1后,再复制到新范围。注:原范围内的值不变)
}

插入迭代器back_inserter
用来确保算法有足够的空间存储数据。
std::back_insert_iterator<Container> back_inserter( Container& c );
c 为支持push_back操作的容器。 返回值:能用于添加元素 到容器c尾端的 std::back_insert_iterator 。
int main() {
	vector<int> vec;

	std::fill_n(vec.begin(), 5, 0);                   // 错误: vec容器大小为0,不允许复制数据
	std::fill_n(std::back_inserter(vec), 5, 1);       // 正确: back_inserter为vec容器创建一个 尾部插入迭代器,可用于在容器vec尾部插入元素
}

(3) 重排容器元素的算法:
这些算法重排容器中元素的顺序。
void sort( RandomIt first, RandomIt last );
fitst,last 为标记要排序的元素范围的两个迭代器。
ForwardIt unique( ForwardIt first, ForwardIt last );
fitst,last 为标记要处理的元素范围的两个迭代器重排输入序列,并将相邻的重复项“消除。 返回值:指向不重复值范围末尾的迭代器。
int main() {
	vector<int> vec1{ 1,3,2,4 };
	vector<int> vec2{ 4,3,2,3,1,1 };

	std::sort(vec1.begin(), vec1.end());      // vec1={1,2,3,4}(范围内元素进行排序)

	std::sort(vec2.begin(), vec2.end());      // vec2={1,2,3,3,4}(注: 使用unique之前的序列一定要是有序的!,否则没有作用,所以要先调用sort排序)
	auto end_unique = std::unique(vec2.begin(), vec2.end());      // vec2={1,2,3,4,3,4} (注: 实际只有前面{1,2,3,4}是有效的,后面{3,4}是无效的。end_unique为指向{1,2,3,4}中最后一个元素(4)的下一个元素(3)的迭代器)
	cout << *end_unique;      // 3
	vec2.erase(end_unique, vec2.end());       // vec2={1,2,3,4}(使用unique后通常会 调用容器的erase方法,它擦除未指定值{3,4}并减小容器的物理大小,以匹配其新的逻辑大小)
}
注:
  • unique只对有序序列操作,所以调用unique之前 先调用sort;
  • 调用unique后,重复的元素并未被真正删除,真正删除必须使用 容器自身的删除操作。(如:vec2.erase(end_unique, vec2.end());)
  • 标准库算法 对迭代器而不是容器 进行操作。因此算法不能(直接) 添加或删除元素。

10.3 定制操作

(1) 向算法传递函数:
谓词:用以判断参数 是否具有某些性质的 函数对象 (可以是函数,函数指针,仿函数等);是一个可调用的表达式,返回结果是一个能用作条件的值(bool类型)。
  • 一元谓词:接受一个参数
  • 二元谓词:接受两个参数
void stable_sort( RandomIt first, RandomIt last );
对first、last标记的范围内的元素进行"稳定排序"(保留 "相等元素" 的原始相对位置)。
bool isShorter(const string& s1, const string& s2) {
	return s1.size() < s2.size();
}

int main() {
	vector<string> vec{ "aaad","aaac","aab","aaa","aaab" };

	std::sort(vec.begin(), vec.end(),isShorter);               // vec={"aab","aaa","aaad","aaac","aaab"}(按长度排是正确的,但按字典序是错误的!)

	// 希望将vec按大小排序的同时,还希望 具有相同长度的元素按字典序排列
	std::sort(vec.begin(), vec.end());                         // vec={"aaa","aaab","aaac,"aaad","aab"}(按字典序排序)
	std::stable_sort(vec.begin(), vec.end(), isShorter);       // vec={"aaa","aab","aaab","aaac","aaad"} (按长度排序,但又能够保持相同元素(长度相对的元素) 的原本相对次序,因此也就保持了字典序)
}
注:
  • 带有 ”stable“ 前缀的函数 可保证相同元素的原本相对次序 在排序之后保持不变。
  • stable_sort内部实现是 归并排序,稳定; sort的内部实现是 快速排序,不稳定。

(2) lambda表达式:
lambda表达式:表示一个可调用的代码单元,可以理解成是一个未命名的内联函数。
用途:有时可能希望操作可以接受更多的参数。(如:根据算法接受一元谓词还是 二元谓词,我们传递给算法的谓词 必须严格接受一个或两个参数。但是,有时我们希望进行的操作需要更多参数,超出了算法对谓词的限制。
形式:[capture list](parameter list) -> return type {function body}。
  • capture list捕获列表 是一个lambda所在函数定义的 局部变量的列表 (通常为空)。不可忽略
  • return type 是返回类型。可忽略
  • parameter 是参数列表。可忽略
  • function body 是函数体。不可忽略
InputIt find_if( InputIt first, InputIt last,UnaryPredicate p );
first、last、p分别为 标记要查找元素范围的两个迭代器、谓词。查找第一个满足特定要求的元素。 返回值:第一个使谓词返回非0值的元素;如果不存在这样的元素,则返回尾迭代器end()。
UnaryFunction for_each( InputIt first, InputIt last, UnaryFunction f );
first、last、f分别为 标记要查找元素范围的两个迭代器、函数对象。此算法接受一个函数对象,并对输入序列中的每个元素 调用此对象。
BidirIt partition( BidirIt first, BidirIt last, UnaryPredicate p );
first、last、p标记要重排序元素范围的两个迭代器、谓词。对容器内容进行划分,使得谓词p为true的值 会排在容器的前半部分,而使谓词p为false 的值会排在后半部分。 返回值:指向最后一个使谓词为true的元素之后的位置的迭代器。
// 练习 10.13
int main() {
	std::vector<string> vec = { "aaaa","aaaa1","bbbb1","bbbb","cccc","cccc1","dddddd" };

	auto end_position = std::partition(vec.begin(), vec.end(), compare);       // vec={"dddddd","aaaa1","bbbb1","cccc1","cccc","bbbb","aaaa"}(满足条件 "str.size()>5" 的为前四个元素{"dddddd","aaaa1","bbbb1","cccc1"})
	vec.erase(end_position, vec.end());          // vec={"dddddd","aaaa1","bbbb1","cccc1"},删除不满足条件的元素(即后面三个元素{"cccc","bbbb","aaaa"})
	

	// partition只能接受一元谓词,则说明作为谓词的compare函数 只能有一个参数,所以必须将 "长度5" 硬编码到compare函数体内。如果条件长度改为6,则需要额外编写一个函数。
	// 使用lambda表达式就可以巧妙解决这一点,通过捕获列表捕获 局部变量len,因此可以不受参数数量的限制的影响
	size_t len = 6;
	auto end_position = std::partition(vec.begin(), vec.end(), [len](const string& str) {return str.size() >= len; });   
	vec.erase(end_position, vec.end());          // vec={"dddddd"}
}
int main() {
	vector<string> vec = { "aaa","bbb","ccc" };

	auto target_itr = std::find_if(vec.begin(), vec.end(), [](const string& str)-> bool { return str == "ccc"; });      // *target_itr="ccc"(lambar表达式的返回类型 "->bool" 可以省略,编译器会根据返回语句自动推导)

	std::for_each(vec.begin(), vec.end(), [](const string& str) {cout << str << " "; });      // 输出{"aaa","bbb","ccc"}(之所以可以不用捕获 而直接使用cout,是因为cout是定义在当前所在函数(main)之外的名字)
}
注:捕获列表只用于 局部非static变量,lambda可以直接使用 局部static变量 和 在它所在函数之外声明的名字。(如: cout)

(3) lambda捕获和返回:
定义一个lambda时,会生成一个与lambda对应的 新的类类型 和该类型的一个对象。
默认情况下,从lambda生成的类都包含一个 对应该lambda所捕获变量的 数据成员,在lambda对象创建时被初始化。
lambda捕获列表:
[ ] 
空捕获列表。lambda不能使用 所在函数中的变量。一个lambda只有在捕获变量后 才能使用它们。
[names]
names是一个 逗号分隔的名字列表,这些名字都是lambda所在函数的 局部变量。默认情况下,捕获列表中的变量都被拷贝,但名字前如果使用了&,则采用引用捕获方式。
[&]
隐式捕获列表,采用 引用捕获方式。lambda体中所使用的 来自所在函数的实体 都采用引用方式使用。
[=]
隐式捕获列表,采用值捕获方式。lambda体将 拷贝所使用的 来自所在函数的实体的值。
[&, identifier_list] identifier_list是一个 逗号分隔的名字列表,包含0个或多个来自所在函数的变量,这些变量采用 值捕获方式;而任何隐式捕获的变量都采用 引用方式捕获。 注:identifier_list中的名字前面不能使用&。
[=, identifier_list]
identifier_list中的变量采用 引用方式捕获;而任何隐式捕获的变量都 采用值方式捕获。注:identifier_list中的名字不能包括this,且前面必须使用&。
  • 值捕获:前提是变量可以拷贝。(如:cout不可拷贝)
  • 引用捕获:必须保证在lambda执行时,变量是存在的。(如:函数返回一个lambda,该lambda不能包含引用捕获)
  • 隐式捕获:让编译器推断捕获列表,在捕获列表中写一个 &(引用方式) 或 =(值方式)。auto f3 = [=] {return v1;}
  • 混合使用隐式捕获和显示捕获:捕获列表中的第一个元素必须是一个 &或=。此符号指定了 默认捕获方式 为引用或值。
int main() {
	
	// 被捕获的变量的值 是在lambda"创建时"拷贝,而不是调用时拷贝:
	int a = 1;
	auto f0 = [a] {return a; };
	a = 0;
	int j = f0();   // j为1,f0保存了我们创建它时 a的拷贝


	// 值捕获: 前提是变量可以拷贝
	int b = 1;
	auto f1 = [b] {return b; };           // 正确
	auto f2 = [cout] {cout << "a"; };     // 错误: cout不可以拷贝


	// 引用捕获: 必须保证在lambda执行时,变量是存在的
	int c = 1;
	auto f3 = [&c] {return c; };


	// 隐式捕获:让编译器推断捕获列表,在捕获列表中写一个 &(引用方式) 或 =(值方式)
	int d = 1;
	auto f4 = [=] {return d; };
	auto f5 = [&] {return d; };


	// 混合使用隐式捕获和显示捕获:捕获列表中的第一个元素必须是一个 &或=。此符号指定了 默认捕获方式为引用或值。
	int e = 1, f = 1;
	auto f6 = [&, f] {return e + f; };     // f为显式捕获,值捕获方式; e为隐式捕获,引用捕获方式
	auto f7 = [=, &e] {return e + f; };    // f为隐式捕获,值捕获方式; e为显式捕获,引用捕获方式
	auto f8 = [=, f] {return e + f; };     // 错误: 不能同时存在隐式和显式的 值捕获
	auto f9 = [&, &f] {return e + f};      // 错误: 不能同时存在隐式和显式的 引用捕获
	auto f10 = [&, =] {return e + f};      // 错误: 不能同时存在两种隐式捕获


	// 默认情况下,对于一个值被拷贝的变量,lambda不会改变其值。如果我们希望能改变一个被捕获的变量的值,就必须在参数列表尾加上关键字mutable。
	int g = 1;
	auto f11 = [g]() {return ++g; };              // 错误: 捕获的变量g不可修改
	auto f12 = [g]()mutable { return ++g; };      // 正确: 捕获的变量g,它在刚开始被捕获的初始值是1
	cout << f12();;    // 调用一次f12之后,变成了2
	cout << f12();     // 调用一次f12之后,变成了3
	cout << g;         // 1(由于是值捕获,所以局部变量g的值一直不会变,最终还将输出1)   

	
	// 如果lambda体不只有单一的return语句,编译器将不能推断lambda的返回类型,必须显式指定lambda返回类型。(然而VS-2019下编译器不需要显式返回类型也能推断,但为了移植性,最好按标准写)
	vector<int> vec1{ 1,-1,-1 };
	vector<int> vec2{ 4,4,4 };
	auto f13 = [](int i) {if (i < 0)return -i; else return i; };       // 作用: 返回i的绝对值 (正确: VS-2019下的编译器不需要显式返回类型也能推断)
	std::transform(vec1.begin(), vec1.end(), vec2.begin(), f13);       // vec2={1,1,1},将第一个范围内的元素的"绝对值",覆盖到第二个范围内的元素
}
建议:尽量减少捕获的数据量,尽可能避免捕获指针或引用。(保不齐你捕获的对象 在我们用的时候不存在了,或者已经 不是我们现在所需要的值了)
注:默认情况下,对于一个值被拷贝的变量,lambda不会改变其值。如果我们希望能改变一个被捕获的变量的值,就必须在 参数列表尾加上关键字mutable。(然而VS-2019下的编译器 不需要显式返回类型也能推断,但为了移植性,最好按标准写

(4) 参数绑定:
  • lambda表达式是适合在 "少数地方使用" 的简单操作。如果是 "很多地方使用" 相同的操作,还是需要定义函数。
  • 如果函数需要传递多个参数,但又有参数个数限制时(如一元谓词),可以使用"lambda表达式",也可以使用"参数绑定"将函数包装成一元谓词。
标准库bind函数:定义在头文件functional中,可以看做为一个通用的函数适配器。
形式:auto newCallable = bind(callable, arg_list);
解释:newCallable为绑定后的可调用对象;callable为目标函数名;arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。
  • 我们调用newCallable的时候,newCallable会调用callable 并传递给它arg_list中的参数。
  • arg_ list中的参数可能包含形如 _n 的名字,其中n是一个整数。这些参数是“占位符",表示newCallable的参数。(如:_1为newCallable的第一个参数,_2 为第二个参数)
  • 名字 _n 都定义在一个名为placeholders的命名空间中,而这个命名空间本身 定义在std命名空间中。(使用这些名字,两个命名空间都要写,如:std::placeholders::_1)
using namespace std::placeholders;      // 为了方便使用_n,直接声明命名空间placeholders

void print_s(std::ostream& os,const string &s) {
	os << s << " ";
}

bool isShorter(const string& str1, const string& str2) {
	return str1.size() < str2.size();
}

void print(int a, int b,int c) {
	cout << a << " " << b << " " << c << endl;
}

int main() {
	// 理解bind函数的使用:
	int a = 1, b = 2, c = 3;
	
	auto cpre = std::bind(print, _1,_2,c);
	cpre(a, b);         // 输出: 1,2,3 (_1对应a,_2对应b。分别为可调用对象cpre的第1,2个参数。调用cpre时将调用cpre(_1,_2),映射为print(_1,_2,c),即print(a,b,c))
	
	auto cpre1 = std::bind(print, _2, _1, c);
	cpre1(a, b);        // 输出: 2,1,3 (_1对应a,_2对应b。分别为可调用对象cpre的第1,2个参数。调用cpre时将调用cpre(_1,_2),映射为print(_2,_1,c),即print(b,a,c))

	
	// 应用: 用bind参数"重排参数顺序":
	vector<string> vec{"a", "ccc", "bb"};
	std::sort(vec.begin(), vec.end(), isShorter);                    // vec={"a","bb","ccc"} (根据isShorter函数,实现从短到长排序)
	std::sort(vec.begin(), vec.end(), bind(isShorter, _2, _1));      // vec={"ccc","bb","a"} (在不改变isShorter函数 或不另外定义一个函数的条件下,实现从长到短排序)


	// 绑定引用参数
	vector<string> vec1{ "aaa","bbb","ccc" };
	std::for_each(vec1.begin(), vec1.end(), bind(print_s, ref(cout), _1));      // 输出: {"aaa","bbb","ccc"}  注: cout不能拷贝!(bind拷贝其参数,而我们不能拷贝一个ostream。如果我们希望传递给bind一个对象而又不拷贝它,就必须使用标准库ref函数)
	std::for_each(vec1.begin(), vec1.end(), bind(print_s, cout, _1));           // 错误: 默认情况下,cout被bind拷贝到 bind返回的可调用对象中,但是cout不允许拷贝
	std::for_each(vec1.begin(), vec1.end(), bind(print_s, &cout, _1));          // 错误: 必须使用ref函数(ref返回一个对象,包含给定的引用,此对象可以拷贝)
}
  • 默认情况下,bind的那些 不是占位符的参数 被拷贝到bind返回的 可调用对象中。但是对有些绑定的参数,我们希望以引用方式传递,或是要绑定参数的类型 无法拷贝,就必须以引用方式传递。
  • 因为bind拷贝其不是占位符的参数。如果我们希望传递给bind一个对象 而又不拷贝它,就必须使用标准库ref函数cref函数定义在头文件functional中。
  • ref函数:返回一个对象,包含给定参数的引用,此对象是可以拷贝的。
  • cref函数:返回一个保存const引用的对象。

10.4 再探迭代器

(1) 插入迭代器:

插入器是一种 迭代器适配器,接受一个容器,生成一个迭代器,能实现向给定容器添加元素。
插入器有三种类型,差异在于元素插入的位置:
  1. back_inserter:创建一个使用 push_back的迭代器。
  2. front_inserter:创建一个使用 push_front的迭代器。
  3. inserter:创建一个使用 insert的迭代器。(除了接受一个容器,还接受一个 指向给定容器的迭代器,元素会被插入到 给定迭代器所指向的元素之前
插入迭代器操作:
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
int main() {
	list<int> lst{ 1,2,3 };
	list<int> lst1{ 1,2,3 };

	auto inserter_iter=std::inserter(lst, lst.begin());      // 返回插入迭代器,该插入迭代器与lst绑定,值永远插在 迭代器lst.begin()指向的元素 之前(也就是插在1之前)
	inserter_iter = 4;        // lst={4,1,2,3} (4插在1之前)
	inserter_iter = 5;        // lst={4,5,1,2,3} (5插在1之前)

	// 其效果与上面代码一样
	auto iter = lst1.insert(lst1.begin(), 4);    // lst1={4,1,2,3} (iter指向新插入的值4)
	iter++;                                      // 通过递增使iter再次指向1
	iter = lst1.insert(iter, 5);                 // lst1={4,5,1,2,3} (注: 传给intsert的第一个参数不能是lst1.begin(),此时lst1.begin()指向4而不是1)


	// front_ inserter生成的迭代器 会将插入的元素序列 的顺序颠倒过来,而inserter和back_ inserter则不会
	list<int> lst2{ 1,2,3,4 };
	list<int> lst3, lst4, lst5;
	std::copy(lst2.begin(), lst2.end(), std::inserter(lst3, lst3.begin()));      // lst3={1,2,3,4}(每次插入的位置 都是在inserter返回的插入迭代器inserter_iter之前,而inserter_iter指向的位置是不变的。最终效果是顺序的)
	std::copy(lst2.begin(), lst2.end(), std::front_inserter(lst4));              // lst4={4,3,2,1}(每次插入的位置 都是在lst4.begin()之前,也就是当前插入元素 永远为lst4的第一个元素。最终效果是逆袭的)
	std::copy(lst2.begin(), lst2.end(), std::back_inserter(lst5));               // lst4={1,2,3,4}(每次插入的位置 都是在lst5.end()之前,也就是当前插入元素 永远为lst5的最后一个元素。最终效果是顺序的)
}
注:
  • front_ inserter生成的迭代器 会将插入的元素序列 的顺序颠倒过来,而inserter和back_ inserter则不会。
  • 只有在容器支持push_front的情况下,我们才可以使用front_inserter;只有在容器支持push_back的情况下,我们才能使用back_inserter。

(2) iostream迭代器:

迭代器可与 输入或输出流 绑定在一起,用于迭代遍历 所关联的IO流。
通过使用流迭代器,我们可以用泛型算法 从流对象中读取数据 以及向其写入数据。
istream_iterator的操作:
istream_iterator<T> in(is);
in从输入流is读取类型为T的值
istream_iterator<T> end;
读取类型是T的值的istream_iterator迭代器,表示尾后位置(默认初始化的迭代器,相对于创建一个可以当作 尾后值 使用的迭代器)
in1 == in2
in1和in2必须读取相同类型。如果他们都是尾后迭代器,或绑定到相同的输入,则两者相等。
in1 != in2
类似上条
*in
返回从流中读取的值
in->mem
与*(in).mem含义相同
++in, in++
使用元素类型所定义的>>运算符从流中读取下一个值。前置版本返回一个指向 递增后迭代器的引用,后置版本返回旧值。
int main() {
	// 从cin对象中读取内容,并输出到"屏幕"上(要通过cout):
	std::istream_iterator<int> is_iter_int(cin);         // 从cin中读取int 到is_iter_int指向的流对象
	cout << *is_iter_int;                                // 返回is_iter_int指向的流对象中 读取的值,并输出


	// 从文件中读取内容,并输出到"屏幕"上(要通过cout):
	std::ifstream in_f("aaa.txt");                            // in_f从 当前项目所在文件夹中的 "aaa.txt"文本文件 中读取数据  (手动去创建这样的一个文件夹,并写入内容。代码创建并写也可以,去看第八章内容,为了方便,不再赘述)
	std::istream_iterator<string> is_iter_string(in_f);       // 从in_f对象中读取string
	cout << *is_iter_string;                                  // 输出的内容为 "aaa.txt"文本文件 中的内容
	in_f.close();

	
	// 使用istream_iterator 从标准输入中读取数据,存入一个vector: (方法一)
	vector<int> vec;
	std::istream_iterator<int> is_end;                   // 相当于istream尾后迭代器 (默认初始化的迭代器,相对于创建一个可以当作 尾后值 使用的迭代器)
	std::istream_iterator<int> is_iter_int1(cin);        // 从cin中读取int 到is_iter_int1指向的流对象 (这里触发第一次cin输入,作用是 初始化is_iter_int1指向的istream对象)

	while (is_iter_int1 != is_end) {          // 按Ctrl+z、Ctrl+d、输入EOF 都能退出循环(Ctrl+z一般用于string; Ctrl+d、输入EOF一般用于int)
		vec.push_back(*is_iter_int1++);       // 后置递增运算 "读取流",返回迭代器的旧值; 解引用迭代器,获得从流读取的前一个值 (后置运算符的优先级 比解引用高)
	}                                         // 每一次递增运算都会触发"读取流",也就触发输入cin,因为读取流和cin绑定(不用纠结为什么会触发,应该是重载了"运算符++")
	

	// 使用istream_iterator 从标准输入中读取数据,存入一个vector: (方法二)
	std::istream_iterator<int> is_end1;            // 相当于istream尾后迭代器
	std::istream_iterator<int> is_iter_int2(cin);

	vector<int> vec1(is_iter_int2, is_end1);       // 我们用 一对表示元素范围的迭代器 来构造vec1(范围构造过程 需要肯定移动迭代器,就会自增,自增就会触发"读取流"。输入EOF后之后,is_iter_int2的值就与尾后迭代器相等,也就退出了"循环")
	

	// 使用算法操作流迭代器:
	std::istream_iterator<int> is_iter_int3(cin), is_end2;

	int counts = std::accumulate(is_iter_int3, is_end2, 0);    // 该调用会计算出 从标准输入读取的值 的和。(原理和上例解释一样)
}
注:
  • 默认初始化的迭代器,相对于创建一个可以当作 尾后值 使用的迭代器。
  • 对于一个绑定到流的迭代器,一旦其关联的流 遇到文件尾或遇到IO错误,迭代器的值就与 尾后迭代器相等。

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。
int main() {
	vector<int> vec{ 1,2,3,4,5 };
	std::ostream_iterator<int>os_iter_int(cout, " ");        // 输出流迭代器os_iter_int 绑定到了cout对象。并且每次使用os_iter_int 向cout写入int值时,后面都会追加一个" "
															 // 特别注意: 也就是因为可以用"<<运算符"实现 cout<<int类型值,要是cout没有对输出的类型 定义<<运算符,就不允许!)
															
	// 用ostream_ iterator来输出值的序列: (推荐写法)												
	for (auto x : vec)	           // 输出: {1 2 3 4 5 }
		*os_iter_int++ = x;        // "我的理解"为: os_iter_int是指向cout对象的迭代器(相当于指针),*os_iter_int=x 就相当于给cout赋值一样(但此时调用cout定义的"<<运算符" 将x写入cout对象,也就是写到屏幕)
		                           // 先调用后置递增,再解引用*,但是解引用* 用的是递增前的旧值。递增就相当于 往后移动更新cout, 接着再次往cout中写入一个值。(注意: 这只是站在 指针角度 去思考问题,指针和迭代器是有差异的,但是迭代器又在模仿指针行为)
									 


	// 等价方式: (但不推荐)
	for (auto x : vec)             // 输出: {1 2 3 4 5 }        
		os_iter_int = x;           // 作用在ostream_iterator上的 *、++运算符 虽然存在,但是返回值都是 迭代器本身 (相当于 *、++ 不做任何事情)。说明这么做是为了 模仿指针行为,
	                               // 好处: (1)在这种写法中,流迭代器的使用与其他迭代器的使用保持一致。 (2)如果想将此循环 改为操作其他迭代器类型,修改起来非常容易。(3)对于读者来说,此循环的行为也更为清晰。


	// 再次优化: (使用泛型算法copy 比循环更简洁)
	std::copy(vec.begin(), vec.end(), os_iter_int);        // 输出: {1 2 3 4 5 }
	                                                       // 总结: 流迭代器就相当于 流对象指针,在对流对象循环操作时 多利用流迭代器使用泛型算法。("流思想": 不要把 用cout来输出内容 仅仅看作输出到窗口。是内容写入了cout流对象,只不过是因为cout绑定了窗口,才最终写到窗口)
} 
注:运算符 *和++ 实际上对ostream_iterator对象 不做任何事情。(虽然忽略它们对我们的程序 没有任何影响,但为了 “一致性” 应该使用)

(3) 反向迭代器:

反向迭代器就是在容器中 从尾元素向首元素反向移动 的迭代器。(对于反向迭代器,递增和递减的操作含义 会颠倒)
可以通过调用 rbegin()rend()crbegin()crend() 成员函数来获得 反向迭代器。(这些函数返回指向容器 尾元素和首元素之前一个位置 的迭代器)

int main() {
	string line{ "abc,def,ghi" };

	// 打印字符串line中的 第一个单词:
	auto comma = find(line.cbegin(), line.cend(), ',');          // comma是指向字符串line中 第一个','的正向迭代器(从前往后找)
	cout << string(line.cbegin(), comma);                        // 输出: "aaa" (打印字符串line中 第一个','之前的内容,即第一个单词"aaa")


	//  打印字符串line中的 最后一个单词:
	auto rcomma = find(line.crbegin(), line.crend(), ',');       // rcomma是指向字符串line中 最后一个','的反向迭代器(从后往前找)

	cout << string(line.crbegin(), rcomma);                      // 错误输出: "ihg" (逆向打印)
	cout << string(rcomma.base(), line.cend());                  // 正确输出: "ghi" (反向迭代器rcomma 通过调用base()成员函数,转换成相应 正向迭代器,从而实现正向打印)        
}                                       
注:
  • 不可能从一个 forward_list或流迭代器 创建反向迭代器。
  • 反向迭代器通过调用 base()成员函数 获得相应的正向迭代器。(转换后的正向迭代器 和原反向迭代器 指向的并不是同一元素)

10.5 泛型算法结构

(1) 5类迭代器:
算法所要求的迭代器操作 可以分为5个迭代器类别 
输入迭代器
只读,不写;单遍扫描,只能递增。              必须支持  ==、!=、++、*、->  运算符
输出迭代器
只写,不读;单遍扫描,只能递增。              必须支持  ++、* 运算符
前向迭代器
可读写;多遍扫描,只能递增。                     必须支持  ==、!=、++、*、->  运算符
双向迭代器
可读写;多遍扫描,可递增递减。                 必须支持  ==、!=、++、--、*、->  运算符
随机访问迭代器
可读写,多遍扫描,支持全部迭代器运算。   必须支持  ==、!=、<、<=、>、>=、++、--、+、+=、-、-=、*、->  运算符。iter[n] 等价于 *(iter[n])

(2) 算法的形参模式:
  • 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都是迭代器参数,是否使用 要依赖于执行的操作。
注:接受单独beg2的算法假定 从beg2开始的序列与beg和end所表示的范围 至少一样大。

(3) 算法命名规范:
  • 一些算法使用重载形式传递一个谓词。
  • 接受一个元素值的算法 通常有一个不同名的版本。(如:加_if,接受一个谓词代替元素值)
  • 区分拷贝元素的版本和不拷贝的版本:拷贝版本通常加_copy。
nique(beg, end);                     // 使用==运算符比较元素
unique(beg, end, comp);              // 使用comp比较元素

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


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

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

10.6 特定容器算法

list和forward_list的 成员函数版本的算法:
lst.merge(lst2)
将来自lst2的元素合并入lst,二者都必须是有序的。元素将从lst2中删除,合并之后,lst2变为空。
lst.merge(lst2, comp)
同上,使用给定比较操作。
lst.remove(val)
调用erase删除掉 与给定值相等(==) 的每个元素。
lst.remove_if(pred)
调用erase删除掉 令一元谓词为真 的每个元素。
lst.reverse()
反转lst中元素的顺序
lst.sort()
使用<排序元素
lst.sort(comp)
同上,使用给定比较操作。
lst.unique()
调用erase删除 同一个值的连续拷贝。使用==。
lst.unique(pred)
同上,使用给定的二元谓词。
注:对于list和forward_list,优先使用成员函数版本的算法 而不是通用算法。上面的操作都返回void


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或flst相同的链表。
(p, lst2, b, e)
同上,b和e表示lst2中的合法范围。将给定范围中的元素 从lst2移动到lst或first中。lst2可以是 与lst或flst相同的链表,但p不能指向 给定范围中的元素。
注:链表特有版本与通用版本间的 一个至关重要的区别是 链表版本会改变底层的容器。(如:unique的链表版本 会删除重复的元素)



C++primer 读书笔记 文章被收录于专栏

C++primer精总!

全部评论

相关推荐

点赞 收藏 评论
分享
牛客网
牛客企业服务