10.1 泛型算法概述
本章提到的泛型算法:
find | InputIt find( InputIt first, InputIt last, const T& value ); | first,last ,value分别为 两个标记范围的迭代器、目标查找值。 返回值:如果找到给定目标值,返回对应的迭代器,否则返回第二个参数,即标记结尾的迭代器。 |
accumulate | T 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:
用来确保算法有足够的空间存储数据。
(3) 重排容器元素的算法:
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) 插入迭代器:
插入器是一种 迭代器适配器,接受一个容器,生成一个迭代器,能实现向给定容器添加元素。
插入器有三种类型,差异在于元素插入的位置:
- back_inserter:创建一个使用 push_back的迭代器。
- front_inserter:创建一个使用 push_front的迭代器。
- 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<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所表示的范围 至少一样大。
- 一些算法使用重载形式传递一个谓词。
- 接受一个元素值的算法 通常有一个不同名的版本。(如:加_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的 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不能指向 给定范围中的元素。 |