10.泛型算法

news2024/11/15 7:16:18

文章目录

      • *泛型算法*
        • *10.1概述*
        • *10.2初识泛型算法*
          • *10.2.1只读算法*
            • *算法和元素类型*
            • *操作两个序列的算法*
          • *10.2.2写容器元素的算法*
            • *算法不检查写操作*
            • *介绍`back_inserter`*
            • *拷贝算法*
          • *10.2.3重排容器元素的算法*
            • *消除重复单词*
            • *使用`unique`*
        • *10.3定制操作*
          • *10.3.1向算法传递函数*
            • *谓词*
            • *排序算法*
          • *10.3.2`lambda`表达式*
            • *介绍`lambda`*
            • *向`lambda`传递参数*
            • *使用捕获列表*
            • *调用`find_if`*
            • *`for_each`算法*
            • *完整的`biggies`*
          • *10.3.3`lambda`捕获和返回*
            • *值捕获*
            • *引用捕获*
            • *隐式捕获*
            • *可变`lambda`*
            • *指定`lambda`返回类型*
          • *10.3.4参数绑定*
            • *标准库`bind`函数*
            • *`bind`的参数*
            • *绑定引用参数*
        • *10.4再探迭代器*
          • *10.4.1插入迭代器*
          • *10.4.2`iostream`迭代器*
            • *`istream_iterator`操作*
            • *使用算法操作流迭代器*
            • *`istream_iterator`允许使用懒惰求值*
            • *`ostream_iterator`操作*
            • *使用流迭代器处理类类型*
          • *10.4.3反向迭代器*
            • *反向迭代器和其他迭代器间的关系*
        • *10.5泛型算法结构*
          • *10.5.1五类迭代器*
            • *迭代器类别*
          • *10.5.2算法形参模式*
            • *接受单个目标迭代器的算法*
            • *接受第二个输入序列的算法*
          • *10.5.3算法命名规范*
            • *一些算法使用重载形式传递一个谓词*
            • *`_if`版本的算法*
            • *区分拷贝元素的版本和不拷贝的版本*
        • *10.6特定容器算法*
            • *`splice`成员*
            • *链表特有的操作会改变容器*

泛型算法

10.1概述

大多数算法都定义在头文件algorithm中。标准库还在头文件numeric中定义了一组数值泛型算法。一般情况下,这些算法并不直接操作容器,而是遍历由两个迭代器指定的一个元素范围来进行操作。

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

int val = 42;	// 将查找的值
auto result = find(vec.cbegin(), vec.cend(), val);  // 包头不包尾
// 报告结果
cout << "The value " << val
	<< (result == vec.cend() ? " is not present" : " is present") << endl;

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

10.2初识泛型算法

除了少数例外,标准库算法都对一个范围内的元素进行操作,将此元素范围称为输入范围

10.2.1只读算法
算法和元素类型

accumulate函数接受三个参数:前两个指出了需要求和的元素的范围,第三个参数是和的初值:int sum = accumulate(vec.cbegin(), vec.cend(), 0);
accumulate的第三个参数的类型决定了函数中使用哪个加法运算符以及返回值的类型。额外注意,第三个参数不能是字符串字面值,原因在于,如果传递了一个字符串字面值,用于保存和的对象的类型将是const char*,而其并没有加法运算符。
最佳实践:对于只读取而不改变元素的算法,通常最好使用cbein()cend()。但是,如果计划使用算法返回的迭代器来改变元素的值,就需要使用beginend的结果作为参数。

操作两个序列的算法

equal用于确定两个序列是否保存相同的值。它将第一个序列中的每个元素与第二个序列中的对应元素进行比较。如果所有对应元素都相等,则返回true,否则返回false
此算法接受三个迭代器:前两个表示第一个序列中的元素范围,第三个表示第二个序列的首元素。需要注意,equal基于一个非常重要的假设:它假定第二个序列至少与第一个序列一样长。此算法要处理第一个序列中的每个元素,它假定每个元素在第二个序列中都有一个与之对应的元素。
那些只接受一个单一迭代器来表示第二个序列的算法,都假定第二个序列至少与第一个序列一样长

10.2.2写容器元素的算法

一些算法将新值赋予序列中的元素。使用这类算法时,必须注意确保序列原大小至少不小于要求算法写入的元素数目。记住,算法不会执行容器操作,因此它们自己不可能改变容器的大小
一些算法会自己向输入范围写入元素,这些算法本质上并不危险,它们最多写入与给定序列一样多的元素。
fill接受一对迭代器表示一个范围,还接受一个值作为第三个参数。fill将给定的这个值赋予输入序列中的每个元素:fill(vec.begin(), vec.end(), 0); // 将每个元素重置为0
关键概念:迭代器参数。用一个单一迭代器表示第二个序列的算法都假定第二个序列至少与第一个一样长。如果第二个序列是第一个序列的一个子集,则程序会产生一个严重错误——会试图访问第二个序列中末尾之后(不存在)的元素。

算法不检查写操作

一些算法接受一个迭代器来指出一个单独的目的位置。这些算法将新值赋予一个序列中的元素,该序列从目的位置迭代器指向的元素开始。
fill_n接受一个单迭代器、一个计数值和一个值,它将给定值赋予迭代器指向的元素开始的指定个元素。fill_n假定写入指定个元素是安全的。即:fill_n(dest, n, val)假定dest指向一个元素,而从dest开始的序列至少包含n个元素:

vector<int> vec;
// 注意第二个参数是vec.size(),而不是指定的计数值
fill_n(vec.begin(), vec.size(), 0); // 将所有元素重置为0

一个初学者非常容易犯的错误是在一个空容器上调用fill_n(或类似的写元素的算法):fill_n(vec, 10, 0); // vec中并没有元素
向目的位置迭代器写入数据的算法假定目的位置足够大,能容纳要写入的元素

介绍back_inserter

一种保证算法有足够元素空间来容纳输出数据的方法是使用一种向容器中添加元素的迭代器,即插入迭代器back_inserter接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器。当通过此迭代器赋值时,赋值运算符会调用push_back将一个具有给定值的元素添加到容器中。常常使用back_inserter来创建一个迭代器,作为算法的目的位置来使用

vector<int> vec;
fill_n(back_inserter(vec), 10, 0);  // 将10个元素逐个赋予到back_inserter返回的迭代器,每次赋值都调用push_back
拷贝算法

拷贝(copy)算法接受三个迭代器,前两个表示一个输入范围,第三个表示目的序列的起始位置。此算法将输入范围中的元素拷贝到目的序列中。传递给copy的目的序列至少要包含与输入序列一样多的元素,这一点很重要。该函数返回的是其目的位置迭代器(递增后)的值。

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

多个算法都提供所谓的"拷贝"版本。这些算法计算新元素的值,但不会将它们放置在输入序列的末尾,而是创建一个新序列保存这些结果。
replace读入一个序列,并将其中所有等于给定值的元素都改为另一个值:

replace(ilist.begin(), ilist.end(), 0, 42);  // 将值为0的元素改为42

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

replace_copy(ilist.cbegin(), ilist.cend(), back_inserter(ivec), 0, 42);
// ilist并未改变,ivec包含ilist的一份拷贝,不过原来在ilist中值为0的元素在ivec中都变为42
10.2.3重排容器元素的算法

某些算法会重排容器中元素的顺序,一个明显的例子是sort,它是利用元素类型的<运算符来实现排序的。

消除重复单词

为了消除重复单词,首先将vector排序,使得重复的单词都相邻出现。一旦排序完毕,就可以使用另一个称为unique的标准库算法来重排vector,使得不重复的元素出现在vector的开始部分。由于算法不能执行容器的操作,将使用vectorerase成员来完成真正的删除操作。

// 按字典排序并删除重复元素
void elimDups(vector<string> &words) {
    sort(words.begin(), words.end());
    // 返回指向不重复区域之后一个位置的迭代器
    auto end_unique = unique(words.begin(), words.end());
    words.erase(end_unique, words.end());
}
使用unique

unique算法重排输入序列,将相邻的重复项"消除"(并不是真正的删除,只是覆盖相邻的重复元素,使得不重复元素出现在序列的开始部分),并返回一个指向最后一个不重复元素之后的位置的迭代器。此位置之后的元素仍然存在,但不知道它们的值是什么。
如果没有重复元素,则unique会返回指向尾后元素的迭代器

10.3定制操作

很多算法都会比较输入序列中的元素。默认情况下,这类算法使用元素类型的<==运算符完成比较。标准库还为这些算法定义了额外的版本,允许提供自己定义的操作来代替默认运算符。

10.3.1向算法传递函数
谓词

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

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

sort(words.begin(), words.end(), isShorter);
排序算法

stable_sort算法维持相等元素的原有顺序:

// 将words按字典序重排,并消除重复单词
elimDups(words);
// 按长度重新排序,长度相同的单词维持字典序
stable_sort(words.begin(), words.end(), isShorter);
10.3.2lambda表达式

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

介绍lambda

可以向一个算法传递任何类别的可调用对象。一个lambda表达式表示一个可调用的代码单元,
可以将其理解为一个未命名的内联函数:

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

其中,capture list(捕获列表)是一个lambda所在函数中定义的局部变量的列表(通常为空)。
需要注意,lambda必须使用尾置返回来指定返回类型,可以忽略参数列表和返回类型,但必须永远包含捕获列表和函数体。并且,其调用方式与普通函数的调用方式相同,都是使用调用运算符()

auto f = [] { return 42; };
cout << f() << endl;

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

lambda传递参数

与普通函数不同,lambda不能有默认实参。因此,一个lambda调用的实参数目永远与形参数目相等。

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

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

使用捕获列表

虽然一个lambda可以出现在一个函数中,使用其局部变量,但它只能使用那些明确指明的变量。一个lambda通过将局部变量包含在其捕获列表中来指出将会使用这些变量。捕获列表指引lambda在其内部包含访问局部变量所需的信息。

[sz](const string &a) { return a.size() >= sz; };
调用find_if

find_if的调用返回一个迭代器,指向第一个匹配的元素。如果这样的元素不存在,则返回尾后迭代器的一个拷贝。

// 这儿的words容器是按照单词长度进行升序排列了的
auto wc = find_if(words.begin(), words.end(), [sz] (const string &a) { 
    return a.size >= sz; 
});
auto count = words.end() - wc;  // 计算满足size >= sz的元素的数目

// 调用make_plural来输出"word"或"words"
cout << count << " " << make_plural(count, "words", "s") 
    << " of length " << sz << " or longer" << endl;
for_each算法

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

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

捕获列表只用于局部非static变量,lambda可以直接使用局部static变量和它所在函数之外声明的名字。例如,cout定义在头文件iostream中。因此,只要在该函数出现的作用域中包含了头文件iostream,那么lambda就可以使用cout

完整的biggies
void biggies(vector<string> &words, vector<string>::size_type sz) {
	// 将words按字典序排序,删除重复单词
    elimDups(words);
    // 按长度排序,长度相同的单词维持字典序
    stable_sort(words.begin(), words.end(), [] (const string &a, const string &b) { 
        return a.size() < b.size(); 
    });
    
    // 获取一个迭代器,指向第一个满足size() >= sz的元素
    auto wc = find_if(words.begin(), words.end(), [sz] (const string &a) { 
        return a.size() >= sz; 
    });
    // 计算满足size() >= sz的元素的数目
    auto count = words.end() - wc;

    cout << count << " " << make_plural(count, "words", "s") << " of length " << sz << " or longer" << endl;

	// 打印长度大于等于给定值的单词,每个单词后面接一个空格
    for_each(wc, words.end(), [] (const string &s) { 
        cout << s << " "; 
    });
    cout << endl;
}
10.3.3lambda捕获和返回

当定义一个lambda时,编译器生成一个与lambda对应的新的未命名的类类型。当向一个函数传递一个lambda时,同时定义了一个新类型和该类型的一个对象:传递的参数就是此编译器生成的类类型的未命名对象。默认情况下,从lambda生成的类都包含一个对应lambda所捕获的变量的数据成员。类似任何普通类的数据成员,lambda的数据成员也在lambda对象创建时被初始化。

值捕获

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

void fcn1() {
    size_t v1 = 42;
    auto f = [v1] { return v1; };
    v1 = 0;
    auto j = f();   // j为42;f保存了创建它时v1的拷贝
}
引用捕获
void fcn2() {
    size_t v1 = 42;
    auto f2 = [&v1] { return v1; };
    v1 = 0;
    auto j = f2();  // j为0;f2保存了v1的引用,而非拷贝
}

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

void biggies(vector<string> &words, vector<string>::size_type sz, 
    ostream &os = cout, char c = ' ') {
    for_each(words.begin(), words.end(), [&os, c] (const string &s) { 
        os << s << c; 
    });
}

也可以从一个函数返回lambda。函数可以直接返回一个可调用对象,或者返回一个类对象,该类含有可调用对象的数据成员。如果函数返回一个lambda,则与函数不能返回一个局部变量的引用类似,此lambda也不能包含引用捕获。
当以引用捕获一个变量时,必须保证在lambda执行时变量是存在的
一般来说,应该尽量减少捕获的数据量,来避免潜在的捕获导致的问题。而且,如果可能的话,应该避免捕获指针或引用。

隐式捕获

可以让编译器根据lambda体中的代码来推断要使用哪些变量。为了指示编译器推断捕获列表,应在捕获列表中写一个&=&告诉编译器采用捕获引用方式,=则表示采用值捕获方式:

wc = find_if(words.begin(), words.end(), [=] (const string &s) { 
    return s.size() >= 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; });
}

当混合使用隐式捕获和显式捕获时,捕获列表中的第一个元素必须是一个&=。并且,显式捕获的变量必须使用与隐式捕获不同的方式,即,如果隐式捕获使用的是引用方式,则显式捕获命名变量必须采用值方式。

在这里插入图片描述

可变lambda

默认情况下,对于一个值被拷贝的变量,lambda不会改变其值。如果希望能改变一个被捕获的变量的值,就必须在参数列表首加上关键字mutable。因此,可变lambda能省略参数列表:

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

一个引用捕获的变量是否(如往常一样)可以修改依赖于此引用指向的是一个const类型还是一个非const类型。

指定lambda返回类型

默认情况下,如果一个lambda体包含return之外的任何语句,则编译器假定此lambda返回void。与其他返回void的函数类似,被推断返回voidlambda不能返回值。
函数transform接受三个迭代器和一个可调用对象。前两个迭代器表示输入序列,第三个迭代器表示目的位置。算法对输入序列中每个元素调用可调用对象,并将结果写到目的位置。如果目的位置迭代器与表示输入序列开始位置的迭代器是相同的,则transform将输入序列中每个元素替换为可调用对象操作该元素得到的结果:

transform(vi.begin(), vi.end(), vi.begin(), [] (int i) {
    return i < 0 ? -i : i;
});

// 错误:编译器推断这个版本的lambda返回类型为void,但它返回了一个int值
transform(vi.begin(), vi.end(), vi.begin(), [] (int i) {
    if (i < 0) {
        return -i;
    } else {
        return i;
    }
});

// 显示定义返回类型
transform(vi.begin(), vi.end(), vi.begin(), [] (int i) -> int {
    if (i < 0) {
        return -i;
    } else {
        return i;
    }
});
10.3.4参数绑定

对于那种只在一两个地方使用的简单操作,lambda表达式是最有用的。如果需要在很多地方使用相同的操作,通常应该定义一个函数。类似的,如果一个操作需要很多语句才能完成,通常使用函数更好。
如果lambda的捕获列表为空,通常可以用函数来代替它。例如,既可以用一个lambda,也可以用函数isShorter来实现将vector中的单词按长度排序。
但是,对于捕获局部变量的lambda,用函数来替换它就不是那么容易了。例如,用在find_if调用中的lambda比较一个string和一个给定大小。可以很容易地编写一个完成同样工作的函数:

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

但是,不能用这个函数作为find_if的一个参数,原因在于find_if接受一个一元谓词,因此传递给find_if的可调用对象必须接受单一参数。为了用check_size来代替lambda,必须解决这类问题。

标准库bind函数

可以将bind函数看作一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来"适应"原对象的参数列表:auto newCallable = bind(callable, arg_list);。其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable参数。即,当调用newCallable时,newCallable会调用callable,并传递给它arg_list中的参数。
arg_list中的参数可能包含形如_n的名字,其中n是一个整数。这些参数是占位符,表示newCallable的参数,它们占据了传递给newCallable的参数的位置,数值n表示生成的可调用对象中参数的位置:_1newCallable的第一个参数,以此类推:

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

// 此bind调用只有一个占位符,表示只接受单一参数。占位符出现在arg_list的第一个位置,
// 表示此参数对应checkSize的第一个参数,且参数类型为const string&。因此,调用check6
// 必须传递给它一个string类型的参数,check6会将此参数传递给checkSize
auto check6 = bind(checkSize, std::placeholders::_1, 6);

auto wc = find_if(words.begin(), words.end(), bind(checkSize, _1, sz));
bind的参数

可以用bind修正参数的值,更一般的,可以用bind绑定给定可调用对象中的参数或重新安排其顺序:

auto g = bind(f, a, b, _2, c, _1);
g(x, y);    // 等价于f(a, b, y, c, x)

// 用bind重排参数顺序
sort(words.begin(), words.end(), isShorter);
sort(words.begin(), words.end(), bind(isShorter, _2, _1));
绑定引用参数

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

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

但是,不能直接用bind来代替对流对象的捕获。如果希望传递一个对象又不拷贝它,就必须使用标准库ref函数:

for_each(words.begin(), words.end(), bind(print, ref(os), _1, ' '));

函数ref返回一个对象,包含给定的引用,此对象是可以拷贝的。标准库还有一个cref函数,生成一个保存const引用的类。

10.4再探迭代器

标准库还定义了额外几种迭代器:

  • 插入迭代器:这些迭代器被绑定到一个容器上,可用来向容器插入元素。
  • 流迭代器:这些迭代器被绑定到输入或输出流上,可用来遍历所关联的IO流。
  • 反向迭代器:这些迭代器向后而不是向前移动。除了forward_list之外的标准库容器都有反向迭代器。
  • 移动迭代器:这些专用的迭代器不是拷贝其中的元素,而是移动它们。
10.4.1插入迭代器

插入器是一种迭代器适配器,它接受一个容器,生成一个迭代器,能实现向给定容器添加元素。当通过一个插入迭代器赋值时,该迭代器调用容器操作来向给定容器的指定位置插入一个元素。

在这里插入图片描述

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

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

理解插入器的工作过程是很重要的:当调用inserter(c, iter)时,得到一个迭代器,接下来使用它时,会将元素插入到iter原来所指向的元素之前的位置。即:如果it是由inserter生成的迭代器,则:

*it = val;

// 等价于

it = c.insert(it, val); // it指向新加入的元素
++it;   // 递增it使它指向原来的元素

front_inserter生成的迭代器的行为与inserter生成的迭代器完全不一样。当使用front_inserter时,元素总是插入到容器第一个元素之前:

list<int> lst = {1, 2, 3, 4};
list<int> lst2, lst3;
copy(lst.begin(), lst.end(), front_inserter(lst2)); // 调用push_front:4 3 2 1
copy(lst.begin(), lst.end(), inserter(lst3));   // 1 2 3 4
10.4.2iostream迭代器
istream_iterator操作

当创建一个流迭代器时,必须指定迭代器将要读写的对象类型。一个istream_iterator使用>>来读取流。因此,istream_iterator要读取的类型必须定义了输入运算符。当创建一个istream_iterator时,可以将它绑定到一个流。当然,还可以默认初始化迭代器,这样就创建了一个可以当作尾后值使用的迭代器:

istream_iterator<int> int_it(cin);  // 从cin读取int
istream_iterator<int> int_eof;  // 尾后迭代器
ifstream in("afile");
istream_iterator<string> 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(*int_it++);
}

// 可以将程序重写为如下形式,体现了istream_iterator更有用的地方
vector<int> vec(in_iter, eof);   // 从迭代器范围构造vec

在这里插入图片描述

使用算法操作流迭代器

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

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

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

ostream_iterator操作

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

在这里插入图片描述

可以用ostream_iterator来输出值的序列:

ostream_iterator<int> out_iter(cout, " ");
for (auto e : vec) {
    // 赋值语句实际上将元素写到cout。值得注意的是,当向out_iter赋值时,可以忽略解引用
    // 和递增运算,即等价于:outer_iter = e;但是,为了与其他迭代器的使用保持一致,
    // 更推荐下面的写法,如果想将此循环改为操作其他迭代器类型,修改起来非常容易。
    *out_iter++ = e;
}

cout << endl;

可以通过调用copy来打印vec中的元素,比编写循环更为简单:copy(vec.begin(), vec.end(), out_iter);

使用流迭代器处理类类型
istream_iterator<Sales_item> item_iter(cin), eof;
ostream_iterator<Sales_item> out_iter(cout, "\n");
// 将第一笔交易记录在sum中,并读取下一条记录。
Sales_item sum = *item_iter++;

while (item_iter != eof) {
	// 如果当前交易记录(存在item_iter中)有着相同的ISBN号
    if (item_iter->isbn() == sum.isbn()) {
        sum += *item_iter++;	// 将其加到sum上并读取下一条记录
    } else {
        out_iter = sum;	// 输出sum当前值
        sum = *item_iter++;	// 读取下一条记录
    }
}

out_iter = sum;	// 记得打印最后一组记录的和
10.4.3反向迭代器

在这里插入图片描述

反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器。对于反向迭代器,递增以及递减操作的含义会颠倒过来。递增一个反向迭代器会移动到前一个元素;递减一个迭代器会移动到下一元素。
除了forward_list之外,其他容器都支持反向迭代器。可以通过调用rbeginrendcrbegincrend成员函数来获得反向迭代器。这些成员函数返回指向容器尾元素和首元素之前一个位置的迭代器。
需要注意的是,流迭代器不支持递减运算,因为不可能在一个流中反向移动

反向迭代器和其他迭代器间的关系
// 在一个逗号分隔的列表中查找最后一个元素
auto rcomma = find(line.crbegin(), line.crend(), ',');
// 错误:将逆序输出单词的字符,即:如果输入的是FIRST,MIDDLE,LAST则这条语句会打印TSAL
cout << string(line.crbegin(), rcomma) << endl;
// 由于使用的是反向迭代器,会反向处理相关元素。因此,会从crbegin开始反向打印

而现在希望的是正常顺序打印,这个时候需要调用reverse_iteratorbase成员函数来完成这一转换,此成员函数会返回其对应的普通迭代器:

// 正确:得到一个正向迭代器,从逗号开始读取字符直到line末尾
cout << string(rcomma.base(), line.cend()) << endl;

在这里插入图片描述

rcommarcomma.base()指向不同的元素,line.crbegin()line.cend()也是如此。这些不同保证了元素范围无论是正向处理还是反向处理都是相同的。
从技术上讲,普通迭代器与反向迭代器的关系反映了左闭合区间的特性。关键点在于[line.crbegin(), rcomma)[rcomma.base(), line.cend())指向line中相同的元素范围。为了实现这一点,rcommarcomma.base()必须生成相邻位置而不是相同位置,crbegin()cend()也是如此。

10.5泛型算法结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n00Tp4DL-1649420854637)(evernotecid://95126015-5853-4CC7-AE3B-105960046EC5/appyinxiangcom/15766490/ENResource/p711)]

10.5.1五类迭代器

迭代器是按它们所提供的操作来分类的,而这种分类形成了一种层次。除了输出迭代器之外,一个高层类别的迭代器支持低层类别迭代器的所有操作。
C++标准指明了泛型和数值算法的每个迭代器参数的最小类别。例如,find算法在一个序列上进行一遍扫描,对元素进行只读操作,因此至少需要输入迭代器。replace函数需要一对迭代器,至少是前向迭代器。其第三个迭代器表示目的位置,必须至少是输出迭代器。其他的例子类似。对每个迭代器参数来说,其能力必须与规定的最小类别至少相当。向算法传递一个能力更差的迭代器会产生错误
对于向一个算法传递错误类别的迭代器的问题,很多编译器不会给出任何警告或提示。

迭代器类别

输入迭代器(类似于泛型算法中的输入范围,因此用来读取数据):可以读取序列中的元素。一个输入迭代器必须支持:

  • 用于比较两个迭代器的相等和不相等运算符。
  • 用于推进迭代器的前置和后置递增运算。
  • 用于读取元素的解引用运算符:解引用只会出现在赋值运算符的右侧。
  • 箭头运算符,等价于(*it).member,即,解引用迭代器,并提取对象的成员。

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

输出迭代器:可以看作输入迭代器功能上的补集——只写而不读元素。输出迭代器必须支持:

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

只能向一个输出迭代器赋值一次。类似输入迭代器,输出迭代器只能用于单遍扫描算法。用作目的位置的迭代器通常都是输出迭代器。例如,copy函数的第三个参数就是输出迭代器。ostream_iterator类型也是输出迭代器。

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

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

随机访问迭代器:提供在常量时间内访问序列中任意元素的能力。此类迭代器支持双向迭代器的所有功能,此外还支持迭代器的加减、比较运算:

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

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

10.5.2算法形参模式

在任何其他算法分类之上,还有一组参数规范。大多数算法具有以下几种形式之一:

// alg是算法的名字,beg和end表示算法所操作的输入范围。
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是一个直接指向容器的迭代器,那么算法将输出数据写到容器中已存在的元素内。更常见的情况是,dest被绑定到一个插入迭代器或者是一个ostream_iterator。插入迭代器会将新元素添加到容器中,因而保证空间是足够的。ostream_iterator会将数据写入到一个输出流,同样不管要写入多少个元素都没有问题。

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

这些算法通常使用第二个范围中的元素与第一个输入范围结合来进行一些运算。
接受单独的beg2的算法假定从beg2开始的序列与begend所表示的范围至少一样大

10.5.3算法命名规范
一些算法使用重载形式传递一个谓词

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

unique(beg, end);   // 使用==运算符比较元素
unique(beg, end, comp); // 使用comp比较元素
_if版本的算法

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

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

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

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

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

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

10.6特定容器算法

对于listforward_list,应该优先使用成员函数版本的算法而不是通用算法
例如,通用版本的sort要求随机访问迭代器,因此不能用于listforward_list,因为这两个类型分别提供双向迭代器和前向迭代器。而另外一些算法需要交换输入序列中的元素。一个链表可以通过改变元素间的链接而不是真的交换它们的值来快速交换元素。因此,这些链表版本的算法的性能比对应的通用版本好得多。

在这里插入图片描述

splice成员

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0tMU532F-1649420854637)(evernotecid://95126015-5853-4CC7-AE3B-105960046EC5/appyinxiangcom/15766490/ENResource/p639)]

链表特有的操作会改变容器

多数链表特有的算法都与其通用版本很相似,但不完全相同。链表特有版本与通用版本的一个至关重要的区别是链表版本会改变底层的容器

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/79934.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【JavaSE】那些异常

目录 1. 何为异常 2. 异常的分类 2.1 运行异常 / 非受查异常 &#xff1a; 3. 异常的处理思想 4. 异常的抛出 5. 异常的捕获 5.1 异常声明 throws 5.2 try-catch 捕获异常并处理 6. finally 7. 异常的处理流程 8. 自定义异常 1. 何为异常 在Java中&#xff0c;将程序执…

LeetCode 1884. 鸡蛋掉落-两枚鸡蛋 -- 动态规划

鸡蛋掉落-两枚鸡蛋 中等 60 相关企业 给你 2 枚相同 的鸡蛋&#xff0c;和一栋从第 1 层到第 n 层共有 n 层楼的建筑。 已知存在楼层 f &#xff0c;满足 0 < f < n &#xff0c;任何从 高于 f 的楼层落下的鸡蛋都 会碎 &#xff0c;从 f 楼层或比它低 的楼层落下的鸡蛋…

Jedis解读与建议

1. Jedis是什么&#xff1f; 官网 Jedis 是官方推荐的java客户端&#xff01;SpringBoot的RedisTemplate的底层也是Jedis&#xff1b; 2. 为什么使用池化&#xff1f; 背景&#xff1a; Redis为单进程单线程模式&#xff0c;采用队列模式将并发访问变为串行访问。Redis本身没…

leetcode:合并两个有序数组

合并两个有序数组1、题目描述2、解决方案3、代码实现1、题目描述 给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2&#xff0c;另有两个整数 m 和 n &#xff0c;分别表示 nums1 和 nums2 中的元素数目。 请你 合并 nums2 到 nums1 中&#xff0c;使合并后的数组同样按 非…

mutex 锁的理解和思考

并发的影响 goroutine 并发对数据做读写操作&#xff0c;如果没有锁的保护&#xff0c;得到的结果也就是不确定的。我们通过 goroutine 做累加的例子来看一下&#xff0c;下面的情况&#xff0c;我们预期进行了10次循环&#xff0c;每次加1&#xff0c;但执行的结果却不一定的…

离开外包之后,花了10000小时,最后我走进字节跳动拿到了offer

前言&#xff1a; 没有绝对的天才&#xff0c;只有持续不断的付出。对于我们每一个平凡人来说&#xff0c;改变命运只能依靠努力幸运&#xff0c;但如果你不够幸运&#xff0c;那就只能拉高努力的占比。 2020年7月&#xff0c;我有幸成为了字节跳动的一名Java后端开发&#x…

全志A33移植openharmony3.1标准系统之添加产品编译

想玩openharmony很久了,苦于没有合适的板子能让我玩,已经适配好的开发板可真是太贵了啊,所以还是得自己动手啊,毕竟还能深入了解openharmony系统,之前有在A33上把主线uboot和主线内核跑起来,而且drm也是可以正常显示了,现在就基于此将openharmony移植到开发板上。 首先在…

【服务器】基本概念

服务器 文章目录服务器1.概览1.1.本质: **数据接受&传递**, **数据存储**, **数据处理**1.2.种类1.3.单位:1.4.标准1.5.扩展1.6.逻辑架构1.7.缓存Cache:1.8.内存DIMM1.9.DDR1.10.硬盘ref1.概览 1.1.本质: 数据接受&传递, 数据存储, 数据处理 1.2.种类 按应用分类WWW…

Snowflake Decoded基础教程

Snowflake Decoded基础教程 掌握基本的Snowflake概念并获得必要的技能以开始实施基于Snowflake的解决方案 应用程序 课程英文名&#xff1a;Snowflake Decoded Fundamentals and hands on Training 此视频教程共10.0小时&#xff0c;中英双语字幕&#xff0c;画质清晰无水印…

docker启动镜像失败后用日志logs查找失败原因

我用一个自己做的镜像上传到了dockerhub中&#xff0c;然后使用windows拉取pull下来之后&#xff0c;启动不成功&#xff1a; 可以看到&#xff0c;虽然启动了&#xff0c;但是docker ps 后没有看到正在运行的容器&#xff0c;所以我就怀疑启动失败了&#xff0c;但是我又不知道…

关键字(四):goto和void

关键字一.具有争议的关键词—goto二.“空”的关键字—void1.void为什么不能定义变量2.void修饰函数返回值和参数3.void指针一.具有争议的关键词—goto goto语句非常灵活&#xff0c;其实就是从goto这个位置直接跳转到goto后面的那个数据&#xff08;end&#xff09;所对应的位置…

电力系统短期负荷预测(Python代码+数据+详细文章讲解)

&#x1f468;‍&#x1f393;个人主页&#xff1a;研学社的博客 &#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜…

整理 MySQL 常用函数,小伙伴们在也不发愁了

前言 为了简化操作&#xff0c;MySQL 提供了大量的函数&#xff0c;会对传递进来的参数进行处理&#xff0c;并返回一个处理结果&#xff0c;也就是返回一个值。MySQL 包含了大量并且丰富的函数&#xff0c;这里只是对 MySQL 常用函数进行简单的分类&#xff0c;大概包括数值型…

网站域名备案查询方法,批量查询网站域名备案的教程

网站域名备案查询方法&#xff0c;批量查询网站域名备案的教程 批量查域名备案操作步骤: 第一步、打开SEO综合查询工具。 第二步、添加需要查询的网站域名&#xff08;要查多少放多少&#xff0c;一行一个域名&#xff09;。 第三步、勾选域名ICP备案。 第四步、点击开始查询…

python带你体验唯美雪景,愿这个冬天的你,不缺暖阳

前言 嗨喽&#xff0c;大家好呀~这里是爱看美女的茜茜呐 又到了学Python时刻~ 大雪已至&#xff0c;冬天无恙&#xff0c;愿这个冬天的你&#xff0c;不缺暖阳&#xff0c;好日常在 一、画一朵小雪花 import turtle import time from turtle import * # codingutf-8def sno…

%00截断

%00截断 CVE-2013-4547 影响版本&#xff1a;Nginx 0.8.41 ~ 1.4.3 / 1.5.0 ~ 1.5.7 影响说明&#xff1a;绕过服务器策略&#xff0c;上传webshell 环境说明&#xff1a;Nginx 1.4.2 /test.jpg%00.php该漏洞利用了Nginx错误的解析了URL地址&#xff0c;导致可以绕过服务端限…

Armadillo与OpenCV矩阵数据mat、vec与Mat的相互转换

本文介绍在C 语言中&#xff0c;矩阵库Armadillo的mat、vec格式数据与计算机视觉库OpenCV的Mat格式数据相互转换的方法。 在C 语言的矩阵库Armadillo与计算机视觉库OpenCV中&#xff0c;都有矩阵格式的数据类型&#xff1b;而这两个库在运行能力方面各有千秋&#xff0c;因此实…

shiro-spring-boot-starter

第2.1.7章 WEB系统最佳实践Spring文件配置之spring-shiro.xml 2016年还在使用shiro&#xff0c;后来使用应用springboot之后&#xff0c;因为有了网关&#xff0c;感觉网关就可以做一些拦截&#xff0c;就没必要一定要使用shiro&#xff0c;如果你使用平台还需要每个系统自己做…

小白学编程(Java)20:判断一个数是否是质数

解题思路&#xff1a; 第一步&#xff1a;弄明白质数是什么&#xff1f; 质数的定义&#xff1a;质数又称素数。一个大于1的自然数&#xff0c;除了1和它自身外&#xff0c;不能被其他自然数整除的数叫做质数&#xff1b;否则称为合数&#xff08;规定1既不是质数也不是合数&am…

微信公众号开发——获取AccessToken接口调用凭据

&#x1f60a; 作者&#xff1a; 一恍过去&#x1f496; 主页&#xff1a; https://blog.csdn.net/zhuocailing3390&#x1f38a; 社区&#xff1a; Java技术栈交流&#x1f389; 主题&#xff1a; 微信公众号开发——获取AccessToken接口调用凭据⏱️ 创作时间&#xff1…