文章目录

  • 概念
    • find()函数
    • 迭代器令算法不依赖于容器
    • 但算法依赖于元素类型的操作
    • 算法永远不会执行容器的操作
  • 只读算法
    • accumulate()函数
    • 从两个序列中读取元素(equal函数为例)
      • 迭代器作为参数形成两个序列
    • equal()
  • 写容器元素的算法
    • 概念
    • fill()
    • fill_n()
    • 插入迭代器back_inserter
    • 插入迭代器是否与“标准库算法不会改变它们所操作的容器的大小”相悖
    • 拷贝(copy)算法
    • replace算法
    • 这里的容器大小指的是元素数量
  • 定制操作
    • 谓词
    • stable
    • 可调用对象
    • lambda表达式
      • 概念
      • 原理
      • 捕获列表
        • find_if
        • for_each
      • 捕获方式
        • 值捕获
          • mutable
        • 引用捕获
          • 修改引用捕获的变量
        • 隐式捕获
    • function模板
      • 概念
      • function与重载函数
    • bind函数
      • 概念
      • 使用placeholders名字
      • 作用
        • 削减参数数量
        • 重排参数顺序
      • 绑定引用参数
  • 泛型算法结构——迭代器类别
    • 概念
    • 不太重要的概念
    • 迭代器类别
      • 输入迭代器
      • 输出迭代器
      • 前向迭代器
      • 双向迭代器
      • 随机访问迭代器
  • 算法的命名规范
    • _if版本的算法
    • _copy版本的算法
  • list和forward_list独有的算法
    • 概念
    • 链表数据结构特有的splice算法
    • 多数链表特有的算法都与其通用版本很相似,但不完全相同。

概念

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

泛型算法的一大优点是 “泛型”,也就是一个算法可用于多种不同的数据类型,算法与所操作的数据结构分离。这对编程效率的提高是非常巨大的。

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

因此,并不是算法应该改变或不该改变容器的问题。 为了实现与数据结构的分离,为了实现通用性,算法根本就不该知道容器的存在。 算法访问数据的唯一通道是迭代器。是否改变容器大小,完全是迭代器的选择和责任。

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


find()函数

以find()函数为例来理解。传递给find的前两个参数是表示元素范围的迭代器,第三个参数是一个值。find将范围中每个元素与给定值进行比较。它返回指向第一个等于给定值的元素的迭代器。如果范围中无匹配元素,则find返回第二个参数来表示搜索失败。因此,我们可以通过比较返回值和第二个参数来判断搜索是否成功。

由于指针就像内置数组上的迭代器,因此我们也可以用find在数组中查找值:


迭代器令算法不依赖于容器

在find函数流程中,除了比较大小外,其他步骤都可以用迭代器操作来实现:

  1. 利用迭代器解引用运算符可以实现元素访问;
  2. 如果发现匹配元素,find可以返回指向该元素的迭代器;
  3. 用迭代器递增运算符可以移动到下一个元素;
  4. 尾后迭代器可以用来判断find是否到达给定序列的末尾;
  5. find可以返回尾后迭代器来表示未找到给定元素。

但算法依赖于元素类型的操作

虽然迭代器的使用令算法不依赖于容器类型,但大多数算法都使用了一个(或多个)元素类型上的操作。 例如,find用元素类型的==运算符完成每个元素与给定值的比较。不过,我们将会看到,大多数算法提供了一种方法,允许我们使用自定义的操作来代替默认的运算符。


算法永远不会执行容器的操作

泛型算法运行于迭代器之上而不会执行容器操作的特性带来了一个令人惊讶但非常必要的编程假定:算法永远不会改变底层容器的大小。算法可能改变容器中保存的元素的值,也可能在容器内移动元素,但永远不会直接添加或删除元素。



只读算法

一些算法只会读取其输入范围内的元素,但不改变元素。
例如下面这三个:

  • find()
  • count():接收一对迭代器和一个值作为参数,返回给定值在序列中出现的次数。

accumulate()函数

定义在头文件numeric中。接受三个参数,前两个指出需要求和的元素的范围,第三个参数时和的初值,其类型决定了函数中使用哪个加法运算符以及返回值的类型(这也就要求序列中元素的类型必须与第三个参数匹配,或者能够转换为第三个参数的类型)。

下面是另一个例子,由于string定义了+运算符,所以我们可以通过调用accumulate来将vector中所有string元素连接起来:

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

此调用将v中每个元素连接到一个string上,该string初始时为空串。注意,我们通过第三个参数显式地创建了一个string。如果单纯将空串当做一个字符串字面值传递给第三个参数是不可以的,会导致一个编译错误。

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

原因在于,如果我们传递了一个字符串字面值,用于保存和的对象的类型将是const char*。如前所述,此类型决定了使用哪个+运算符。由于const char*并没有+运算符,此调用将产生编译错误。

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


从两个序列中读取元素(equal函数为例)

迭代器作为参数形成两个序列

一些算法从两个序列中读取元素。构成这两个序列的元素可以来自于不同类型的容器。两个序列中元素的类型也不要求严格匹配。算法要求的只是能够比较两个序列中的元素。

操作两个序列的算法之间的区别在于我们如何传递第二个序列。 一些算法接受三个迭代器:前两个表示第一个序列的范围,第三个表示第二个序列中的首元素。 其他算法接受四个迭代器:前两个表示第一个序列的元素范围,后两个表示第二个序列的范围。

用一个单一迭代器表示第二个序列的算法都假定第二个序列至少与第一个一样长。确保算法不会试图访问第二个序列中不存在的元素是程序员的责任。


equal()

equal():用于确定两个序列是否保存相同的值。它将第一个序列中的每个元素与第二个序列中的对应元素进行比较。如果所有对应元素都相等,则返回true,否则返回false。 此算法接受三个迭代器:前两个表示第一个序列中的元素范围,第三个表示第二个序列的首元素(第二个序列中的元素数目应大于等于第一个序列)。

根据上面的概念,我们可以比较不同容器中不同的元素类型,只要两者能通过“ == ” 进行比较即可,因此,两个序列可以是vector和list<const char*>。

那些只接受一个单一迭代器来表示第二个序列的算法,都假定第二个序列至少与第一个序列一样长。



写容器元素的算法

概念

一些算法将新值赋予序列中的元素。 当我们使用这类算法时,必须注意确保序列原大小不小于我们要求算法写入的元素数目。 记住,算法不会执行容器操作,因此它们自身不可能改变容器的大小。

一些算法会自己向输入范围写入元素。这些算法本质上并不危险,它们最多写入与给定序列一样多的元素。

但是有一些算法接受一个迭代器和一个计数值来划定范围,然后写入元素。这种算法假定目的位置足够大,能容纳要写入的元素,也就是说算法不检查写操作。检查目标输入序列是否为空是程序员的责任,对空容器调用这些算法得到的结果是未定义的(但是可以通过back_insert(),来实现对空容器的操作)。

分别以 fill() 和 fill_n() 为例来探究:

fill()

接受一对迭代器表示一个范围,还接受一个值作为第三个参数。将给定值赋予序列中的每个元素。

给定范围有效:

给定范围越界:

给定容器为空:


fill_n()

fill_n接受一个单迭代器、一个计数值和一个给定值。他将给定值赋予迭代器指向的元素开始的计数值个元素。

给定范围有效:

给定范围越界:

给定容器为空:


插入迭代器back_inserter

  • 是定义在头文件iterator中的一个函数。
  • 是一种保证算法有足够元素空间来容纳输出数据的方法
  • 是一种向容器中添加元素的迭代器。
  • 接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器。
  • 当我们通过此迭代器赋值时,赋值运算符会调用push_back将一个具有给定值的元素添加到容器中。

实例:

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

这样可以避免对空容器操作的未定义行为。具体实现:

在每步迭代中,fill_n向给定序列的一个元素赋值。由于我们传递的参数是back_inserter返回的迭代器,因此每次赋值都会在vc上调用push_back(是向空容器添加元素,而不再是简简单单的对空容器的元素赋值这一未定义行为)。最终,这条fill_n调用语句向vec的末尾添加了10个元素,每个元素的值都是0。

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

插入迭代器是否与“标准库算法不会改变它们所操作的容器的大小”相悖

严格来说,标准库算法根本不知道有“容器”这个东西。它们只接受迭代器参数,运行于这些迭代器之上,这些迭代器只能顺序或随机访问容器中的元素,造成的效果就是算法只能读取元素、改变元素值、移动元素,但无法添加或删除元素。

但当我们传递给算法插入器,例如back_inserter时,由于这类迭代器能调用下层容器的操作(如push_back)来向容器插入元素,造成的算法执行的效果就是向容器中添加了元素。

因此,关键要理解:标准库算法从来不直接操作容器,它们只操作迭代器,从而间接访问容器。能不能插入和删除元素,不在于算法,而在于传递给它们的迭代器是否具有这样的能力。


拷贝(copy)算法

拷贝算法是另一个向目的位置迭代器指向的输出序列中的元素写入数据的算法。

  • 接受三个迭代器,前两个迭代器设置范围,第三个表示目标序列的起始位置。
  • 将前两个迭代器划定的输入范围中的元素拷贝到第三个迭代器指定的目标序列中。
  • 后面的序列必须能容纳前面的元素

可以实现内置数组的拷贝:

  int a1[] = {0, 1, 2, 3, 4};int a2[sizeof(a1)/sizeof(*a1)]; // 与a1大小一样auto ret = copy(begin(a1), end(a1), a2); // 把a1的内容拷贝给a2// ret指向拷贝到a2的尾元素之后的位置

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

也可用来实现不改变原序列值的目的:

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

replace算法

replace算法读入一个序列,并将其中所有等于给定值的元素都改为另一个值。

此算法接受4个参数:

  • 前两个是迭代器,表示输入序列;
  • 后两个一个是要搜索的值;
  • 另一个是新值。它将所有等于第一个值的元素替换为第二个值。
replace(vc.begin(), vc.end(), 0, 2); // 将序列中所有0都替换为2

如果我们希望保留原序列不变,可调用replace_copy。该算法接受额外第三个迭代器参数,指出调整后序列的保存位置:

replace_copy(vc.cbegin(), vc.cend(), back_inseret(ivec), 0, 2);

调用后,vc未改变,ivec包含vc的一份拷贝,ivec中原vc对应位置的0都变为2。


这里的容器大小指的是元素数量

举个例子:

即使我们用reserve分配了至少能容纳10个int的内存空间。但调用fill_n的行为仍然是未定义的。

这是因为泛型算法对于容器的要求并不是有足够的空间,而是足够的元素数量。

而reserve调整的是capacity的值,而非size的值(下面两图),换言之,此时vc依然为空,没有任何元素。而算法又不具备向容器添加元素的能力, 因此fill_n仍然失败。




定制操作

上面说过,sort算法默认使用元素类型的<运算符。但我们可能遇见下列情况:

  • 我们希望的排序顺序与<所定义的顺序不同
  • 序列可能保存的是未定义<运算符的元素类型

这时就需要重载sort的默认行为。

谓词

谓词是一个可调用的表达式,返回结果是一个能用做条件的值。

标准库算法使用的谓词分为两类:一元谓词(只接受单一参数)和二元谓词(有两个参数)。接受谓词参数的算法对输入序列中的元素调用谓词。因此,元素类型必须能转换为谓词的参数类型。

实例:
按照string的<运算符排序:

使单词按照长度排序,相同长度的单词按照字典序排列:

stable

带有stable的函数可保证相等元素的原本相对次序在排序后保持不变

实例,找出vector<string>内长度大于等于5的元素:

partition有时会打乱原有相对次序:

stable_partition会在排序的基础上尽量维持原来的相对次序:


可调用对象

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

可调用:对于一个对象或一个表达式,如果可以对其使用调用运算符——(),则称它为可调用的。

可调用对象有四种:

  1. 函数
  2. 函数指针
  3. 重载了函数调用运算符的类
  4. lambda表达式

lambda表达式

概念

有时我们希望进行的操作需要更多的参数,以至于超出了算法对为此的限制(二元谓词无法满足需求)。此时就可以使用lambda表达式。

一个lambda表达式表示一个可调用的代码单元。我们可以将其理解为一个未命名的内联函数。

与任何函数类似,一个lambda具有一个返回类型、一个参数列表和一个函数体。 但与函数不同,lambda可能定义在函数内部。一个lambda表达式具有如下形式:

  • capture list(捕获列表)是一个lambda所在函数中定义的局部变量的列表(通常为空);
  • return 表示返回类型。
  • parameter list表示参数列表。
  • function body表示函数体。
  • 与普通函数不同,lambda必须使用尾置返回来指定返回类型。
  • 我们可以忽略参数列表和返回类型,但必须永远包含捕获列表和函数体。
  • lambda的调用方式与普通函数的调用方式相同,都是使用调用运算符。
  • lambda不能有默认实参。换言之,调用的实参数目永远与形参数目相等。
  • lambda表达式之间不能相互赋值,即使看起来类型相同。

在lambda中忽略括号和参数列表等价于指定一个空参数列表。

关于返回类型:

  • 如果忽略返回类型,lambda根据函数体中的代码推断出返回类型。
  • 如果函数体只是一个return语句,则返回类型从返回的表达式的类型推断而来。
  • 如果包含任何单一return语句之外的内容,则返回类型为void,此时lambda不能返回值。(C++11)

一个仅忽略返回类型的实例:

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

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

一个lambda表达式表示一个可调用的代码单元。我们可以将其看为一个未命名的内联函数:


原理

当定义了lambda表达式之后,编译器按照仿函数的形式自动生成一个lamber_uuid类(本质还是未命名的),并重载其中的operator(),当用户进行调用的时候会自动通过该类的匿名对象来调用,使得其看起来和普通的函数一样。

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

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


捕获列表

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

捕获列表允许的形式:

下面我们通过实例来详细了解:

find_if

用find_if寻找vs中长度大于5的元素。

find_if返回一个迭代器,指向第一个长度不小于给定参数的元素。如果这样的元素不存在,返回尾后迭代器的拷贝。

for_each

通过for_each算法来打印find_if找到的符合要求的元素。

接受一个可调用对象,并对输入序列中每个元素调用此对象。

捕获方式

值捕获

采用值捕获的前提是变量可以拷贝。与传值参数不同的是,被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝:

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

mutable

默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。

如果我们希望能改变一个被捕获的变量的值,必须在参数列表首加上关键字mutable。 但是由于上面的被捕获变量的值是在创建lambda时拷贝的,因此在lambda表达式内修改变量值也不会影响值本身:


引用捕获

在lambda函数体内使用引用捕获的变量时,实际上使用的是引用所绑定的对象。

引用捕获与返回引用有着相同的问题:如果我们采用引用方式捕获一个变量,就必须确保被引用的对象在lambda执行的时候是存在的。lambda捕获的都是局部非static变量,这些变量在函数结束后就不复存在了。如果lambda可能在函数结束后执行,捕获的引用指向的局部变量已经消失。

修改引用捕获的变量

引用捕获是否可以修改依赖于引用指向的是否是一个const类型。在lambda表达式内修改引用捕获同样会导致变量改变:


隐式捕获

隐式捕获:通过在捕获列表中写一个&或者=指示编译器推断捕获列表,=表示采用值捕获,&表示采用引用捕获。

// st为隐式捕获,方式为值捕获
auto f = [=]{return st;};

也可以混用隐式捕获和显示捕获,但列表中第一个元素必须是一个&或者=,指定隐式捕获方式为引用或值,且显式捕获的变量必须用与隐式捕获不同的方式(显式为引用捕获隐式必须为值捕获,反之亦然):

int st,c;auto f1 = [=, &c]{return st + c;}; // st隐式、值捕获;c显式、引用捕获
auto f2 = [&, st]{return st + c;}; // c隐式、引用捕获;st显式、值捕获

function模板

概念

C++中可调用对象(如函数指针,仿函数,lambda表达式等)的虽然都有一个比较统一的操作形式,但是定义方法五花八门,这样就导致使用统一的方式保存可调用对象或者传递可调用对象时,会十分繁琐。C++11中提供了std::function和std::bind统一了可调用对象的各种操作。

例如:

// 普通函数
int add(int a, int b){return a+b;} // lambda表达式
auto mod = [](int a, int b){ return a % b;}// 仿函数
struct divide{int operator()(int denominator, int divisor){return denominator/divisor;}
};

上面的几种不同的可调用对象虽然类型不同,但是根据参数和返回值可以共享同一种调用形式int(int ,int),通过function就可以统一其调用形式:

std::function<int(int ,int)>  a = add;
std::function<int(int ,int)>  b = mod ;
std::function<int(int ,int)>  c = divide();

定义格式:std::function<返回值(参数列表)> 名字

  • std::function 是一个可调用对象包装器,是一个类模板,可以容纳除了类成员函数指针之外的所有可调用对象,它可以用统一的方式处理函数、函数对象、函数指针,并允许保存和延迟它们的执行。
  • std::function可以取代函数指针的作用,因为它可以延迟函数的执行,特别适合作为回调函数使用。它比普通函数指针更加的灵活和便利。

function与重载函数

在使用function的时候还有一个需要注意的点,就是我们不能将重载过的函数直接放入function对象中,因为会有二义性的问题。

例如:

int add(int x, int y);
double add(double x, double y);map<string, function<int(int, int)>> map;
map.insert({"+", add});//此时无法判断是哪个add//所以此时就不能直接使用函数名进行插入
//可以通过使用函数指针来指向对应函数,再通过函数指针插入来消除二义性
int (*func)(int, int) = add;
map.insert({"+", func});


bind函数

概念

定义在functional中。相当于一个通用的函数适配器。

调用bind的一般形式为:

auto newCallable = bind(callable, arg_list);
  • newCallable本身是一个可调用对象
  • arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。即,当我们调用newCallable时,newCallable会调用callable,并传递给它arg_list中的参数。

arg_list中的参数可能包含形如_n的元素,其中n是一个整数。这些参数是 “占位符”,表示newCallable的参数,它们占据了传递给newCallable的参数的“位置”。数值n表示生成的可调用对象中参数的位置: _1为newCallable的第一个参数,_2为第二个参数,依此类推。

使用placeholders名字

名字_n都定义在一个名为placeholders的命名空间中,这个命名空间本身定义在std命名空间。

为了使用_1我们必须做出以下声明:

using std::placeholders::_1

说明_1定义在命名空间placeholders中,而placeholders又定义在命名空间std中。

这样写也有弊端:我们必须为每个_n提供单独的using声明,太过繁琐。 因此我们可以使用另外一种形式:

using namespace namespace_name;

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

因此我们可以使用下面这两种形式:

using namespace std::placeholders;using namespace std;
using namespace placeholders;

与bind函数一样,placeholders命名空间也定义在functional头文件中。


作用

削减参数数量

函数作为可调用对象,lambda表达式能出现的地方函数也可以。但问题是算法要求一元谓词,函数的形参大于一个怎么办?lambda可以通过捕获列表解决一元谓词的限制。函数可以通过bind函数适配器削减参数数量。

还是以之前find_if函数为例:

我们可以通过形如这样的函数起到lambda表达式的作用:

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

但是由于find_if接受一个一元谓词,因此传递给find_if的可调用对象必须接受单一参数。check_size接受两个参数显然不符合要求。那么怎么解决呢?lambda将sz放入捕获列表,check_size可以使用bind函数,用一个定值作为其大小参数来调用check_size:

auto check5 = bind(check_size, _1, 5);
// _1形式亦可写为placeholders::_1
// 前者需要添加语句 using namespace placeholders;

此bind调用只有一个占位符,表示check5只接受单一参数。占位符出现在arg_list的第一个位置,表示check5的此参数对应check_size的第一个参数。此参数是一个const string&。因此,调用check6必须传递给它一个string类型的参数,check6会将此参数传递给check_size。

将原来基于lambda的find_if调用替换为使用bind的版本:

bool check_size(const string& s, string::size_type sz){return s.size() >= sz;
}int main(int argc, char const *argv[]) {vector<string> vs = {"daa","bbasdf","ccccc","dddd","aebbbb"};sort(vs.begin(), vs.end(), isshorter);auto check5 = bind(check_size,  placeholders::_1, 5);auto flag = find_if(vs.begin(), vs.end(), check5);for_each(flag, vs.end(),[](const string& s){cout << s << " ";});cout << endl;
}

输出结果:


重排参数顺序

bool isshorter(const string& s1, const string& s2){return s1.size() < s2.size();
}

按单词长度由短至长排序:

sort(words.begin(), words.end(), isshorter);

按单词长度由长至短排序:

sort(words.begin(), words.end(), bind(isshorter, _2, _1));

在第一个调用中,当sort需要比较两个元素A和B时,它会调用isshorter(A,B)。

在第二个对sort的调用中,传递给isshorter的参数被交换过来了。因此,当sort比较两个元素时,就好像调用isshorter(B,A)一样。


绑定引用参数

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

我们想用函数替换下面lambda表达式:

ostream& os = cout;
for_each(flag, vs.end(),[&os](const string& s) {os << s << " ";});

目标功能函数:

ostream &print(ostream & os, const string&s){return os << s;
}

如果要用它替换lambda就必须用到bind:

ostream &os(cout);
for_each(flag, vs.end(), bind(print, os, _1));

但是这样做是错误的,我们说,bind拷贝参数列表给返回的可调用对象,ostream是不能拷贝的。 如果希望传递给bind一个对象而又不拷贝它,就必须使用标准库ref函数

for_each(flag, vs.end(), bind(print, ref(os), _1));

值得一提的是, 在编译器中对ostream的引用不加ref是不报错的,但是不能运行。换言之,检查某个参数bind能否拷贝是程序员的责任。

函数ref返回一个对象,包含给定的引用。返回的对象是可拷贝的。

类似的还有cref函数,生成一个保存const引用的类。

与bind一样,函数ref和cref也定义在头文件functional中。



泛型算法结构——迭代器类别

概念

算法分类方式可以如同一开始所写那样,分为:是否读、写或者是重排序类中的元素。

也可以从迭代器入手进行分类:

不同的算法要求其迭代器提供的操作不同。某些算法,如find,只要求通过迭代器访问元素、递增迭代器以及比较两个迭代器是否相等这些能力。其他一些算法,如sort,还要求读、写和随机访问元素的能力。算法所要求的迭代器操作可以分为5个迭代器类别(iterator category),每个算法都会对它的每个迭代器参数指明须提供哪类迭代器。


不太重要的概念

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

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

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

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


迭代器类别

输入迭代器

输入迭代器(input iterator):只读,不写;单遍扫描,只能递增。 一个输入迭代器必须支持:

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

输入迭代器只用于顺序访问。 对于一个输入迭代器,*it++保证是有效的,但递增它可能导致所有其他指向流的迭代器失效(私以为也就是值被读取出来了,其他指向流的迭代器指向的元素没有了,就会导致它们失效)。其结果就是,不能保证输入迭代器的状态可以保存下来并用来访问元素。因此,输入迭代器只能用于单遍扫描算法。(私以为类似于输入流没法被读取第二次。) 算法find和accumulate要求输入迭代器;而istream_iterator是一种输入迭代器。


输出迭代器

输出迭代器(output iterator):只写而不读元素;单遍扫描,只能递增。 输出迭代器必须支持:

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

我们只能向一个输出迭代器赋值一次。 用作目的位置的迭代器通常都是输出迭代器。例如,copy函数的第三个参数就是输出迭代器。ostream_iterator类型也是输出迭代器。


前向迭代器

前向迭代器(forward iterator):可以读写元素;多遍扫描,只能递增。这类迭代器只能在序列中沿一个方向移动。

  • 支持所有输入和输出迭代器的操作
  • 可以多次读写同一个元素
  • 可以保存前向迭代器的状态

算法replace要求前向迭代器。forward_list上的迭代器是前向迭代器。


双向迭代器

双向迭代器(bidirectional iterator):可以正向/反向读写序列中的元素。

  • 支持所有前向迭代器的操作
  • 支持前置和后置递减运算符(–)

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


随机访问迭代器

随机访问迭代器(random-access iterator):提供在常量时间内访问序列中任意元素的能力。

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

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



算法的命名规范

_if版本的算法

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

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

这两个算法提供了命名上差异的版本,而非重载版本,因为两个版本的算法都接受相同数目的参数,因此可能产生重载歧义。 虽然很罕见,但为了避免任何可能的歧义,标准库选择提供不同名字的版本而不是重载。


_copy版本的算法

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

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

两个算法都调用了lambda(参见10.3.2节,第346页)来确定元素是否为奇数。在第一个调用中,我们从输入序列中将奇数元素删除。在第二个调用中,我们将非奇数(亦即偶数)元素从输入范围拷贝到v2中。



list和forward_list独有的算法

概念

与其他容器不同,链表类型list和forward_list定义了几个成员函数形式的算法。例如,它们定义了独有的sort、merge、remove、reverse和unique。通用版本的sort要求随机访问迭代器,因此不能用于list和forward_list,因为这两个类型分别提供双向迭代器和前向迭代器。

链表类型定义的独有算法中,部分算法的通用版本可以用于链表。(显然上面说的sort不在“部分”中) 但代价太高。这些算法需要交换输入序列中的元素。一个链表可以通过改变元素间的链接而不是真的交换它们的值来快速“交换”元素。 因此,这些链表版本的算法的性能比对应的通用版本好得多。



链表数据结构特有的splice算法

没有通用版本。

多数链表特有的算法都与其通用版本很相似,但不完全相同。

链表特有版本与通用版本间的一个至关重要的区别是链表版本会改变底层的容器。例如,remove的链表版本会删除指定的元素。unique的链表版本会删除第二个和后继的重复元素。

类似的,merge和splice会销毁其参数。

例如,

  • 通用版本的merge将合并的序列写到一个给定的目的迭代器;两个输入序列是不变的。
  • 而链表版本的merge函数会销毁给定的链表——元素从参数指定的链表中删除,被合并到调用merge的链表对象中。在merge之后,来自两个链表中的元素仍然存在,但它们都已在同一个链表中。

泛型算法(lambda表达式、function类模板、bind函数适配器、迭代器类别、链表数据结构独有的算法)相关推荐

  1. C++11 lambda表达式、function类模板、bind函数适配器

    文章目录 lambda表达式 lambda表达式的语法 lambda表达式的原理 function模板 function与重载函数 bind函数适配器 lambda表达式 当我们在写代码的时候如果经常 ...

  2. C++设计模式由浅入深(二)—— 类模板和函数模板

    二.类和函数模板 C++的模板编程特性是一个又大又复杂的话题,有许多著作专门传授这种特性和技巧.在本书中,我们会用到许多C++中的高级泛型编程特性.那么我们该如何去理解贯穿次数中的这些语言结构呢?本章 ...

  3. 模板 (函数模板语法 ,类模板与函数模板的区别,:函数模板案例,普通函数与函数模板的区别,普通函数与函数模板调用规则,模板的局限性,类模板分文件编写.cpp,Person.hpp,类模板与友元)

    **01:函数模板语法: #include<iostream> using namespace std;//交换两个整型函数 void swapInt(int &a ,int &a ...

  4. C++_static,类模板、函数模板、namespace

    C++_static,类模板.函数模板.namespace 1.static 2.类模板 3.函数模板 4.namespace 5.深入,更多细节 参考:侯捷<C++面向对象高级编程>

  5. c++模板--2(模板机制,模板的局限性,类模板,类模板做函数的参数)

    函数模板机制结论 编译器并不是把函数模板处理成能狗处理任何类型的函数 函数模板通过具体类型产生不同的函数 编译器会对函数模板进行两次编译,在声明的地方对模板代码的本身进行编译,在调用的地方对参数替换后 ...

  6. C++提高部分_C++类模板成员函数类外实现---C++语言工作笔记092

    然后我们再来说,类模板成员函数的类外实现 上面我们写一个Person类,然后 类模板template 然后写一个Person构造方法,然后,写一个showPerson方法 然后后面我们要用字符串,我们 ...

  7. C++提高部分_C++类模板与函数模板的区别---C++语言工作笔记088

    然后我们再去看看类模板和函数模板的区别, 类模板没有自动类型推导,这一种使用方式. 类模板在模板参数列表中是可以有默认参数的. 用例子去说明一下,可以看到我们写了一个Person类,然后 这个类有两个 ...

  8. 类模板与函数模板区别

    类模板与函数模板区别主要有两点 类模板没有自动类型推导的使用方式 类模板在模板参数列表中可以有默认参数 测试代码 #include <iostream> #include <stri ...

  9. C++模板学习02(类模板)(类模板语法、类模板与函数模板的区别、类模板中的成员函数创建时机、类模板对象做函数参数、类模板与继承、类模板成员函数类外实现、类模板分文件编写、类模板与友元)

    C++引用详情(引用的基本语法,注意事项,做函数的参数以及引用的本质,常量引用) 函数高级C++(函数的默认参数,函数的占位参数,函数重载的基本语法以及注意事项) C++类和对象-封装(属性和行为作为 ...

最新文章

  1. python 培训-Python培训周末班|python是什么?能干什么?
  2. [CF460E]Roland and Rose
  3. Qracle学习:初识oracle
  4. 采用redis+ThreadLocal获取全局的登录用户信息(一)
  5. 使用C#编程解决数独求解过程(从图片识别到数独求解)第二篇
  6. python实现栈_Python 栈
  7. JavaScript let、const与var的区别
  8. ResultSet的getDate()、getTime()和getTimestamp()比较
  9. 从C#到Objective-C,循序渐进学习苹果开发(2)--Objective-C和C#的差异
  10. python自定义排序规则_python自定义排序
  11. Java之消息摘要之commons codec
  12. resize2fs调整ext2\ext3\ext4文件系统的大小
  13. 主题:一个成功的RIA技术需要满足的条件(转)
  14. 【翻译】Flux安全审计已经结束
  15. QLCDNumber设置背景色和显示数字颜色
  16. Android平台epub阅读器推荐
  17. console的基础使用
  18. js getday()获取值不对
  19. 【计算机系统结构】Micro-operation微操作
  20. java模拟三人爬山_java笔记——模拟多人徒步爬山例子

热门文章

  1. ssm框架里面前端拿HTML写,ssm框架引入Vue,声明式渲染,标签的href拼接字符串
  2. python批量转换图片格式_python批量将图片转换为JPEG格式
  3. java 采样_Java编程实现beta分布的采样或抽样实例代码
  4. linux打开另一台linux的图形化,【工具大道】ssh登录Linux服务器,并显示图形化界面...
  5. 龙芯购买MIPS指令集的授权
  6. WinCE系统的编译过程
  7. python table对象_(RPA学习):Python-docx 常用方法
  8. 【转】TCP/IP协议--TCP的超时和重传
  9. nextcloud php 版本,Nextcloud版本升级教程
  10. java引入resource下的模板_Beetl自定义ResourceLoader,实现特殊的模板加载需求