10 泛型算法

目录

0. 概述

  • 顺序容器只定义了很少的操作:
    • 在多数情况下,我们可以添加和删除元素、访问首尾元素、确定容器是否为空以及获得指向首元素或尾元素之后位置的迭代器。
  • 如果我们想要做:
    • 查找特定元素、替换或删除一个特定值、重排元素顺序等。
  • 标准库并未给每个容器都定义成员函数来实现这些操作,而是定义了一组泛型算法
    • 称它们为“算法”,是因为它们实现了一些经典算法的公共接口,如排序和搜索;
    • 称它们是“泛型的”,是因为它们可用于不同类型的元素和多种容器类型(不仅包括标准库类型,如vector或list,还包括内置的数组类型)。
  • 泛型算法:一些经典算法的公共接口,用于不同类型元素、多种容器类型的排序、搜索
  • 泛型算法不会影响容器的大小,不能直接删除、添加元素,及算法不能执行容器操作;只能使用插入迭代器间接添加元素
  • 算法库对其所有在容器上的操作有约束:决不修改容器的大小(不插入、不删除)
alg(beg,end,other args);
alg(beg,end,dest,other args);
alg(beg,end,beg2,other args);
alg(beg,end,beg2,end2,other args);
  • 其中,dest为目标迭代器:
    • (1)直接指向容器的迭代器,那么算法将输出数据写到容器中已存在的元素内。
    • (2)更常见的情况是,dest被绑定到一个插入迭代器或是一个ostream_iterator。
      • 插入迭代器会将新元素添加到容器中,因而保证空间是足够的;
      • ostream_iterator 会将数据写入一个输出流,同样不管要写多少个元素都没有问题。
  • other args:自定义比较函数——一元或二元谓词/lambda表达式/bind函数

1. 泛型算法概述

  • 头文件
    • 大多数算法都定义在头文件algorithm中
    • 标准库还在头文件numeric中定义了一组数值泛型算法
  • 操作对象:迭代器
    • 一般情况下,这些算法并不直接操作容器,而是遍历由两个迭代器指定的一个元素范围来进行操作。通常情况下,算法遍历范围,对其中每个元素进行一些处理。
  • 算法如何工作:如何用于不同类型的容器——算法工作时不依赖于容器类型,也不依赖于容器所保存的元素类型
    • 以find为例。find的工作是在一个未排序的元素序列中查找一个特定的元素。概念上,find应该执行如下步骤:
    • 1 访问序列中的首元素
    • 2 比较此元素与我们要查找的值
    • 3 如果此元素与我们要查找的值匹配,find返回标识此元素的值。
    • 4 否则,find前进到下一个元素,重复执行步骤2和3
    • 5 如果到达序列尾,find应停止
    • 6 如果find到达序列尾,它应该返回一个指出元素未找到的值。此值和步骤3返回的值必须具有相容的类型。
    • 这些步骤都不依赖于容器所保存的元素类型。因此,只要有一个迭代器可用来访问元素,find就完全不依赖于容器类型(甚至无须理会保存元素的是不是容器)。
  • 迭代器令算法不依赖于容器,但算法依赖于元素类型的操作
    • 大多数算法都使用了一个元素类型上的操作, 如==, !=等, 这些操作需要元素类型的支持.
    • 不过大多数算法提供了一种方法, 允许我们使用自定义的操作来代替默认的运算符。
  • 注意:泛型算法永远不会执行容器的操作
    • 泛型算法本身不会执行容器的操作,它们只会运行于迭代器之上,执行迭代器的操作。
    • 泛型算法运行于迭代器之上而不会执行容器操作的特性保证了:
      • 泛型算法永远不会改变底层容器的大小
      • 可能改变容器中保存的元素的值,也可能在容器内移动元素,但永远不会直接添加或删除元素

2. 初始泛型算法

  • 除了少数例外,标准库算法都对一个范围内的元素进行操作。我们将此元素范围称为“输入范围”。
  • 接受输入范围的算法总是使用前两个参数来表示此范围[ ),两个参数分别是指想要处理的第一个元素和尾元素之后位置的迭代器。
  • 理解算法的最基本的方法就是了解它们是否读取元素、改变元素、或是重排元素顺序。

2.1 只读算法:只会读取其输入范围内的元素,而从不改变元素

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

find搜素算法:操作单序列,搜索特定值

  • find(迭代器1,迭代器2,目标值)
  • 传递给find的前两个参数是表示元素范围的迭代器,第三个参数是一个值。
  • find将范围中每个元素与给定值进行比较。它返回指向第一个等于给定值的元素的迭代器。
    • 如果范围中无匹配元素,则find返回第二个参数来表示搜索失败
    • 因此,我们可以通过比较返回值和第二个参数来判断搜索是否成功
// 假定我们有一个int的vector,希望指定vector中是否包含一个特定值
int val=42;
auto result=find(vec.cbegin(),vec.cend(),val);

// 用find在一个string的list中查找一个给定值
string val="a value";
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。

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

accumulate算法:操作单序列,求和(算术求和,连接字符串)

  • accumulata(迭代器1,迭代器2,求和初值),定义在头文件numeric中。

    • accumulate函数接受三个参数,前两个指出了需要求和的元素的范围,第三个参数是和的初值。
    • 第三个参数的类型决定了函数中使用哪个加法运算符以及返回值的类型。第三个参数类型定义了“+”运算的类型才可以使用accumulate
    • 序列中元素的类型必须与第三个参数匹配,或者能够转换为第三个参数的类型。如下例中,vec中的元素可以是int,或者是double、long long或任何其他可以加到int上的类型。
  • 算术求和

// 假定vec是一个整数序列,则:
int val = 42;
vector<int> ivec{1, 2, 3, 42, 4};
// accumulate定义在numeric头文件中, 对ivec中元素求和
int sum = accumulate(ivec.cbegin(), ivec.cend(), 0); 
// 第三个参数决定使用那个加法运算符以及返回值类型
```C++
- 连接字符串
```C++
// 由于string定义了+运算符,我们可以通过调用accumulate来将vector中所有string元素连接起来:
vector<string> svec{"hello", "world"};
string sum2 = accumulate(svec.cbegin(), svec.cend(), string("")); 
// string定义了+操作, 所以可以用该函数把string连起来; 第三个参数显式地创建了一个string。
  • 将空串当做一个字符串字面值传递给第三个参数是不可以的,会导致一个编译错误
//错误:const char*上没有定义+运算符
string sum=accumulate(v.cbegin(),v.cend(),"");
// 原因在于,如果我们传递了一个字符串字面值,用于保存和的对象的类型将是const char*。如
// 前所述,此类型决定了使用哪个+运算符。由于const char*并没有+运算符,此调用将产生编译错误。

equal算法:操作两个序列,求等长类型相等否—

  • equal(序列1首迭代器,序列1尾后迭代器,序列2首迭代器)
  • 用于确定两个序列是否保存相同的值,它将第一个序列中的每个元素与第二个序列中的对应元素进行比较。如果所有对应元素都相等,则返回true,否则返回false。
  • 算法假设两个序列一样长,在第二个序列中取出相同长度和第一个序列比较;
  • 两个序列容器类型可以不同,元素类型也可以不同,只要能用 == 比较就行
  • 此算法接受三个迭代器:前两个表示第一个序列中的元素的范围,第三个表示第二个序列的首元素:
//roster2中的元素数目应该至少与roster1一样多
equal(roster1.cbegin(),roster1.cend(),roster2.cbegin());

操作两个序列的算法要求

  • 那些只接受一个单一迭代器来表示第二个序列的算法,都假定第二个序列至少与第一个序列一样长
  • 操作两个序列的算法要求
    • 两个序列的元素可以来自不同容器,可以是不同元素类型
      • 一些算法从两个序列中读取元素。构成这两个序列的元素可以来自于不同类型的容器。例如,第一个序列可能保存于一个vector中,而第二个序列可能保存于一个list、deque、内置数组或其它容器中。而且,两个序列中元素的类型也不要求严格匹配。算法要求的只是能够比较两个序列中的元素。例如,对equal算法,元素类型不要求相同,但是我们必须能使用==来比较来自两个序列中的元素。
    • 操作两个序列的算法之间的区别在于如何传递第二个序列。
      • 1)接受三个迭代器:前两个表示一个序列的范围,第三个表示第二个序列中的首元素。如equal。用一个单一迭代器表示第二个序列的算法都假定第二个序列至少与第一个一样长。确保算法不会试图访问第二个序列中不存在的元素是程序员的责任。
      • 2)接受四个迭代器:前两个表示第一个序列的元素范围,后两个表示第二个序列的范围

2.3 back_inserter函数:返回插入迭代器,常与写容器元素算法配合使用的函数,使得标准库算法可以间接添加元素

  • back_inserter函数与写容器元素算法配合使用,使得标准库算法可以间接添加元素。
    • 因为标准库算法对迭代器而不是容器进行操作,因此算法不能(直接)添加或删除元素
  • back_inserter函数:返回插入迭代器,常作为算法的目的位置dest来使用,使得写入算法可以添加元素。
    • 操作两个序列的写算法一般对作为第三参数的目标容器进行赋值操作,此时我们可以将插入迭代器代替代表目标容器的第三参数来实现对目标容器的增长功能(即对容器1的操作结果添加到目标容器2尾部)
  • back_inserte是定义在头文件iterator中的一个函数。
  • 一种保证算法有足够元素空间来容纳输出数据的方法是使用插入迭代器(insert iterator)。插入迭代器是一种向容器中添加元素的迭代器。
  • 普通迭代器 VS 插入迭代器
    • 通常情况下,当我们通过一个迭代器向容器元素赋值时,值被赋予迭代器指向的元素
    • 而当我们通过一个插入迭代器赋值时,一个与赋值号右侧值相等的元素被添加到容器中
  • back_inserter接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器
    • 当我们通过插入迭代器赋值时,赋值运算符会调用push_back将一个具有给定值的元素添加到容器中
vector<int> vec;   //空容器
auto it=back_inserter(vec); //返回插入迭代器it,通过它赋值会将元素添加到vec中
*it=42;//vec现在有一个元素,值为42
  • 我们常常使用back_inserter来创建一个迭代器,作为算法的目的位置来使用。例如:
vector<int> vec;  //空向量
auto it=back_inserter(vec); //返回插入迭代器it
//正确:back_inserter创建一个插入迭代器,可以用来向vec添加元素
fill_n(back_inserter(vec),10,0);  //添加10个元素到vec
/*
在每步迭代中,fill_n向给定容器序列的一个元素赋值。
由于我们传递的参数是back_inserter返回的迭代器,因此每次赋值都会在vec上调用push_back。
最终,这条fill_n调用语句向vec的末尾添加了10个元素,每个元素的值都是0
*/

2.4 写容器元素的算法

  • 必须注意确保序列原大小至少不小于我们要求算法写入的元素数目。记住,算法不会执行容器操作,因此它们自身不可能改变容器的大小。它们最多写入与给定序列一样多的元素。
  • 插入迭代器的主要功能:把一个赋值操作转换为把相应的值插入容器的操作(通过调用容器的push_back操作)
  • 算法库对所有在容器上的操作有约束:决不修改容器的大小(不插入、不删除)。有了插入迭代器,既使得算法库可以通过迭代器对容器插入新的元素,又不违反这一统带,即保持了设计上的一致性
  • 多个算法都提供所谓的“拷贝”版本,不对原序列本身做改变,而是返回改变后的新序列
    • 非拷贝版本在原序列基础上修改,拷贝版本返回改变后的新序列

fill算法:1换n,全换值

  • fill(beg, end, 重置值)
  • fill接受一对迭代器表示一个范围,还接受一个值作为第三个参数。fill将给定的这个值赋予输入序列中的每个元素。
fill(vec.begin(),vec.end(),0); //将迭代器范围的每个元素值重置为0
//将容器的一个子序列设置为10
fill(vec.begin(),vec.begin()+vec.size()/2,10);

fill_n算法:1换n,部分换值

  • 函数fill_n接受一个单迭代器、一个计数值和一个值。它将给定值赋予迭代器指向的元素开始的指定个元素。
  • fill_n(dest,n,val):fill_n(beg, 前几个值重置,重置值)
    • 函数fill_n假定dest指向一个元素,而从dest开始的序列至少包含n个元素。
vector<int> vec;  //空vector
//使用vec,赋予它不同值
fill_n(vec.begin(),vec.size(),0);  //将所有元素重置为0
  • 一个初学者非常容易犯的错误是在一个空容器上调用fill_n(或类似的写元素的算法)——因为不能改变容器大小
vector<int> vec;  //空向量
//灾难:修改vec中10个(不存在)元素
fill_n(vec.begin(),10,0);
// 这个调用是一场灾难,我们指定了要写入10个元素,但vec中并没有元素——它是空的,这条语句的结果是未定义的。
  • 向目的迭代器写入数据的算法假定目的位置是足够大,能容纳要写入的元素

copy算法:拷贝,n换n

  • copy(beg1, end1, beg2),返回指向拷贝到a2的尾元素之后的位置
  • 此算法接受三个迭代器:前两个表示一个输入范围,第三个表示目的序列的起始位置。此算法将输入范围中的元素拷贝到目的序列中。
  • copy返回指向拷贝到目标容器的尾元素之后的位置的迭代器。
  • 注意:传递给copy的目的序列至少要包含与输入序列一样多的元素,这一点很重要。
  • 可以用copy实现内置数组的拷贝:
int a[]={0,1,2,3,4,5,6,7,8,9};
int a2[sizeof(a1)/sizeof(*a1)];  // a2与a1大小一样
auto ret=copy(begin(a1),end(a1),a2); //把a1的内容拷贝到a2,返回ret,ret指向拷贝到a2的尾元素之后的位置

replace算法:替换序列某元素,原序列本身改变

  • replace(beg, end, 范围内某值a,替换a的新b)
  • replace算法读入一个序列,并将其中所有等于给定值的元素都改为另一个值
  • 此算法接受4个参数:前两个是迭代器,表示输入序列,后两个一个是要搜索的值,另一个是新值。它将所有等于第一个值的元素替换为第二个值:
replace(ilist.begin(),ilist.end(),0,42);  //将范围内所有值为0的元素改为42

replace_copy算法:替换序列某元素,原序列本身不变

  • replace_copy(beg1, end1, 对序列操作结果保存位置,范围内某值a,替换a的新b)
  • 此算法接受额外第三个迭代器参数,指出调整后序列的保存位置:
//使用back_inserter按需要增长目标序列			replace_copy(ilist.begin(),ilist.end(),back_inserter(ivec),0,42);
// 此调用后,ilis并未改变,ivec包含与ilist的一份拷贝,不过原来在ilist中值为0的元素在ivec中都变为42

2.5 重排容器元素的算法:重排容器中元素的顺序

sort算法

  • sort(beg, end) :范围内从小到大排序,利用元素类型的 < 运算符来实现排序的

unique算法

  • unique(beg, end):一般用于sort排序后的序列,将重复的元素排在后面,返回指向第一个重复元素所在位置的迭代器。常作为容器erase操作的参数来进行重复元素的删除
  • 例子:消除重复单词——sort、unique和容器erase操作配合使用
    • 使用sort:为了消除重复单词,首先将vector排序,使得重复的单词都相邻出现。
    • 使用unique:一旦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删除重复单词
    //删除从end_unique开始直至words末尾的范围内的所有元素
    words.erase(end_unique,words.end());
}
/*
注意:即使words中没有重复单词,这样调用erase也是安全的。
在此情况下,unique会返回words.end()。因此,传递给erase的两个参数具有相同的值:words.end()。
迭代器相等意味着传递给erase的元素范围为空。
删除一个空范围没有什么不良后果,因此程序中即使在输入元素中无重复元素的情况下也是正确的。
*/
  • 删除一个空范围没有什么不良后果

3. 定制操作:传递函数或lambda表达式,为算法定制自己的操作

  • 很多算法都会比较输入序列中的元素。默认情况下,这类算法使用元素类型的<或==运算符完成比较。标准库还为这些算法定义了额外的版本,允许我们提供自己定义的操作来代替默认运算符。
  • 例如,sort算法默认使用元素类型的<运算符。但在以下两种情况下,都需要重载sort的默认行为。
    • 我们希望的排序顺序与<所定义的顺序(从小到大)不同,比如从大到小排序、单词长度排序
    • 我们的序列可能保存的是未定义<运算符的元素类型(如Sales_data).
  • 算法(beg, end, 通过谓词或lambda表达式的定制操作)
  • 当你需要按照某种特定方式进行排序时,你需要给sort指定比较函数,否则程序会自动提供给你一个默认比较函数

3.1 向算法传递函数:用谓词指定算法的比较函数(1或2个参数)

  • 用谓词指定算法的比较函数(1或2个参数):算法一般只能接受一元或二元谓词谓词实际上就是一个一元或二元参数函数
    • 因为算法只能将容器迭代器范围的一个元素或两个元素作为参数送进比较函数中
  • 我们自己指定算法的比较函数,需要用到sort的重载版本,它接受三个参数,第三个参数是一个谓词。
  • 谓词:一个可调用的表达式,其返回结果是一个能用作条件的值
  • 标准库算法所使用的谓词分为两类:
    • 一元谓词(意味着它们只接受单一参数)
    • 二元谓词(意味着它们接受两个参数):两两比较
  • 接受谓词参数的算法对输入序列中的元素调用谓词。因此,元素类型必须能转换为谓词的参数类型。
  • 使用二元谓词的sort算法:sort(beg, end, 谓词),接受一个二元谓词参数的sort版本用这个谓词代替<来比较元素。
//比较函数,用来比较长度排序单词
bool isShorter(const string &s1,const string &s2)
{	
    return s1.size()<s2.size();
}
//按长度由短至长排序words
sort(words.begin(),words.end(),isShorter);
  • 使用二元谓词的stable_sort稳定排序算法
    • stable_sort(beg, end, 谓词),维持相等元素的原有顺序
    • 即两相等元素a和b,排序前a在b前面,排序后即使二者相等,也保证a在b前面(但sort()方法不保证)
  • C++中sort和stable_sort的区别:
    • sort是快速排序实现,因此是不稳定的;stable_sort是归并排序实现,因此是稳定的;(什么叫不稳定,有两个相同的数A和B,在排序之前A在B的前面,而经过排序之后,B跑到了A的前面,对于这种情况的发生,我们管他叫做排序的不稳定性,自己想一下快速排序的思路就知道为啥不稳定了)
    • 对于相等的元素sort可能改变顺序,stable_sort保证排序后相等的元素次序不变;
    • 如果提供了比较函数,sort不要求比较函数的参数被限定为const,而stable_sort则要求参数被限定为const,否则编译不能通过。
elimDups(words);//将words按字典序重排,并消除重复单词
//按长度重新排序,长度相同的单词维持字典序
stable_sort(words.begin(),words.end(),isShorter);
for(const auto &s:words)
    cout<<s<<" ";
cout<<endl;
  • 使用一元谓词的find_if查找算法:find_if(beg, end, 谓词) 查找第一个具有特定大小的元素
    • find_if算法对输入序列中的每个元素调用给定的这个谓词。它返回第一个使谓词返回非0值的元素,如果不存在这样的元素,则返回尾迭代器。

3.2 lambda表达式:给算法指定更复杂的比较函数(允许多参数)

  • lambda表达式:给算法指定更复杂的比较函数
    • 允许多参数,即lambda除了算法送进来的容器的一个或两个元素作为参数,还可以通过[捕获列表]捕获外部变量的方式拓展参数数目,空捕获列表[]就相当于谓词函数
  • 根据算法接受一元谓词还是二元谓词,我们传递给算法的谓词必须严格接受一个或两个参数。但是,有时我们希望进行的操作需要更多参数,超出了算法对谓词的限制——使用lambda表达式替代
  • 我们可以向一个算法传递任何类别的可调用对象。
  • 对于一个对象或一个表达式,如果可以对其使用调用运算符,则称它为可调用的。即,如果e是一个可调用的表达式,则我们可以编写代码e(args),其中args是一个逗号分隔的一个或多个参数的列表。
  • 到目前为止,我们使用过的仅有的两种可调用对象是函数和函数指针。还有其他两种可调用对象:重载了函数调用运算符的类,以及lambda表达式。
  • 可调用对象:
    • 函数
    • 函数指针
    • 重载了函数调用运算符的类
    • lambda表达式

lambda表达式(匿名函数)

  • 一个lambda表达式表示一个可调用的代码单元。我们可以将其理解为一个未命名的内联函数。与任何函数类似,一个lambda具有一个返回返回类型、一个参数列表和一个函数体。但与函数不同,lambda可能定义在函数内部。
  • 格式1:[capture list] (parameter list) ->return type
  • 格式2:[capture list] (params list) mutable exception-> return type
    • [捕获] (形参) mutable 或 exception 声明 -> 返回值类型
    • capture list:捕获(外部变量)列表,通常为空——不可省略
    • params list:形参列表——可省
    • mutable指示符:用来说用是否可以修改捕获的变量——可省
    • exception:异常设定——可省
    • return type:返回类型——可省
    • function body:函数体——不可省略
  • [capture list]:捕获外部变量列表(通常为空)
    • 标识一个 Lambda 表达式的开始,这部分必须存在,不能省略
    • []: 常常也被称为引入符号(lambda-introduct).
      • 声明当前是个lambda表达式
      • 捕获列表:使得lambda可以访问外部变量
    • 有以下形式:
捕获形式 说明
[] 空捕获列表,即不捕获任何外部变量。此时lambda不能访问外部变量。
[变量名, …] 默认以值的形式捕获指定的多个外部变量(用逗号分隔),如果引用捕获,需要显示声明(使用&说明符)
[this] 以值的形式捕获this指针。函数体内可以使用 Lambda 所在类中的成员变量
[&] 以值的形式捕获所有外部变量
[=] 以引用形式捕获所有外部变量
[=, &x] 变量x以引用形式捕获,其余变量以传值形式捕获
[&, x] 变量x以值的形式捕获,其余变量以引用形式捕获
  • (params list) 形参列表
    • 没有参数时,这部分可以省略。参数可以通过按值(如 (a, b))和按引用 (如 (&a, &b)) 两种方式进行传递。
    • 与普通函数调用类似,调用一个lambda时给定的实参类型被用来初始化lambda的形参
    • 通常,实参和形参的类型必须匹配,但与普通函数不同,lambda不能有默认参数。因此,一个lambda调用的实参数目永远与形参数目相等。一旦形参初始化完毕,就可以执行函数体了。
  • mutable 或 exception 声明
    • 这部分可以省略。按值传递函数对象参数时,加上 mutable 修饰符后,可以修改传递进来的拷贝(注意是能修改拷贝,而不是值本身)。
    • exception 声明用于指定函数抛出的异常,如抛出整数类型的异常,可以使用 throw(int)。
  • -> 返回值类型:尾置返回类型
    • 标识函数返回值的类型,当返回值为 void,或者函数体中只有一处 return 的地方(此时编译器可以自动推断出返回值类型)时,这部分可以省略。
    • 如果你的函数体内只有一个return语句,可以完全不用写尾置返回类型->return_type
    • 如果lambda的函数体包含任何单一return语句之外的语句,且未指定返回类型,则返回void。
  • lambda不写尾置返回类型即->return_type时
    • 函数体内只有一个return语句:返回类型从返回的表达式的类型推断而来
    • 其他情况:如果一个lambda体包含return之外的任何语句
      • 编译器默认此lambda返回类型为void。与其它返回void的函数类型类似,被推断返回void的lambda不能返回值。如果你函数体内有return语句不是返回void就会报错
      • 要返回其他类型:必须使用尾置返回类型->return_type
// 使用标准库transform算法和一个lambda来将一个序列中的每个负数替换为其绝对值:					transform(vi.begin(),vi.end(),vi.begin(), [] (int i) {return i<0?-i:i;});
// 但是,如果我们将程序改写为看起来是等价的if语句,就会产生编译错误:
// 错误:不能推断lambda的返回类型
transform(vi.begin(),vi.end(),vi.begin(),[] (int i) {if(i<0) return -i; else return i;});

// 当我们需要为一个lambda定义返回类型时,必须使用尾置返回类型:
transform(vi.begin(),vi.end(),vi.begin(),[] (int i)->int  {if(i<0) return -i; else return i;});
  • {函数体}
    • 标识函数的实现,这部分不能省略,但函数体可以为空。
  • []示例:不能使用外部变量.
[] (int x, int y) { return x + y;} // 隐式返回类型
[] (int& x) { ++x;} // 没有 return 语句 -> Lambda 函数的返回类型是 'void'
[] () { ++global_x;} // 没有参数,仅访问某个全局变量
[] { ++global_x;} // 与上一个相同,省略了 (形参)

/// 如果使用外部变量会报错:
// 错误:sz未捕获
[] (const string &s);
{ return s.size()>=sz;};
  • [=]值捕获:在lambda创建时以传值方式传递外部所有变量,不能在lambda函数内部修改外部变量,否则会报错(外部修改不影响内部,内部不能修改
// 与传值参数类似,采用值捕获的前提是变量可以拷贝。
// 与参数不同,被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝
int id=2,id1=3;
auto f = [=]() {       // 这里加不加()都可以
    // id = 1; // 报错,内部不能修改
    cout << "id=" << id << endl;    // 2
    cout << "id1=" << id1 << endl;  // 3
};
id=3;
f();
/*				
前面id被修改为3,为何输出没变——因为是传值,外部修改对f函数没影响,所以id仍旧是2,要想修改 用引用传入.
*/
  • 添加mutable可以在lambda内部修改外部变量,但是当lambad函数结束后,变量仍旧恢复(外部修改不影响内部,可以内部修改且内部修改也不影响外部
    • 如果lambda内部修改了变量,但是并没有使用mutable关键字,报错:error: increment of read-only variable ‘id’
    • 使用mutable后, 前面必须添加()
    • 内部可以对外部变量修改,但是跳出lambda,并不会影响外部,如果要影响,必须传引用.
// 示例1:
int id=2,id1=3;
auto f = [=]()mutable{
    id--;
    cout << "id=" << id << endl;    // 1
    cout << "id1=" << id1 << endl;  // 3
};
id=3;
f();
cout << "id=" << id << endl;  //3
					
// 示例2:
int id=2,id1=3;
auto f = [=]()mutable{
    id--;
    cout << "id=" << id << endl;    // 1
    cout << "id1=" << id1 << endl;  // 3
};
f();
cout << "id=" << id << endl;  //2
  • [&]引用捕获:在lambda创建时以传引用方式传递外部所有变量,可以在lambda内部修改外部变量(外部修改影响内部,可以内部修改且内部修改也影响外部。传递引用可以不加mutable关键字
    • 当以引用方式捕获一个变量时,必须保证在lambda执行时变量是存在的。
    • lambda捕获的都是局部变量,这些变量在lambda所在的外部函数结束后就不复存在了。如果lambda可能在函数结束后执行,捕获的引用指向的局部变量已经消失。
// 示例1
int id=2,id1=3;
auto f = [&](){
    cout << "id=" << id << endl;    // 3
    cout << "id1=" << id1 << endl;  // 3
};
id=3;
f();
cout << "id=" << id << endl;    // 3

// 示例2
int id=2,id1=3;
auto f = [&](){
    id++;
    cout << "id=" << id << endl;    // 4
    cout << "id1=" << id1 << endl;  // 3
};
id=3;
f();
cout << "id=" << id << endl;    // 4
  • [this]:对于[=]与[&], 可以直接使用this指针,而对于[]则需要显示[this]
class X {
private:
    int __x, __y;
public:
    X(int x, int y) : __x(x), __y(y) {}

    int operator()(int a) { return a; }

    int f() {
        // 下列 lambda 的语境是成员函数 X::f
        // 对于[=]或[&]的形式,lambda 表达式可以直接使用 this 指针
        return [&]() -> int {
            return operator()(this->__x + __y); // X::operator()(this->x + (*this).y)
            // 拥有类型 X*
        }();
    }


    int ff() {
        return [this]() {
            return this->__x;
        }();
    }
};

调用:
// this 指针
X x_(1, 2);
cout << "x_.f()=" << x_.f() << endl;   // 1+2=3
cout << "x_.ff()=" << x_.ff() << endl; // 1

lambda使用场景

  • 直接使用,对于一些不重复使用的函数,使用完了自动丢弃
int result = [](int a) {return a*a; }(10);
cout << result << endl;
  • 如果要反复利用,可以使用变量来存储
auto pow_2 = [](int a) {return a*a; };
cout << pow_2(10) << endl;
  • lambda的类型
cout << typeid(pow_2).name() << endl;   //类型是class <lambda_a10bdd2a3443eccb
  • 在sort算法中使用lambda表达式:sort(beg, end, lambda表达式)
// 编写一个与isShorter函数完成相同功能的lambda:
[] (const string &s1,const string &s2)
{ return s1.size()<s2.size();}

// 使用此lambda来调用stable_sort;
stable_sort(words.begin(),words.end(),[](const string &s1,const string &s2)
{return s1.size()<s2.size();});
// 当stable_sort需要比较两个元素时,它就会调用给定的这个lambda表达式。
  • 在find_if算法中使用lambda表达式:find_if(beg, end, lambda表达式)
// 查找第一个长度大于等于sz的元素:
// 获取一个迭代器,指向第一个满足size()>=sz的元素
auto wc=find_if(words.begin(),words.end(),[sz] (const string &s) {return s.size()>=sz;});
/*
这里对find_if的调用返回一个迭代器,指向第一个长度不小于给定参数sz的元素。
如果这样的元素不存在,则返回words.end()的一个拷贝。
我们可以使用find_if返回的迭代器来计算从它开始到words的末尾一共有多少个元素:
*/
//计算满足size>=sz的元素的数目
auto count=words.end() -wc;

3.3 参数绑定:用bind函数设法拓展算法的谓词函数的参数数量(如果要重复使用比较函数而不方便用lambda时)

  • 用bind函数实现和lambda一样的功能:
    • bind(比较函数,_1, (_2), 外部变量1,…) = 外部变量1->return_type{}
    • 其中_1和_2接受算法自己送进来的参数。_1代表一元谓词,_1和_2表示二元谓词
  • 对于那种只有一两个地方使用的简单操作,lambda表达式是最有用的。如果我们需要在很多地方使用相同的操作,通常应该定义一个函数,而不是多次编写相同的lambda表达式。类似的,如果一个操作需要很多语句才能完成,通常使用函数更好

标准库bind函数:生成一个可调用对象, 实现和lambda一样的功能,即设法传入lambda[捕获列表]的外部变量

  • bind函数:定义在头文件functional中,可以将bind函数看作一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。
  • 调用bind的一般形式:
    • auto newCallable=bind(callable,arg_list);
    • newCallable为一个新可调用对象,callable为原可调用对象
    • 当我们调用newCallable时,newCallable会调用callable,并传递给它arg_list中的参数。
    • arg_list参数列表:实参列表,以逗号分隔,对应给定的callable的参数。
      • 参数列表包含 _n 占位符和其他参数,对应原可调用对象callable的参数。
      • 形如_n的参数:其中n是一个整数。这些参数是“占位符”,表示新可调用对象newCallable要传进来的参数,有多少个占位符就表示新调用对象newCallable有多少个参数。数值n表示生成的新可调用对象newCallable中参数的位置:_1为newCallable的第一个参数,_2为第二个参数,以此类推。
      • _1,_2...:无论出现在什么位置,都是根据数字来表示这是newCallable的第几个参数
      • _1,_2等占位符定义在std::placeholders命名空间中,所以要使用:using namespace std::placeholders;
      • 其他参数:为原调用对象callable的参数,或者说函数体所需的外部变量实参,等价于lambda表达式的[捕获外部变量列表]
    • arg_list中参数的位置顺序即调用Callable的参数顺序
// 示例1
auto newCall=bind(check_size,_1,sz);
/*
bind(check_size,_1,sz)返回一个新的函数newCall。
由于只有一个占位符_1,newCall只接受一个参数。
占位符出现在arg_list的第一个位置,表示新调用对象newCall的此参数对应旧调用对象check_size的第一个参数。
新调用对象newCall是接受一个参数的函数;而旧调用对象check_size是接受两个参数的函数,只是提前(将第二个参数)绑定了外部变量sz
当调用了newCall(s),实际是调用了check_size(s, sz)
*/
string s="hello";                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        
bool b1=newCall(s); //newCall(s)会调用check_size(s, sz)
  • 如果lambda的捕获列表为空,通常可以用(谓词)函数来代替它
  • 对于捕获外部变量的lambda(捕获列表不为空),用函数来替换它就不是那么容易,可以用bind函数来等效。
// 在find_if调用中的lambda比较一个string和一个给定大小sz:
// 查找第一个长度大于等于sz的元素:
// 获取一个迭代器,指向第一个满足size()>=sz的元素
auto wc=find_if(words.begin(),words.end(),[sz] (const string &s) {return s.size()>=sz;});

// 我们可以很容易地编写一个完成同样工作的函数:
bool check_size(const string &s,string::size_type sz)
{
    return s.size() >=sz;
}
/*
但是,我们不能用这个函数作为find_if的一个参数。
因为find_if接受一个一元谓词,因此传递给find_if的可调用对象必须只接受单一参数。
find_if的lambda使用捕获列表来保存sz。
为了用check_size来代替此lambda,我们使用bind函数绑定check_size的sz参数:

使用bind,我们可以将原来基于lambda的find_if调用:
auto wc=find_if(words.begin(),words.end(),[sz](const string &s)
替换为如下使用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传递给它。
因此,find_if可以有效地对输入序列中每个string调用check_size,实现string的大小与sz的比较。
*/
  • 占位符_n都定义在placeholders命名空间
    • 名字_n都定义在一个名为placeholders的命名空间中,而这个命名空间本身定义在std命名空间中。为了使用这些名字,两个命名空间都要写上。
    • 例如,_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头文件中。
// 示例1
// g是有两个参数的可调用对象
// func是有5个参数的函数
auto g = bind(func, a, b, _2, c, _1);
// 生成一个新的可调用对象,它有两个参数,分别用占用符_2和_1表示。
// 传递给g的参数按位置绑定到占位符。即,第一个参数绑定到_1,第二个参数绑定到_2.
// 因此,当我们调用g时,其第一个参数将被传递给f作为最后一个参数,第二个参数将被传递给f作为第三个参数
// f的第一个、第二个和第四个参数分别被绑定到给定的值a、b和c上,即:
// g(_1,_2), 将g的两个参数传递给f调用执行
// 实际上,这个bind调用会将g(_1,_2)映射为f(a,b,_2,c,_1)
// 即,对g的调用会调用f,用g的参数代替占位符,再加上绑定的参数a、b和c。
// 调用g(X, Y), 等于 func(a, b, Y, c, X)
g(X, Y);  

// 示例2
//用bind颠倒isShorter的含义:
//按单词长度由短至长排序
sort(words.begin(),words.end(),isShorter);
//按单词长度由长至短排序
sort(words.begin(),words.end(),bind(isShorter,_2,_1));
  • 绑定引用参数:如果我们在bind函数中像lambda表达式一样传递引用即[&],就得使用ref或cref来绑定,cref会创建一个const引用。
  • 与bind一样,函数ref和cref也定义在头文件functional中。
    • 默认情况下,bind那些不是占位符的参数被拷贝到bind返回的可调用对象中——即lambda表达式的[=]
    • 对有些绑定的参数我们希望以引用方式传递,就得使用ref或cref来绑定——即lambda表达式的[&]
      • lambda表达式中:[&n]
      • bind: ref(n)
using std::placeholders::_1;
// 例如,为了替换一个使用引用方式捕获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)
{
    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返回一个对象,包含给定的引用,此对象是可以拷贝的。
  • 绑定引用参数实例
#include<iostream>
#include<vector>
#include<algorithm>
#include<functional>
using namespace std;

bool check_size(const int x,int &sz)
{
    //改变sz的值
    sz = 6;
    return x > sz;
}

int main(void)
{
    vector<int> v = {
            1,2,3,4,5,6,7,8,9
    };

    int n = 5;
    //传递n的引用
    auto new_check_size = bind(check_size,std::placeholders::_1,ref(n));

    auto it = find_if(v.begin(),v.end(),new_check_size);

    cout<<"n的值为为:"<<n<<endl;

    return 0;
}
####bind的使用完整实例
```C++
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <functional>

using namespace std;
//_1,_n在std::placeholders里面                                                
using namespace std::placeholders;

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

bool shorter(const string &a, const string &b){
    return a.size() < b.size();
}

ostream& print(ostream& os, const string &s, const char &c){
    //c = ',';                                                                    
    return os << s << c;
}

int main(){
    /*                                                                            
    //用bind实现了和lambda一样的功能                                              
    vector<string> svec{"aab","d","aa","bb","e","bbb"};                           
    stable_sort(svec.begin(),svec.end(),[](const string &a, const string &b){     
        return a.size() < b.size();                                               
      });                                                                         
    string::size_type sz = 3;                                                     
    auto idx = find_if(svec.begin(),svec.end(),bind(check_size, _1, sz));         
    cout << *idx << endl;                                                         
    idx = find_if(svec.begin(),svec.end(),[sz](const string &s){                  
        return s.size() >= sz;                                                    
      });                                                                         
    cout << *idx << endl;                                                         
    */

    /*                                                                            
    //用bind改变原来函数的参数的位置                                              
    //升序                                                                        
    vector<string> svec{"aab","d","aa","bb","e","bbb"};                           
    sort(svec.begin(), svec.end(), shorter);                                      
    for(auto const &s : svec){                                                    
      cout << s << " ";                                                           
    }                                                                             
    cout << endl;                                                                 
    //由于调换了shorter参数的位置,所以变成了降序                                 
    sort(svec.begin(), svec.end(),bind(shorter, _2, _1));                         
    for(auto const &s : svec){                                                    
      cout << s << " ";                                                           
    }                                                                             
    cout << endl;                                                                 
    */

    //bind引用,必须使用ref或者cref函数,把对象转化成引用,不能用&                 
    ostream &os = cout;
    const char c = ' ';
    vector<string> svec{"aab","d","aa","bb","e","bbb"};
    for_each(svec.begin(),svec.end(),[&os, c](const string &s){
        os << s << c;
      });
    os << endl;
    for_each(svec.begin(),svec.end(),bind(print, ref(os), _1, cref(c)));
    os << endl;
    cout << c << endl;

}

总结:谓词、lambda和bind之间的关系

  • 谓词:只能是一元或二元谓词。即只接受算法送进来的容器的一个或两个元素作为其一元或二元参数
  • lambda表达式:可以接受多个参数。除了算法送进来的容器的一个或两个元素作为参数,还可以以[捕获列表]捕获外部变量的方式拓展参数数目,空捕获列表[]就相当于谓词函数
  • bind函数:可以接受多个参数。可以实现和lambda一样的功能。如果要重复使用函数,此时不方便用lambda,用bind替代。

4. 再探迭代器:除了普通迭代器,C++标准模板库还定义了几种特殊的迭代器

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

4.1 插入迭代器:把一个赋值操作转换为把相应的值插入容器的操作

  • 插入迭代 器是一种迭代器适配器,它接受一个容器,生成一个迭代器,能实现向给定容器添加元素。
  • 算法库对其所有在容器上的操作有约束:决不修改容器的大小(不插入、不删除)。有了插入迭代器,既使得算法库可以通过迭代器对容器插入新的元素,又不违反这一统带,即保持了设计上的一致性。
    当我们通过一个迭代器进行赋值时,该迭代器调用容器操作push_back来向给定容器的指定位置插入一个元素。下表列出了这种迭代器支持的操作。
  • 插入迭代器有三种类型,差异在于元素插入的位置:
  • 尾部插入器:back_inserter创建一个调用push_back的迭代器(在2.初识泛型算法有详细介绍,常对一个初始为空的容器增加元素)
    • 创建:back_inserter(容器c)
    • 使用:通过调用容器的push_back()成员函数来插入元素
    • 功能:总是在容器的尾端插入元素
    • 限制:只有提供了push_back()成员函数的容器才可以用
    • 适用:vector deque list
      头部插入器:front_inserter创建一个调用push_front的迭代器
    • 创建:front_inserter(容器c)
    • 使用:通过调用容器的push_front()成员函数来插入元素
    • 功能:总是在容器的前端插入元素
    • 限制:只有提供了push_front()成员函数的容器才可以用
    • 适用:deque list
      普通插入器:inserter创建一个使用insert的迭代器。此函数接受三个参数,这个参数必须是一个指向给定容器的迭代器。元素将被插入到给定迭代器所表示的元素之前
    • 创建:inserter(容器c, 给定迭代器iter)
    • 使用:通过调用insert()成员函数来插入元素,并由用户指定插入位置
    • 功能:在容器的指定位置插入元素
    • 限制:STL容器都提供了insert()函数.
    • 适用:提供了insert()函数的STL容器
  • 理解插入迭代器的工作过程
    • 当调用inserter(c,iter)时,我们得到一个迭代器,接下来使用它时,会将元素插入到iter原来所指的位置之前的位置。
// 如果it是由inserter生成的迭代器,则下面这样的赋值语句
*it=val;
// 其效果与下面代码一样
it=c.insert(it,val);//it指向新加入的元素
++it; //递增it使它指向原来的元素

// 示例2
list<int> lst={1,2,3,4};
list<int> lst2,lst3;  //空list
//拷贝完成之后,lst2包含4 3 2 1
copy(lst.begin(),lst.end(),front_inserter(lst2));
//拷贝完成之后lst3包含1 2 3 4 
copy(lst.begin(),lst.end(),inserter(lst3,lst.begin()));

4.2 iostream迭代器:流迭代器,从流中每次一个的读值或写值

  • 虽然iostream类型不是容器,但标准库定义了用于这些IO类型对象的迭代器:
    • istream_iterator读取输入流
    • ostream_iterator向一个输出流写数据
    • 这些迭代器将它们对应的流当作一个特定类型的元素序列来处理。
  • 用处:通过使用流迭代器,我们可以用泛型算法从流对象读取数据以及向其写入数据。
  • 总结流迭代器(只能前向读取或写入,支持++,不能--,因为不可能在一个流中反向移动)
    • 提供了输入操作符(>>)和 输出操作符 (<<)的任何类型都可以创建 istream_iterator 对象和ostream_iterator对象
    • 当创建一个流迭代器时,必须指定迭代器将要读写的对象类型。一个istream_iterator使用>>来读取流。因此,istream_iterator要读取的类型必须定义了输入运算符。
  • istream_iterator的操作(输入流迭代器在使用时总是成对出现,其中一个是尾后迭代器表示输入结束)
    • istream_iterator in(is); //in从输入流is读取数据类型为T的值
      • T表示此istream_iterator的输入类型如int等
      • is为istream_iterator指向的流
    • istream_iterator end; //默认初始化迭代器(定义时不绑定流)
      • 这样就创建了一个可以当作尾后值使用的迭代器——尾后迭代器(读取类型为T的istream_iterator迭代器)。
      • 所有流的尾后迭代器指示一样 ,都表示一个流的读取结束
    • 对于一个绑定到流的迭代器,一旦其关联的流遇到文件尾或遇到IO错误,迭代器的值就与尾后迭代器相等。
    • *in 返回从流中读取的(一个)数据
    • ++in,in++ 使用元素类型所定义的>>运算符从输入流中读取下一个值,前置加版本返回一个递增后迭代器的引用,后置加版本返回迭代器的旧值;
// 定义示例
istream_iterator<int> int_it(cin); //从cin读取int
istream_iterator<int> int_eof; //尾后迭代器
ifstream in("afile");   //构造一个ifstream并打开给定文件
istream_iterator<string> str_in(in); //从“afile读取字符串

// 使用示例
istream_iterator<int> in_iter(cin); //从cin读取int
istream_iterator<int> eof;  //istream尾后迭代器
while(in_iter!=eof)
    vec.push_back(*in_iter++);

// 上例等效于:构造容器时使用输入流迭代器从流中获取元素
istream_iterator<int> in_iter(cin),eof; //从cin读取int
vector<int> vec(in_iter,eof);  //从迭代器范围构造vec
/*
使用了一对表示范围的迭代器来构造vec,这两个迭代器是istream_iterator,
这意味着元素范围是通过从关联的流中读取数据获得的。
这个构造函数从cin读取数据,直至遇到文件尾或者遇到一个不是int的数据为止。
从流中读取的数据被用来构造vec。
*/
  • ostream_iterator的操作
    • 我们可以对任何输出运算符(<<运算符)的类型定义ostream_iterator。
    • 当创建一个ostream_iterator时,我们可以提供(可选的)第二参数,它是一个字符串,在输出每个元素后都会打印此字符串。此字符串必须是一个C风格字符串(即,一个字符串字面值或者一个指向以空字符结尾的字符数组的指针)。
    • 必须将ostream_iterator绑定到一个指定的流。不允许空的或表示尾后位置的ostream_iterator。
    • ostream_iterator out(os); //out将类型为T的值写入到输出流os中
    • ostream_iterator out(os,d); //out将类型为T的值写入到输出流os中,每个值后面都输出一个d,d指向一个空字符结束的数组,实际上d就是两个值之间的间隔符
    • out=val; //把赋值操作转换为用<<运算符将val写入到out所绑定的ostream,val的类型必须与out可写的类型兼容
    • *out,++out,out++ 不对out做任何事情,都返回out
  • 输出方式1:直接使用ostream_iterator进行输出
// 形式1:推荐这种形式
ostream_iterator<int> out_iter(cout," ");
    *out_iter++=e;   //赋值语句实际上将元素写到cout
cout<<endl;
// 此程序将vec中的每个元素写到cout,每个元素加一个空格,每次向out_iter赋值时,写操作就会被提交。

// 形式2:
// 当我们向out_iter赋值时,可以忽略解引用和递增运算。即,循环可以重写成下面的样子:
for(auto e:vec)
    out_iter=e;//赋值语句将元素写到cout
cout<<endl;
/*
运算符*和++实际上对ostream_iterator对象不做任何事情,因此忽略它们对我们的程序没有任何影响。
但是,推荐第一种形式。
在这种写法中,流迭代器的使用与其他迭代器的使用保存一致。
如果想将此循环改为操作其他迭代器类型,修改起来非常容易。
而且,对于读者来说,此循环的行为也更为清晰。
*/
  • 输出方式2:更简单的输出方法:将copy算法与ostream_iterator结合进行输出
// 可以通过调用copy来打印vec中的元素,这比编写循环更为简单:
copy(vec.begin(),vec.end(),out_iter);
cout<<endl;
  • 输入流和输出流迭代器一起使用示例
istream_iterator<int> in_iter(cin);//从标准输入流cin中读取int
istream_iterator<int> eof;//尾后迭代器
vector<int> vec;

//检查in_iter是否等于eof; eof被定义为空istream_iterator,从而可以当作尾后迭代器来使用
while(in_iter!=eof)
    //后置递增运算读取流,返回迭代器的旧值
    //解引用迭代器,获得从流读取的前一个值
    vec.push_back(*in_iter++);
	
/*法1:直接用cout输出
for(vector<int>::iterator  it=vec.begin();it!=vec.end ();it++)
    cout<<*it<<" ";
*/ 

// 法2:用输出流迭代器输出
ostream_iterator<int> out_iter(cout," ");
for(vector<int>::iterator  it=vec.begin();it!=vec.end ();it++)
    *out_iter++=*it;

/*
// 法3:用范围for语句输出
for(auto e:vec)
    *out_iter++=e;
cout<<endl;
*/


/*法4:使用copy到输出流迭代器输出vector元素
ostream_iterator<int> out_iter(cout," ");
copy(vec.begin(),vec.edn(),out_iter);
cout<<endl;
*/

}
  • 使用算法操作简化输入流迭代器的读取和输出流迭代器的输出
  • (1)直接用输入流迭代器和它的尾后迭代器组成读算法的迭代器范围来进行读取
    • 由于算法使用迭代器操作来处理数据,而流迭代器又至少支持某种迭代器操作,因此我们至少可以用某些算法来操作流迭代器。
    • 下面是一个例子,我们可以用一对istream_iterator来调用accumulate:
istream_iterator<int> in_iter(cin),eof;
cout<<accumulate(in_iter,eof,0)<<endl;
/*
此调用会计算出从标准输入读取的值的和。如果输入为:
1 3 7 9 9 
输出为29
*/
  • (2)直接用输出流迭代器作为写算法的目标迭代器来进行输出
//将copy算法与ostream_iterator结合使输出简单化
ostream_iterator<int> out_iter(cout," ");
copy(vec.begin(),vec.edn(),out_iter);
cout<<endl;
  • istream_iterator允许使用懒惰求值
    • 当我们将一个istream_iterator绑定到一个流时,标准库并不保证迭代器立即从流读取数据。具体实现可以推迟从中读取数据,直到我们使用迭代器时才真正读取。标准库中的实现所保证的是,在我们第一次解引用迭代器之前,从流中读取数据的操作已经完成了。对于大多数程序来说,立即读取还是推迟读取并没有什么差别。但是,如果我们创建了一个istream_iterator,没有使用就销毁了,或者我们正在从两个不同的对象同步读同一个流,那么何时读取可能就很重要了
  • 使用流迭代器处理类类型
    • 我们可以为任何定义了输入运算符(>>)的类型创建istream_iterator对象。类似的,只要类型有输出运算符(<<),我们就可以为其定义ostream_iterator。由于Sales_item既有输入运算符也有输出运算符,因此可以使用IO迭代器。例如:
istream_iterator<Sales_item> item_iter(cin),eof;
ostream_iterator<Sales_item> out_iter(cout,"\n");
Sales_item sum=*item_iter++;
while(item_iter!=eof)
{
    if(item_iter->isbn()==sum.isbn())
        sum+=*item_iter++;
    else
    {
        out_iter=sum;
        sum=*item_iter++;
    }
}
out_iter=sum;
/*
此程序使用item_iter从cin读取Sales_item交易记录,并将和写入cout,每个结果后面都跟一个换行符。
定义了自己的迭代器后,我们就可以用item_iter读取第一条交易记录,用它的值来初始化sum.
*/

4.3 反向迭代器:一种反向遍历容器的迭代器。即从最后一个元素到第一个元素遍历容器

  • 反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器。
  • 对于反向迭代器,递增(以及递减)操作的含义会颠倒过来。递增一个反向迭代器(++it)会移动到前一个元素;递减一迭代器(--it)会移动到下一个元素。
  • 除了forward_list之外,其他容器都支持反向迭代器。
  • 我们可以通过调用rbegin、rend、crbegin和crend成员函数来获得反向迭代器。这些成员函数返回指向容器尾元素和首元素之前一个位置的迭代器。
  • 与普通迭代器一样,反向迭代器也有const版本(rbegin/rend)和非const版本(crbegin/crend)。
  • 比较 begin/end 和 rbegin/rend 迭代器
    • c.begin() 返回一个迭代器,它指向容器c的第一个元素
    • c.end() 返回一个迭代器,它指向容器c的最后一个元素的下一个位置
    • c.rbegin() 返回一个逆序迭代器,它指向容器c的最后一个元素
    • c.rend() 返回一个逆序迭代器,它指向容器c的第一个元素前面的位置
  • 使用反向迭代器的例子,它按逆序打印vec中的元素:
vector<int> vec={0,1,2,3,4,5,6,7,8,9};
//从尾元素到首元素的反向迭代器
for(auto r_iter=vec.crbegin;r_iter!=vec.crend();++r_iter)
cout<<*r_iter<<endl;  //打印9,8,7,6,5,4,3,2,1,0
  • 用算法透明地向前或向后处理容器。例如,可以通过向sort传递一对反向迭代器来将vector整理为递减序:
sort(vec.begin(),vec.end());  //从小到大排序
sort(vec.rbegin(),vec.rend());  //从大到小排序
  • 我们只能从既支持++也支持--的迭代器来定义反向迭代器。毕竟反向迭代器的目的是在序列中反向移动。除了forward_list之外,标准容器上的其他迭代器都既支持递增运算又支持递减运算。但是,流迭代器不支持递减运算,因为不可能在一个流中反向移动。因此,不可能从一个forward_list或一个流迭代器创建反向迭代器。
  • 反向迭代器与普通迭代器间的关系和使用区别(反向迭代器.base()可以将反向迭代器转换回普通迭代器,此时二者的指向并不同:反向-普通)
// 假定有一个名为line的string,保存着一个逗号分隔的单词列表
// (1)如果希望打印line中的第一个单词
//在一个逗号分隔的列表中查找一个元素
auto comma=find(line.cbegin(),line.cend(),',');
cout<<string(line.cbegin(),comma)<<endl;
// 如果line中有逗号,那么comma将指向这个逗号;否则,它将等于line.cend().
//当我们打印从line.cbegin()到comma之间的内容时,将打印到逗号为止的序列,或者打印整个string(如果其中不含逗号的话)。

// (2)如果希望打印最后一个单词,可以改用反向迭代器
// 1)先用反向迭代器获得最后一个元素
// 在一个逗号分隔的列表中查找最后一个元素
auto rcomma=find(line.crbegin(),line.crend(),',');
// 由于我们将crbegin和crend传递给find,find将从line的最后一个字符开始向前搜索。
// 当find完成后,如果line中有逗号,则rcomma指向最后一个逗号——即,它指向反向搜索中找到的第一个逗号。
// 如果line中没有逗号,则rcomma指向line.crend()

// 2)打印时将反向迭代器转换回普通迭代器,按正常顺序打印
// a)若不转换回普通迭代器:
//错误:将逆序输出单词的字符
cout<<string(line.crbegin(),rcomma)<<endl;
// 但它会生成错误的输出结果。例如,如果我们的输入是FIRST,MIDOLE,LAST则这条语句会打印TSAL!

// b)转换回反向迭代器:调用反向迭代器的base成员函数来返回其对应的普通迭代器:
//正确:得到一个正向迭代器,从逗号开始读取字符直到line末尾
cout<<string(rcomma.base(),line.cend())<<endl;
  • rcomma和rcomma.base()指向了不同的元素:
    • 从技术上讲,普通迭代器与反向迭代器的关系反映了左闭合区间的特征。关键点在于[line.crbegin(),rcomma)和[rcomma.base(),line.cend())指向line中相同的元素范围

5. 泛型算法结构

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

5.1 5类迭代器

  • 类似容器,迭代器也定义了一组公共操作,一些操作所有迭代器都支持,另外一些只有特定类别的迭代器才支持。例如,ostream_iterator只支持递增、解引用和赋值。vector、string和deque的迭代器除了这些操作外,还支持递减、关系和算术运算。
  • 迭代器是按它们所提供的操作来分类的,而这种分类形成了一种层次。除了输出迭代器之外,一个高层类别的迭代器支持底层类别迭代器的所有操作。

输入迭代器

  • 输入迭代器:可以读取序列中的元素。一个输入迭代器必须支持
    • 用于比较两个迭代器的相等和不相等运算符(==、!=)
    • 用于推进迭代器的前置和后置递增运算(++)
    • 用于读取元素的解引用运算符(*);解引用只会出现在赋值运算符的右侧
    • 箭头运算符(->),等价于(*it).member,即,解引用迭代器,并提取对象的成员
  • 输入迭代器只用于顺序访问。对于一个输入迭代器,*it++保证是有效的,但递增它可能导致所有其他指向流的迭代器失效。其结果就是,不能保证输入迭代器的状态可以保存下来并用来访问元素。因此,输入迭代器只能用于单遍扫描算法。
  • 算法find和accumulate要求输入迭代器;而istream_iterator是一种输入迭代器。

输出迭代器

  • 输出迭代器:可以看做输入迭代器功能上的补集——只写而不读元素。输出迭代器必须支持
    • 用于推进迭代器的前置和后置递增运算(++)
    • 解引用运算符(*),只能出现在赋值运算符的左侧(向一个已经解引用的输出迭代器赋值,就是将值写入它所指向的元素)
  • 我们只能向一个输出迭代器赋值一次。类似输入迭代器,输出迭代器只能用于单遍扫描算法。用作目的位置的迭代器通常都是输出迭代器。
  • copy函数的第三个参数就是输出迭代器。ostream_iterator类型也是输出迭代器。

前向迭代器

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

双向迭代器

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

随机迭代器

  • 随机访问迭代器:提供在常量时间内访问序列中的任意元素的能力。此类迭代器支持双向迭代器的所有功能,此外还支持如下的操作:
    • 用于比较两个迭代器相对位置的关系运算符(<、<=、>和>=)
    • 迭代器和一个整数值的加减运算(+、+=、-和-=),计算结果是迭代器在序列中前进(或后退)给定整数个元素后的位置
    • 用于两个迭代器上的减法运算符(-)得到两个迭代器的距离
    • 下标运算符(iter[n],与*(iter[n])等价
  • 算法sort要求随机访问迭代器,array、deque、string和vector的迭代器都是随机访问迭代器,用于访问内置数组元素的指针也是。

5.2 算法形参模式

  • 在任何其他算法分类之上,还有一组参数规范。大多数算法具有如下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都是迭代器参数,它们分别承担指定目的位置和第二个范围的角色。
    • 除了这些迭代器参数,一些算法还接受额外的、非迭代器的特定参数。
  • 接受单个目标迭代器dest的算法
    • dest参数是一个表示算法可以写入的目的位置的迭代器。算法假定:按其需要写入数据,不管写入多少个元素都是安全的。
    • dest作为目标迭代器:
      • (1)直接指向容器的迭代器,那么算法将输出数据写到容器中已存在的元素内。
      • (2)更常见的情况是,dest被绑定到一个插入迭代器或是一个ostream_iterator。插入迭代器会将新元素添加到容器中,因而保证空间是足够的;ostream_iterator 会将数据写入一个输出流,同样不管要写多少个元素都没有问题
  • 接受第二个输入序列(beg2,end2)的算法题。
    • 接受单独的beg2或是接受beg2和end2的算法用这些迭代器表示第二个输入范围。这些算法通常使用第二个范围中的元素与第一个输入范围结合来进行一些运算。

5.3 算法命名规范

  • 除了参数规范,算法还遵循一套命名和重载规范。这些规范处理诸如:如何提供一个操作代替默认的<或=运算符以及算法是将输出数据写入输入序列还是一个分离的目的位置等为题。
  • (1)一些算法使用重载形式传递一个谓词——因为接受参数数目不同,自定义比较函数
    • 接受谓词参数来代替<或==运算符的算法,以及哪些不接受额外参数的算法,通常都是重载的函数。
    • 函数的一个版本用元素类型的运算符来比较元素;另一个版本接受一个额外谓词参数,来代替<或==:
unique(beg,end);  //使用==运算符比较元素
unique(beg,end,comp); //使用comp比较元素
// 两个调用都重新整理给定序列,将相邻的重复元素删除。
// 第一个调用使用元素类型的==运算符来检查重复元素;
// 第二个则调用comp来确定两个元素是否相等。
// 由于两个版本的函数在参数个数上不相等,因此具体应该调用那个不会产生歧义。
  • (2)_if版本的算法——因为接受参数数目相同
    • 接受一个元素值的算法通常有另一个不同名的版本,该版本接受一个谓词代替元素值。
    • 接受谓词参数的算法都有附加_if前缀:
find(beg,end,val); //查找输入范围中val第一次出现的位置
find_if(beg,end,pred);  //查找第一个令pred为真的元素
// 这两个算法都在输入范围中查找特定的元素第一次出现的位置。
// 算法find查找一个指定值;算法find_if查找使得pred返回非零值的元素。
/*
这两个算法提供了命名上的差异的版本,而非重载版本,因为两个版本的算法都接受相同数目的参数。
因此可能产生重载歧义,虽然很罕见,但为了避免任何可能的歧义,标准库提供不同名字的版本而不是重载。
*/
  • (3)区分拷贝元素的版本和不拷贝的版本
  • 默认情况下,重排元素的算法将重排后的元素写回固定的输入序列中
  • 这些算法还提供另一个版本,将元素写到一个指定的输出目的位置。写到额外目的空间的算法都在名字后面附加一个_copy:
  • 一些算法同时提供_copy和_if版本。这些版本接受一个目的位置迭代器和一个谓词:
reverse(beg,end); //反转输入范围中元素的顺序
reverse_copy(beg,end,dest); //将元素按逆序拷贝到dest

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

6. 特定容器算法:针对list与forward_list,对于list和forward_list应该优先使用成员函数版本的算法而不是通用算法

  • 与其他容器不同,链表类型list与forward_list定义了几个成员函数形式的算法,如下表所示。
  • 特别是,它们定义了独有的sort、merge、remove、reverse和unique。通用版本的sort要求随机访问迭代器,因此不能用于list和forward_list,因为这两个类型分别提供双向迭代器和前向迭代器。
  • 链表类型定义的其他算法的通用版本虽然可以用于链表,但代价太高。这些算法需要交换输入序列中的元素。一个链表可以通过改变元素间的链接而不是真正的交换它们的值来传递“交换”元素。因此,这些链表版本的算法的性能比对应的通用版本好很多。
  • 注意:对于list和forward_list应该优先使用成员函数版本的算法而不是通用算法。

  • splice成员
    • 链表类型还定义了splice算法。其描述见下表。此算法是链表数据结构所特有的,因此不需要通用版本。
  • 链表特有的操作会改变容器
    • 多数链表特有的算法都与其通用版本很相似,但不完全相同。
    • 链表特有版本与通用版本间的一个至关重要的区别是链表版本会改变底层的容器。例如,remove的链表版本会删除指定的元素。unique的链表版本会删除第二个和后继的重复元素。
    • 类似的,merge和splice会销毁其参数。例如,通用版本的remove将合并的序列写给一个给定的目的迭代器:两个输入序列是不变的。而链表版本的merge函数会销毁给定的链表——元素从参数指定的链表中删除,被合并到调用merge的链表对象中。在merge之后,来自两个链表中的元素仍然存在,但它们都已在同一个链表中。
posted @ 2021-05-31 13:19  夏目的猫咪老师  阅读(101)  评论(0编辑  收藏  举报