第十章 泛型算法

泛型指它们可以用于不同类型的元素和多种容器类型。 

算法指它们实现了一些经典算法的公共接口。

泛型算法本身不会执行容器的操作,它们只会运行于迭代器之上,执行迭代器的操作。

大多数算法都定义在头文件<algorithm>中,标准库还在头文件<numeric>中定义了一组数值泛型算法。

一、只读算法

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

(一)find函数 

头文件:<algorithm> 

find将范围中每个元素与给定值进行比较。它返回第一个等于给定值的元素的迭代器。如果范围中无匹配元素,则find返回第二个参数来表示搜索失败。

注:

find寻找的范围不包括第二个参数所在位置。

例:
int val=40;

auto result=find(v.begin(),v.end(),val );

(二)accumulate 

头文件:<numeric> 

接受三个参数,前两个指出了需要求和的元素的范围,第三个参数是和的初值。

注:

序列中元素必须与第三个参数匹配,或者能够转换为第三个参数的类型。

所求和的类型需要定义+运算符。

例:

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

(三)equal 

头文件:<algorithm> 

用于确定两个序列是否保存相同的值。

它将第一个序列中的每个元素与第二个序列中的对应元素进行比较。若所有对应的元素都相同,则返回true,否则,返回false。

接受三个迭代器,前两个表示第一个序列中的元素范围,第三个表示第二个序列的首元素。

注:

第二个序列至少与第一个序列一样长。

例:

equal(v.begin(),v.end(),v2.begin()); 

二、写容器元素的算法 

(一)介绍back_inserter

一种保证算法有足够元素空间来容纳输出数据的方法是使用插入迭代器。

插入迭代器是一种向容器中添加元素的迭代器。当通过一个插入迭代器赋值时,一个与赋值号右侧值相等的元素被添加到容器中。

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

(一)拷贝算法----copy 

头文件:<algorithm> 

接受三个迭代器,前两个表示一个输入范围,第三个表示目的序列的起始位置。

作用是将输入范围中的元素拷贝到目的序列中。

注:

传递给copy的目的序列至少包含与输入序列一样多的元素。

返回其目的位置迭代器的值,即拷贝之后的尾元素的下一位置。

例:

auto ret=copy(begin(a1),end(a1),a2);//ret指向拷贝到a2的尾元素之后的位置。

三、删除容器元素的算法 

(一)unique--消除重复元素

将相邻的重复项删除,并返回一个指向不重复值范围末尾的迭代器。

注:

unique并不会真的删除元素,它只是将重复的元素用不重复的元素覆盖了。 

四、向算法传递函数 

常用于算法函数的重载,它接受第三个参数。例如对sort函数的重载。 

算法函数额外接受的一个参数称为谓词。

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

五、lambda表达式 

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

例如,用find_if算法来查找第一个具有特定大小的元素。可以想到要编写一个函数,令其接受一个string和一个长度,并返回一个bool值表示该string的长度是否大于给定长度。但find_if只接受一元谓词,传递给find_if的任何函数都必须严格接受一个参数,为了解决这个问题,就需要向算法传递任何类别的可调用对象。

对于一个对象或一个表达式,若可以对其使用调用运算符,则称它是可调用的。

例如,若e是一个可调用的表达式,则我们可以编写代码e(args),其中args是一个逗号分隔的一个或多个参数的列表。 

可调用对象有:

1.函数

2.函数指针

3.重载了函数调用操作符的类

4.lambda表达式 

(一)lambda简述 

一个lambda表达式表示一个可调用的代码单元。可以将其理解一个为未命名的内联函数。与函数类似,一个lambda具有一个返回类型、一个参数列表和一个函数体。但与函数不同的是,lambda可能定义在函数内部。

形式:

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

其中,capture list(捕获列表)是一个lambda所在函数中定义的局部变量的列表(通常为空)。

return type、parameter list和function body与任何普通函数一样,分别表示返回类型、参数列表和函数体。但,与普通函数不同,lambda必须使用尾置返回来制定返回类型。

lambda可以忽略参数列表和返回类型,但必须包含捕获列表和函数体。

例:

auto f= [ ] { return 42;};//定义了一个可调用对象f,它不接受参数,返回42。

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

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

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

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

若lambda的函数体包含任何单一return语句之外的内容,且未指定返回类型,则返回void。 

(二)向lambda传递参数 

与一个普通函数调用类似,调用一个lambda时给定的实参被用来初始化lambda的形参。通常,实参和形参的类型必须匹配。但,与普通函数不同,lambda不能有默认参数。因此,一个lambda调用的实参数目永远与形参数目相等,一旦用形参初始化完毕,就可以执行函数体了。

lambda通过将局部变量包含在其捕获列表中来指出将会使用这些变量,捕获列表指引lambda在其内部包含访问局部变量所需的信息。

例:

int x=0;

auto sum=[x](int y){return x+y;}; 

一个lambda可以直接使用定义在当前函数之外的名字。

例如,

for_each(wc,words.end(),[](const string& s){cout<<s<<" ";});

在上述代码中,cout不是定义在函数中的局部名字,而是定义在都文件iostream中。因此,只要在函数的作用域中包含了头文件iostream,lambda就可以使用cout。

(三)lambda的捕获和返回

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

可以这样理解,当向一个函数传递一个lambda时,同时定义了一个新类型和该类型的一个对象:传递的参数就是此编译器生成的类类型的未命名的对象。类似,当使用auto定义一个用lambda初始化的变量时,定义了一个从lambda生成的类型的对象。

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

类似参数传递,变量的捕获方式也有两种类型,即,值或引用。

1.值捕获

与传值参数类似,采用值捕获的前提是变量可以拷贝。

但与参数不同,被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝。 

例:

void func()
{
    size_t v1=42;
    auto f=[v1]{return v1;};
    v1=0;
    auto j=f();//j的值为42。f保存了我们创建它时v1的拷贝。
}

2.引用捕获 

一个以引用方式捕获的变量与其他任何类型的引用的行为类似。

引用捕获与返回引用有着相同的问题和限制。如果采用引用方式捕获一个变量,就必须确保被引用的对象在lambda执行的时候存在。 

例:

void func()
{
    size_t v1=42;
    auto f2=[&v1]{return v1;};
    v1=0;
    auto j=f2();//j的值为0。f2保存v1的引用,而非拷贝。
}

3.隐式捕获 

除了显示的列出我们希望使用的来自所在函数的变量之外,还可以让编译器根据lambda中的代码来推断我们要使用哪些变量。

为了指示编译器推断捕获列表,应在捕获列表中写一个&或=。&告诉编译器采用引用捕获方式,=表示采用值捕获方式。 

例:

wc=find_if(words.begin(),words.end(),[=](const string &s){return s.size()>=sz;});

//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;});
}

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

当混合使用隐式捕获和显式捕获时,显式捕获的变量必须使用与隐式捕获不同的方式。即,若隐式捕获是引用方式,则显式捕获命名变量必须采用值方式。

例: 

void func()
{
    int v1=42;
    int v2=43;
    auto f=[&,v2]{return v1+v2;};
    //v1采用隐式捕获方式,为引用捕获。v2采用显式捕获方式,为值捕获。
}

4.lambda捕获列表 

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

4.可变lambda 

默认情况下,对于一个值被拷贝的变量,lambda不会改变其值。若我们希望能改变一个被捕获的变量的值,就必须在参数列表尾加上关键字mutable。 

例:

void func()
{
    size_t v1=42;
    //f可以改变它所捕获的变量的值
    auto f=[v1]()mutable{return ++v1;};
    v1=0;
    auto j=f();//j为43。
}

(四)指定lambda返回类型 

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

当有return之外的语句且需要返回类型时,必须使用尾置返回类型。

例:

void func()
{
    auto f=[](int i)->int
    {if(i<0)return -i;else return i;};
}

六、标准库bind函数 

还可以使用bind函数来解决向find_if传递只接受一个参数的函数问题。

头文件:<functional> 

可以将bind函数看作一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。 

形式:

auto newCallable=bind (callable, arg_list);

newCallable本身是一个可调用对象。

arg_list是一个逗号分隔的参数列表,对应给定的callable的参数,即,当调用newCallable时,newCallable会调用callable,并传递给它arg_list中的参数。

arg_list中的参数可能包含形如_n的名字,其中n是一个整数。这些参数表示newCallable的参数。

数值n表示生成的可调用对象中参数的位置,_1为newCallable中的第一个参数,_2为第二个参数,以此类推。

例:

 auto check6=bind(check_size,_1,6);

此bind调用只有一个占位符,表示check6只接受单一参数。占位符出现在arg_list的第一个位置,表示check6的此参数对应check_size的第一个参数。

string s="hello";

bool b1=check6(s);//check6(s)会调用check_size(s,6);

(一)使用placeholders名字 

名字_n都定义在一个名为placeholders的命名空间中,而这个命名空间本身定义在std命名空间中。为了使用这些名字,两个命名空间都要写上。

例如,_1对应的using声明为

using std::placeholders::_1; 

此声明说明要使用的名字_1定义在命名空间placeholder中,而此命名空间又定义在命名空间std中。

对每个占位符名字,都必须提供一个using声明。编写这样的声明很烦人,也很容易出错。可以使用另外一种不同的using语句,而不是分别声明每个占位符:

using namespace namespace_name;

这种形式说明希望所有来自namespace_name的名字都可以在程序中使用。

例如,

using namespace std::placeholder;

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

(二)bind的参数

可以用bind绑定给定可调用对象中的参数或重新安排其顺序。

例如,假定f是一个可调用对象,它有5个参数,则下面对bind的调用:

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

生成一个新的可调用对象,它有两个参数,分别用占位符_2和_1表示。这个新的可调用对象将它自己的参数作为第三个和第五个参数传递给f。f的第一个、第二个和第四个参数分别被绑定到给定的值a、b和c上。

传递给g的参数按位置绑定到占位符上。即,第一个参数绑定到_1,第二个参数绑定到_2。因此,当调用g时,其第一个参数将被传递给f作为最后一个参数,第二个参数将被传递给f作为第三个参数。即,对g的调用会调用f,用g的参数代替占位符,再加上绑定的参数a、b和c。

例如,调用g (X,Y)会调用

f(a,b,Y,c,X);

1.绑定引用参数

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

例如,用bind进行引用方式捕获ostream的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中。

七、再探迭代器 

除了为每个容器定义的迭代器之外,标准库在头文件iterator中还定义了额外几种迭代器。

1.插入迭代器

这些迭代器被绑定到一个容器上,可用来向容器插入元素。

2.流迭代器

这些迭代器被绑定到输入或输出流上,可用来遍历所关联的IO流。

3.反向迭代器

这些迭代器向后而不是向前移动。除了forward_list之外的标准库容器都有迭代器。

4.移动迭代器

这些专用的迭代器不是拷贝其中的元素,而是移动它们。 

(一)插入迭代器 

插入器是一种迭代器适配器,它接受一个容器,生成一个迭代器,能实现向给定容器添加元素。

当通过一个插入迭代器进行赋值时,该迭代器调用容器操作来向给定容器的指定位置插入一个元素。 

插入迭代器操作
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

 插入器有三种类型,差异在于元素插入的位置:

1.back_inserter创建一个使用push_back的迭代器。

2.front_inserter创建一个使用push_front的迭代器。

3.inserter创建一个使用insert的迭代器。此函数接受第二个参数,这个参数必须是一个指向给定容器的迭代器。元素将被插入到给定迭代器所表示的元素之前。

只有在容器支持push_front的情况下,才可以使用front_inserter。其余也是如此。 

(二)iostream迭代器 

虽然iostream类型不是容器,但标准库定义了可以用于这些IO类型对象的迭代器。

istream_iterator读取输入流,ostream_iterator向一个输出流写数据。

这些迭代器将它们对应的流当作一个特定类型的元素序列来处理。通过使用流迭代器,可以用泛型算法从流对象读取数据以及向其写入数据。 

1.istream_iterator操作 

当创建一个流迭代器时,必须指定迭代器将要读写的对象类型。一个istream_istreator使用>>来读取流。因此,istream_iterator要读取的类型必须定义了输入运算符。 

当创建一个istream_iterator时,可以将它绑定到一个流。还可以默认初始化迭代器,这样就创建了一个可以当作尾后值使用的迭代器。

istream_iterator<int> int_it(cin);//从cin读取int
istream_iterator<int> int_eof;//尾后迭代器
ifstream in("afile");
istream_iterator<int> str_it(in);//从"afile"读取字符串

下面是一个用istream_iterator从标准输入读取数据,存入一个vector的例子:

istream_iterator<int> in_iter(cin);//从cin读取int
istream_iterator<int> eof;//istream尾后迭代器
while(in_iter!=eof)//当有数据可供读取时
    //后置递增运算读取流,返回迭代器的旧值
    //解引用迭代器,获得从流读取的前一个值
    vec.push_back(*in_iter++);

此循环从cin读取int值,保存在vec中。在每个循环步中,循环体代码检查in_iter是否等于eof。eof被定义为空的istream_iterator,从而可以当作尾后迭代器来使用。对于一个绑定到流的迭代器,一旦其关联的流遇到文件尾或遇到IO错误,迭代器的值就与尾后迭代器相等。

可以将上述代码重写为如下形式:

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

由于算法使用迭代器操作来处理数据,而流迭代器又至少支持某些迭代器操作,因此至少可以用某些算法来操作流迭代器。

例如,

istream_iterator<int> in(cin),eof;
cout<<accumulate(in,eof,0)<<endl;
1.2.istream_iterator允许使用懒惰求值

当将一个istream_iterator绑定到一个流时,标准库并不保证迭代器立即从流读取数据。具体实现可以推迟从流中读取数据,直到使用迭代器时才真正读取。标准库中的实现保证的是,在第一次解引用迭代器之前,从流中读取数据的操作已经完成了。对于大多数程序来说,立即读取还是推迟读取没什么差别。但,若我们创建了一个istream_iterator,没有使用就销毁了,或者正从两个不同的对象同步读取同一个流,那么何时读取就可能很重要了。 

2.ostream_iterator操作 

可以对任何具有输出运算符(<<)的类型定义ostream_iterator。当创建一个ostream_iterator时,可以提供(可选的)第二参数,它是一个字符串,在输出每个元素后都会打印此字符串。此字符串必须是一个C风格字符串(即,一个字符串字面常量或者一个指向以空字符结尾的字符数组的指针)。

必须将ostream_iterator绑定到一个指定的流,不允许空的或表示尾后位置的ostream_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<int> out_iter(cout," ");
for(auto e: vec)
    *out_iter++=e;//赋值语句将元素写到cout
cout<<endl;

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

(三)反向迭代器 

反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器。对于反向迭代器,递增(以及递减)操作的含义会颠倒过来。例,递增一个反向迭代器会移动到前一个元素。

我们只能从既支持++也支持--的迭代器来定义反向迭代器。除了forward_list外,标准容器上的其他迭代器都既支持递增运算符又支持递减运算。但,流迭代器不支持递减运算,因为不可能在一个流中反向移动。因此,不可能从一个forward_list或一个流迭代器创建反向迭代器。 

可以通过调用rbegin、rend、crbegin和crend成员函数来获得反向迭代器。这些成员函数返回指向容器尾元素和首元素前一个位置的迭代器。与普通迭代器一样,反向迭代器也有const和非const版本。

反向迭代器操作:

vec.crbegin()//指向最后一个元素的位置。

vec.crend()//指向首元素之前的位置。 

例:

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

假定有一个名为line的string,保存这一个逗号分隔的单词列表,希望打印line中的最后一个单词,使用find能很容易地完成这一任务:

auto rcomma=find(line.crbegin(),line.crend(), ',');

 但,当打印单词时,这会出现错误:

cout<<string(line.crbegin(),rcomma)<<endl;

若输入的是first,middle,last。这段程序将会输出tsal。

由于使用的是反向迭代器,会反向处理string。因此,是反向打印line的内容。我们不能直接使用rcomma,因为它是一个反向迭代器,可以通过调用reverse_iterator的base成员函数将rcomma转换回一个普通迭代器。

cout<<string(rcomma.base(),line.cend())<<endl;

八、泛型算法结构 

任何算法最基本的特性是它要求迭代器提供哪些操作。

算法所要求的迭代器操作可以分为5个迭代器类别,每个算法都对它的每个迭代器参数指明了需提供哪类迭代器。 

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

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

算法还共享一组参数传递规范和一组命名规范。 

(一)5类迭代器 

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

迭代器是按它们所提供的操作来分类的,而这种分类形成了一种层次。除了输出迭代器之外,一个高层类别的迭代器支持底层类别迭代器的所有操作。

C++标准指明了泛型和数值算法的每个迭代器参数的最小类别。对每个迭代器参数来说,其能力必须与规定的最小类别至少相当。向算法传递一个能力更差的迭代器会产生错误。 而对于向一个算法传递错误类别的迭代器的问题,很多编译器不会给出任何警告或提示。

1.输入迭代器(input iterator)

输入迭代器可以读取序列中的元素。

输入迭代器只用于顺序访问。 

一个输入迭代器必须支持 

1.用于比较两个迭代器的相等和不相等运算符(==、!=)

2.用于推进迭代器的前置和后置递增运算(++)

3.用于读取元素的解引用运算符(*);解引用只会出现在赋值运算符的右侧

4.箭头运算符(->),等价于(*it).member,即,解引用迭代器,并提取对象的成员

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

istream_iterator是一种输入迭代器。 

2.输出迭代器(output iterator) 

输出迭代器可以看作输入迭代器功能上的补集----只写而不读元素。 

输出迭代器必须支持

1.用于推进迭代器的前置和后置递增运算(++)

2.解引用运算符(*),只出现在赋值运算符的左侧(向一个已经解引用的输出迭代器赋值,就是将值写入它所指向的元素)

只能向一个输出迭代器赋值一次。类似输入迭代器,输出运算符只能用于单遍扫描算法。用作目的位置的迭代器通常都是输出迭代器。

ostream_iterator是一种输出迭代器。

3.前向迭代器(forward iterator) 

前向迭代器可以读写元素。

这类迭代器只能在序列中沿一个方向移动。

前向迭代器支持所有输入和输出迭代器的操作,而且可以多次读写同一个元素。因此,可以保存前向迭代器的状态,使用前向迭代器的算法可以对序列进行多遍扫描。

4.双向迭代器(bidirectional iterator) 

双向迭代器可以正向或反向读写序列中的元素。除了支持所有前向迭代器的操作之外,双向迭代器还支持前置和后置递减运算符(--)。 

除了forward_list外,其他标准库都提供符合双向迭代器要求的迭代器。

5.随机访问迭代器(random-access iterator) 

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

随机访问迭代器还支持以下操作:

1.用于比较两个迭代器相对位置的关系运算符(<、<=、>和>=)

2.迭代器和一个整数值的加减运算(+、+=、-和-=),计算结果是迭代器在序列中前进(后退)给定整数个元素后的位置

3.用于两个迭代器上的减法运算符(-),得到两个迭代器的距离

4. 下标运算符(iter[n]),与*(iter[n])等价

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

(二)5类迭代器分别支持的操作

5类迭代器分别支持的操作
输入迭代器==、!=、++*(解引用)、->
输出迭代器

++、*(解引用)

前向迭代器==、!=、++*(解引用)、->
双向迭代器

==、!=、++、*(解引用)、->、前置和后置递减(--)

随机访问迭代器==、!=、++、--、*(解引用)、->、<、<=、>、>=、+=、-=、+、-、iter[n]、(iter[n])、迭代器之间的相减(-),得到之间的距离

(三)算法形参模式 

在任何其他算法分类之上,还有一组参数规范。

大多数算法具有如下4种形式之一:

1.alg(beg,end,other args);

2.alg(beg,end,dest,other args);

3.alg(beg,end,beg2,other args);

4.alg(beg,end,beg2,end2,other args);

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

除了这些迭代器参数,一些算法还接受额外的、非迭代器的特定参数。

1.接受单个目标迭代器的算法

dest参数是一个表示算法可以写入的目的位置的迭代器。算法假定按其需要写入数据,不管写入多少个元素都是安全的。

向输出迭代器写入数据的算法都假定目标空间足够容纳写入的数据。

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

2.接受第二个输入序列的算法

接受单独的beg2或是接受end2的算法用这些迭代器表示第二个输入范围。这些算法通常使用第二个范围中的元素与第一个输入范围结合来进行一些运算。

若一个算法接受beg2和end2,这两个迭代器表示第二个范围。这类算法接受两个完整指定的范围:[beg,end)表示的范围和[beg2,end2)表示的第二个范围。

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

(四)算法命名规范 

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

1.一些算法使用重载形式传递一个谓词

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

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

 2._if版本的算法

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

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

这两个算法都在输入范围中查找特定元素第一次出现的位置。算法find查找一个指定值;算法find_if查找使得pred返回非零值的元素。

3.区分拷贝元素的版本和不拷贝的版本

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

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

九、list和forward_list算法 

与其他容器不同链表类型list和forward_list定义了几个成员函数形式的算法。

特别,它们定义了独有的sort、merge、remove、reverse和unique。通用版本的sort要求随机访问迭代器,因此不能用于list和forward_list,因为这两个类型分别提供双向迭代器和前向迭代器。

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

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

链表类型还定义了splice算法,此算法是链表数据结构所特有的,因此不需要通用版本。

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或flst。lst2与lst(或flst)可以是相同的链表,但p不能指向给定范围中元素

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值