C++ Primer 学习笔记 第十章 泛型算法

2023-11-09

标准库容器很小,并未给每个容器添加大量功能,而是提供了一组算法,这些算法大多数都独立于任何特定的容器,这些算法是通用的,或者说是泛型的(generic),可用于不同类型容器和元素。

大多数泛型算法定义在头文件algorithm中,头文件numberic中也定义了一些。

一般,这些算法不直接操作容器,而是遍历由两个迭代器指定的一个元素范围来进行操作。

find:

int val = 42;
auto result = find(vec.cbegin(), vec.cend(), val);    // 如找到,返回指向它的迭代器,如没找到,返回vec.cend()

find操作的是迭代器,但数组上的指针也可以。

find工作步骤:
1.访问序列首元素。
2.比较。
3.匹配时返回标识此元素的值。
4.否则,找下一个元素,重复2、3。
5.如到达序列尾,find停止并返回一个指出元素未找到的值,此值的类型必须和步骤3的返回值类型具有相容类型。

以上步骤不依赖于容器保存的元素类型,因此,只要有一个迭代器可用来访问元素,find就完全不依赖于容器类型(甚至无须理会保存元素的是不是容器)。

迭代器令算法不依赖于容器(解引用访问元素、返回指向元素的迭代器、递增迭代器移动到下一个元素、尾后迭代器用来判断find是否到达序列末尾),但算法依赖于元素类型的操作(find用==完成每个元素与给定值的比较,其它算法可能要求元素类型支持<运算符,但大多数算法提供了一个方法,允许我们使用自定义的操作来代替默认的运算符)。

将int类型的值输入到向量中,之后使用count函数计算给定值出现的次数:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int func(int toBeFund) {
	vector<int> ivec;
	int i = 0;
	while (cin >> i) {
		ivec.push_back(i);
	}
	return count(ivec.begin(), ivec.end(), toBeFund);
}

int main() {
	cout << func(5) << endl;
}

泛型算法本身不会执行容器的操作,它们只会运行于迭代器之上,执行迭代器操作,这带来了一些特性:算法永远不会改变底层容器的大小,但可能改变容器中保存的元素的值,也可能在容器内移动元素,但永远不会直接添加(push_back等)或删除(erase等)元素。标准库还定义了一类特殊的迭代器,称为插入器,当给这类迭代器赋值时,它们会在底层的容器上执行插入操作,因此,当算法操作这样一个迭代器时,可以完成向容器添加元素的功能,但算法自身不会做这样的操作。

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

一些算法只会读取其输入范围内的元素,而从不改变元素,如find、count,称为只读算法。

accumulate也是一个只读算法,它定义在头文件numeric中,它的作用是将输入范围内的数字求和,第三个参数是和的初始值:

int sum = accumulate(vec.cbegin(), vec.cend(), 0);

accumulate的第三个参数决定了函数中使用哪个类型上的加法运算法以及返回值的类型。

accumulate将第三个参数作为求和的起点,这意味着元素类型加到和的起点上的操作必须是可行的。

accumulate用于string时:

string sum1 = accumulate(v.cbegin(), v.cend(), string(""));    // 正确
string sum2 = accumulate(v.cbegin(), v.cend(), "");    // 错误,const char *类型上没有定义+操作,造成编译错误

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

equal也是只读算法,它将第一个序列中的每个元素与第二个序列中的对应元素进行比较,如所有对应元素都相等,返回true,否则返回false。此算法接收三个参数,前两个与以往一样表示一个输入范围,第三个参数表示第二个序列的首元素的迭代器:

equal(roster1.cbegin(), roster1.cend(), roster2.cbegin());

equal函数比较的两个序列中的元素类型可以不同,只要两者能用==比较大小,如string和const char *。

equal有一个重要假设,第二个序列中元素数至少与输入范围中的元素数一样长。

	vector<double> ivec = { 0, 1.7, 2.5, 3.2, 4, 5 };
	cout << accumulate(ivec.begin(), ivec.end(), 0) << endl;    // 输出15,即每次累加时两个加数都忽略小数部分
	cout << accumulate(ivec.begin(), ivec.end(), 0.0) << endl;    // 输出16.4
	vector<double> ivec = { 1.7, 2.5 };
	cout << accumulate(ivec.begin(), ivec.end(), 0) << endl;    // 输出3
	
	int i = 1.7 + 2.5;
	cout << i << endl;    // 输出4
	vector<const char *> roster1 = { "aaa", "bbb", "ccc" };
	vector<const char *> roster2 = { "aaa", "bbb", "ccc" };
	cout << equal(roster1.begin(), roster2.end(), roster2.begin()) << endl;    // 输出0,因为比较的是两个指针,只有当指针值相等时才会为true
	
	const char* c1 = "aaa";
	const char* c2 = "aaa";
	cout << (c1 == c2) << endl;    // 输出1,因为内存中只有一份"aaa",两指针指向同一位置

一些算法将新值赋予序列中的元素,在使用这类算法时,必须保证序列原大小至少不小于我们要写入的大小,因为算法不会执行容器操作,因此它们不能改变容器大小。

如fill接受一对迭代器表示输入范围,还接受第三个参数,将第三个参数填充到输入范围内:

fill(vec.begin(), vec.end(), 0);    // 将每个元素置为0

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

传递第二个序列有两种方式,第一种是接受一个迭代器参数表示第二个序列中的首元素;第二种是接受两个迭代器表示第二个序列的范围。第一种时,假定第二个序列至少和第一个一样长,确保算法不会试图访问第二个序列中不存在的元素是程序员的责任。

fill_n接受一个单迭代器、一个计数值和一个值:

vector<int> vec;
fill_n(vec.begin(), vec.size(), 0);    // 将所有元素置为0

fill_n假定写入指定个元素是安全的,即假定dest指向一个元素,而从dest开始的序列至少包含n个元素。超出部分写入的结果是未定义的。

一种能保证算法有足够元素空间来容纳输入的方法是使用插入迭代器,我们通过插入迭代器赋值时,一个与赋值号右侧值相等的元素被添加到容器中。

函数back_inserter定义在头文件iterator中,它接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器,当我们通过此迭代器赋值时,赋值运算符会调用push_back将一个具有给定值的元素添加到容器中:

vector<int> vec;
back_insert_iterator<vector<int>> it = back_inserter(vec);
*it = 42;    // 将42放到vec的尾部
vector<int> vec;
fill_n(back_inserter(vec), 10, 0);    // 正确

拷贝算法(copy)接受三个迭代器,前两个表示输入范围,第三个表示目的序列的起始位置。此算法将输入范围内的元素拷贝到目的序列中,目的序列至少要包含与输入范围一样多的元素:

int a1[] = {0,1,2,3,4,5,6,7,8,9};
int a2[sizeof(a1) / sizeof(*a1)];    // a2大小与a1相同,sizeof的返回结果是常量表达式
auto ret = copy(begin(a1), end(a1), a2);    // 把a1内容拷贝给a2,返回值为a2的尾后元素的指针

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

relpace(ilst.begin(), ilst.end(), 0, 42);    // 将序列中所有0改为42

replace用于string时:

	string s = "abababa";

	replace(s.begin(), s.end(), 'b', "pp");    // 错误,第四个参数只能为一个char

如希望保留原序列不变,可调用replace_copy,此算法第三个位置接受的参数为第三个迭代器:

replace_copy(ilst_cbegin(), ilst_cend(), back_inserter(ivec), 0, 42);    // ilst未改变,ivec包含替换后的ilst副本,其中所有0改为42
	vector<int> vec;
	vec.reserve(10);
	fill_n(vec.begin(), 10, 0);    // 错误
	cout << vec.size() << endl;    // 还是0

算法sort会重排容器中元素的顺序,它是利用元素的<运算符实现的。

为消除容器中重复的单词,可先排序vector,使得重复的单词都相邻出现,再使用函数unique,使得所有相邻的重复项消除只剩一个,unique返回值为最后一个不重复的值的后一个位置:

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并不真正删除重复元素,只是将不重复元素调整到序列开始部分,不重复元素区域后的元素仍存在。

很多算法允许我们提供自己定义的操作来代替默认运算符,如sort默认使用元素类型的<运算符,但我们希望的排序顺序与<所定义的顺序不同,或序列中元素的类型没有定义<操作,这两种情况下,需要重载sort的默认行为。

我们可以使用接受第三个参数的重载版的sort,第三个参数是一个谓词,谓词是一个可调用表达式,返回结果是一个能用作条件的值。标准库算法使用的谓词分两类,一是一元谓词,即接受单一参数;二是二元谓词,它有两个参数。接受谓词参数的算法对输入序列中的元素调用谓词,因此,元素类型必须能转化为谓词的参数类型。

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

sort(words.begin(), words.end(), isShorter);    // 由短到长排序words,即谓词为true的在前面(原版本时使用<,也是<返回true时在前面)

将words按长度重排的同时,还希望具有相同长度的元素按字典序排列,可以使用stable_sort函数,它可以在排序后维持那些值相等的元素的顺序(即稳定排序):

elimDups(words);    // 删除重复元素并将元素按字典序排列
stable_sort(words.begin(), words.end(), isShorter);    // 按长度排序,但同一长度的元素的先后位置不变,即同一长度还是按原顺序(字典顺序)排

partition算法接受一个谓词,将容器内容划分,谓词为true的值会排在容器前半部分,返回值为指向最后一个使谓词为true的元素之后的位置。

bool isShorterThan5(const string& s) {
    return s.size() < 5;
}

void FindLongerThan5(vector<string>& words) {
    auto it = partition(words.begin(), words.end(), isShorterThan5);
    for (; it < words.end(); ++it) {
        cout << *it << endl;
    }
}

根据算法接受一元谓词还是二元谓词,我们传递给算法的谓词必须严格接受一个或两个参数,但有时我们希望传更多的参数,如上例中,将5硬编码到划分序列的谓词中,如果5能由我们输入就更有实际价值。

我们可以向一个算法传递任何类别的可调用对象,对于一个对象或一个表达式,如果可以对其使用调用运算符(),则它称为可调用的。即,如果e是一个可调用表达式,我么可以编写代码e(args),args是一个以逗号分隔的一个或多个参数的列表。

目前我们了解的可调用对象只有函数和函数指针。

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

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

其中,capture list是一个lambda所在函数中定义的局部变量列表(通常为空),lambda必须使用尾置返回。

我们可以忽略参数列表和返回类型,但必须永远包含捕获列表和函数体:

auto f = [] { return 42; };    // 忽略参数列表和括号,等价于指定一个空参数列表;忽略了返回类型,由return语句推断出为int

上例中,我们定义了一个可调用对象f,它不接受参数,返回42。

调用lambda表达式:

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

当lambda表达式的函数体包含着return语句之外的语句且没有指定返回类型时,返回void。(但我测试时并没有返回void,返回类型是return的内容的类型,应该是编译器优化或新版本特性)

lambda不能有默认实参,一个lambda调用的实参数永远与形参数相同。

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

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

// 当stable_sort需要比较两个元素时,它就会调用给定的这个lambda表达式
stable_sort(words.begin(), words.end(), [](const string &a, const string &b) { return a.size() < b.size(); } 

函数不能定义在函数内。函数外只能定义全局变量或者对象 ,而不能执行语句及调用函数。

我们可以使用标准库find_if算法来查找第一个具有特定大小的元素,它接受一对迭代器作为输入范围,第三个参数是一个谓词,find_if算法对输入序列中的每个元素调用给定的谓词,它返回第一个使谓词返回非0(即true)的元素的迭代器,如果不存在这样的元素,返回尾后迭代器。

打印一个string序列中长度大于等于sz的所有串:

void biggies(vector<string> &words, vector<string>::size_type sz) {
    elimDups(words);    // 将words按字典排序并删除重复单词
    
    stable_sort(words.begin(), words.end(), isShorter);    // 按长度排序,长度相同的维持字典序(稳定排序)
    
    auto wc = find_if(words.begin(), words.end(), [sz](const string &s){ return a.size() >= sz; });    // lambda捕获了sz,wc为第一个长度大于等于sz的元素的迭代器
    
    auto count = words.end() - wc;    // 计算满足长度大于等于sz的字符串个数
    cout << count << " " << make_plural(count, "word", "s") << " of length " << sz << " or longer" << endl;    // make_plural为自定义函数,当count>1时,返回将s加到word后的串,当count=1时,返回word

    for_each(wc, words.end(), [](const string &s){ cout << s << " "; });    // 此泛型算法对于迭代器范围内的每个元素执行第三个参数
    cout << endl;
}

上例中find_if只能接受一元谓词,因此,如果没有lambda表达式捕获的sz,只能将长度硬编码到函数中。for_each的lambda谓词使用了cout,是因为lambda可以直接使用当前函数内的static变量和当前函数之外的名字。

只有当lambda在其捕获列表中捕获一个它所在函数中的局部变量,才能在函数体中使用该变量。

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;

void func(vector<string>& svec, vector<string>::size_type sz) {
    auto it = partition(svec.begin(), svec.end(), [sz](const string& s) { return s.size() < sz; });
    for_each(it, svec.end(), [](const string& s) { cout << s << " "; });    // 打印长度大于等于sz的元素
    cout << endl;
}

int main() {
    vector<string> svec = { "aaa", "bbbb", "c", "dd" };
    func(svec, 2);
}

stable_partition与partition作用类似,但它是稳定的分区函数。

当定义一个lambda时,编译器生成一个与lambda对应的新的(未命名的)类类型。可以理解为当我们向函数传递lambda时,我们定义了一个新的类类型和这个新类类型的一个对象,函数中的参数(如find中第三个参数位置定义的lambda表达式)就可以代表这个新类类型的一个对象,类似地,使用auto定义被lambda初始化的变量时,我们定义了一个从lambda生成的新类类型的对象。

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

类似于参数传递,变量的捕获方式也分为值或引用:
在这里插入图片描述
采用值捕获的前提是变量可以拷贝,被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝:

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

引用捕获:

size_t v1 = 42;
auto f = [&v1] { return v1; };
v1 = 0;
auto j = f();    // j为0,f保存的是v1的引用

引用捕获必须保证被引用的对象在lambda执行时是存在的,如,lambda捕获一个局部变量,而出了该局部变量的作用域时,它就不复存在了。由于函数可以返回一个可调用对象,如函数返回一个lambda,则lambda不能包含引用捕获,因为函数不能返回一个局部变量的引用或指针。

当要捕获的对象不能拷贝时,只能使用引用捕获。

建议避免捕获指针或引用,它们绑定的对象可能会被销毁。

隐式捕获:

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

上例中的sz是隐式捕获的,捕获方式为值捕获。

混用隐式和显式捕获方式时,捕获列表中第一个元素必须是一个&或=,此符号指定了默认捕获方式为引用捕获或值捕获,并且显式捕获的变量必须使用与隐式捕获不同的方式,如隐式捕获是引用方式,则显式捕获的变量必须采用值方式,因此不能在其名字前面使用&:

// os为隐式引用捕获,c为显式值捕获,c前面不能加&
for_each(words.begin(), words.end(), [&, c] (const string &s) { os << s << c; };
// os为显式引用捕获,c为隐式值捕获,os前必须加&
for_each(words.begin(), words.end(), [=, &os] (const string &s) { os << s << c; };

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

size_t v1 = 42;
auto f = [v1] () mutable -> size_t { return ++v1; };    // 如没有mutable关键字,++v1会编译报错,不能更改
v1 = 0;
auto j = f();    // j为43

而对于引用捕获的变量是否可以修改依赖于此引用指向的是一个const类型还是一个非const类型:

size_t v1 = 42;
auto f = [&v1] () { return ++v1; };  
v1 = 0;
auto j = f();    // j为1

默认情况下,如果一个lambda体包含return之外的任何语句,则编译器假定此lambda返回void。被推断返回void的lambda不能返回值。但以下是我测试时情况:

    size_t v1 = 42;
	auto f = [&v1]() {     // 没有指定返回类型
		int i = 8;    // 虽然有return语句之外的语句
		return ++v1; 
	};

	cout << f() << endl;    // 输出43,还是可以有返回值

使用transform算法和一个lambda来将一个序列中每个负数替换为其绝对值:

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

函数transform接受三个迭代器和一个可调用对象,前两个迭代器表示输入序列,第三个迭代器表示目的位置。算法对输入序列中每个元素调用可调用对象,并将结果写入目的位置。上例中的lambda体是单一的return语句,返回一个表达式结果,我们无需指定返回类型,因为可以根据条件运算符的类型推断出来,但如果将程序改为:

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

上例会产生编译错误,编译器推断这个版本的lambda返回类型为void,但它返回了一个int值。(但我测试时,能通过编译并完成功能,应该是编译器优化或新版本特性)因此给上例的lambda定义返回类型,lambda的返回类型必须为尾置返回类型:

transform(vi.begin(), vi.end(), vi.begin(), [](int i) -> int { if (i < 0) return -i; else return i; });

count_if函数接受一对迭代器,表示一个输入范围,还接受一个谓词,会对输入范围中的每个元素执行。count_if返回一个计数值,表示谓词多少次为true,使用它统计序列中单词长度超过6的个数:

    vector<string> svec = { "bad", "good", "computer", "keyboard", "screen" };
    auto i = count_if(svec.begin(), svec.end(), [](const string& s) { if (s.size() > 6) return true; else return false; });    // 我的编译器自动推断的i的类型为ptrdiff_t,还是加上返回类型比较好
    cout << i << endl;    // 输出2

lambda捕获局部int变量,并递减变量值,直到为0,一旦变量变为0,再调用lambda应该返回true表示捕获的变量是0:

    int i = 5;
    auto f = [&i]() -> bool { if (i > 0) { --i; cout << i << endl; return false; } else return true; };
    while (!f());

如果是那种只在一两个地方使用的简单操作,lambda表达式是最有用的,如果需要在很多地方使用相同操作,最好用函数。

如果lambda表达式的捕获列表为空,通常可以用函数代替。但是对于捕获局部变量的lambda,用函数来替换它就不容易了,如之前在find_if中调用lambda比较一个string的长度和一个给定大小,我们可以很容易地编写一个接受两个参数的函数:

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

但不能用这个函数作为find_if的参数,因为find_if接受一个一元谓词。

我们可以解决这个问题,通过使用一个新的名为bind的标准库函数,这是C++11新标准,它定义在头文件functional中,可以将bind函数看做一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来适配原对象的参数列表:

auto newCallable = bind(callable, arg_list);

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

arg_list中的参数可能包含形如_n的名字,其中n是一个整数,这些参数是占位符,表示newCallable的参数,它们占据了传递给newCallable的参数的位置,即_1表示newCallable的第一个参数,以此类推。

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

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

string s = "hello";
bool b1 = check6(s);    // 调用check_size(s, 6)

用在find_if中:

auto wc = find_if(words.begin(), words.end(), bind(check_size, _1, sz));

名字_n都定义在一个名为placeholders的命名空间中,这个命名空间本身定义在std命名空间,为使用这些名字,两个命名空间都要写上,上例中使用bind函数前已经使用了:

using std::placeholders::_1;

对每个占位符名字,我们都必须提供一个单独的using声明,很烦人,可以这样:

using namespace std::placeholders;

说明希望来自std::placeholders中的名字在我们程序中都可以直接使用。与bind一样,placeholders命名空间也定义在functional头文件中。

auto g = bind(f, a, b, _2, c, _1);
g(X,Y);    //调用f(a, b, Y, c, X)

上例的一个实用例子:

sort(words.begin(), words.end(), isShorter);    // 从短到长排序
sort(words.begin(), words.end(), bind(isShorter, _2, _1));    // 从长到短排序

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

for_each(words.begin(), words.end(), bind(print, os, _1, ' ');    // 错误,原因在于bind拷贝其参数,但os(ostream类型对象)不能拷贝

为了解决以上问题,即直接传递给bind一个对象,而不是拷贝它,我们必须使用ref函数:

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

函数ref返回一个对象,这个对象包含给定的引用,且这个对象是可以拷贝的。标准库中还有一个cref函数,生成一个保存const引用的类。与bind一样,ref和cref也定义在头文件functional中。

在C++旧版本中,标准库定义了两个分别名为bind1st和bind2nd的函数,类似bind,它们作用相似,但只能分别绑定指定变量到第一个参数和第二个参数,局限性太大,在新标准中已被弃用,即新版本不再支持此特性。新的C++程序中应该使用bind。

使用函数代替lambda,统计出长度小于等于6的单词数量:

#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

bool IsShorterThan6(const string& s) {
    return s.size() < 6;
}

int main() {
    vector<string> svec = { "aaa", "bbbbbbb", "c", "dddddddd", "ee" };
    vector<string>::iterator it = partition(svec.begin(), svec.end(), IsShorterThan6);
    int cnt = it - svec.begin();
    cout << cnt << endl;
}

给定一个string,在一个int类型的vector中查找第一个大小大于string长度的值:

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <functional>
using namespace std;
using namespace std::placeholders;

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

int main() {
    auto f = bind(check_size, "ddddd", _1);
    vector<int> ivec = { 0,1,2,3,4,5,69,7 };
    vector<int>::iterator pos = find_if(ivec.begin(), ivec.end(), f);
    if (pos != ivec.end()) {
        cout << *pos << endl;
    }
}

除了为每个容器定义的迭代器之外,标准库在头文件iterator中还定义了额外几种迭代器:
1.插入迭代器:绑定到一个容器上,可用来向容器插入元素。
2.流迭代器:绑定到输入或输出流上,可用来遍历相关联的IO流。
3.反向迭代器:向普通迭代器相反的方向移动。除了forward_list外,所有标准库容器都有反向迭代器。
4.移动迭代器:这些迭代器移动元素,而不是拷贝它们。

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

插入迭代器支持的操作:
在这里插入图片描述
插入器三种类型:
1.back_inserter:创建一个使用push_back的迭代器。
2.front_inserter:创建一个使用push_front的迭代器。
3.inserter:创建一个使用insert的迭代器,此函数接受第二个参数,这个参数必须是指向给定容器的迭代器,元素将被插入到给定迭代器所表示的元素之前。

只有容器支持push_front的情况下,我们才能使用front_inserter,back_inserter类似。

对于使用insert的插入器:

*it = val;
// 相当于以下代码
it = c.insert(it, val);    // it指向新加入的元素
++it;    // 递增it使它指向原来的元素

而当我们使用front_inserter时:

list<int> lst = { 1, 2, 3, 4 };
list<int> lst2, lst3;
copy(lst.cbegin(), lst.cend(), front_inserter(lst2));    // lst2内容为4321
copy(lst.cbegin(), lst.cend(), inserter(lst3, lst3.begin()));    // lst3内容为1234

标准库还定义了unique_copy,它接受第三个迭代器,表示拷贝不重复元素的目标位置,使用unique_copy将vector中不重复的元素拷贝到一个初始值为空的list中:

#include <iostream>
#include <vector>
#include <iterator>
#include <list>
#include <algorithm>
using namespace std;

int main() {
    vector<int> ivec = { 0,1,2,3,4,5,3,2,1 };
    sort(ivec.begin(), ivec.end());
    list<int> lst;
    unique_copy(ivec.begin(), ivec.end(), back_inserter(lst));

    for (int i : lst) {
        cout << i << endl;
    }
}

虽然iostream不是容器类型,但标准库定义了可以用于这些IO类型对象的迭代器。istream_iterator读取输入流,ostream_iterator向一个输出流写数据,这些迭代器将它们对应的流当作一个特定类型的元素序列来处理。通过使用流迭代器,我们可以使用泛型算法从流对象读取、写入数据。

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尾后迭代器
vector<int> ivec;
while (in_iter != eof) {    // 当输入的值不是int时,只有此非法输入之前的值会保存在ivec中
    ivec.push_back(*in_iter++);
}

for (int i : ivec) {
    cout << i << endl;
}

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

以上程序可简写为以下形式:

istream_iterator<int> in_iter(cin), eof;
vector<int> ivec(in_iter, eof);

使用算法操作流迭代器:

istream_iterator<int> in(cin), eof;
cout << accumulate(in, eof, 0) << endl;

上例将输入中的数值求和。

istream_iterator允许使用懒惰求值,即,我们将一个istream_iterator绑定到一个流时,标准库并不保证迭代器立即从流读取数据,标准库保证的是我们第一次解引用迭代器之前,从流中读取数据的操作已经完成。也就是允许用到的时候再读取。对一般程序来说,影响不大,但对于创建了一个istream_iterator但并没有使用就销毁了和两个不同对象同步读取同一个流时,何时读就很重要了。

我们可以对任何具有输出运算符<<的类型定义ostream_iterator。创建ostream_iterator时,我们可以提供可选的第二参数,它是一个字符串,在输出每个元素后都会打印此字符串,此字符串必须是一个C风格字符串。必须将ostream_iterator绑定到一个指定的流,不允许空的或表示尾后位置的ostream_iterator。

ostream_iterator操作:
在这里插入图片描述
使用ostream_iterator输出值的序列:

vector<int> ivec = {0,1,2,3,4,5};
ostream_iterator<int> out_iter(cout, " ");
for (auto e : ivec) {
    *out_iter++ = e;    // 赋值语句实际上将元素写到cout
}
cout << endl;

以上循环可以简写为:

for (auto e : ivec) {
    out_iter = e;    // *和++对ostream_iterator对象不做任何事,但不推荐这么写
}

可以使用copy来打印ivec中的元素:

vector<int> ivec = {0,1,2,3,4,5};
ostream_iterator<int> out_iter(cout, " ");
copy(ivec.begin(), ivec.end(), out_iter);

我们可以使用流迭代器处理类类型,我们可以为任何定义了>>(<<)操作符的类型创建i(o)stream_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++;    // 将第一笔交易记录存在sum中
while (item_iter != eof) {
    if (item_iter->isbn() == sum.isbn()) {
        sum += *item_iter++;
    } else {
        out_iter = sum;    // 输出当前sum值和一个换行符
        sum = *item_iter++;
    }
}
out_iter = sum;    // 记得输出最后一组记录的和

输入流迭代器的streamItreator->member()(*streamIterator).member()都不会使迭代器前移:

    string s1, s2;
    istream_iterator<string> in(cin);
    ostream_iterator<string> out(cout);

    in->c_str();
    (*in).c_str();

    s1 = *in;
    cout << s1 << endl;

运行它:
在这里插入图片描述

使用流迭代器读取一个文本文件,存入一个vector<string>中:

#include <iostream>
#include <string>
#include <vector>
#include <iterator>
#include <fstream>
using namespace std;

int main() {
    ifstream ifs(文件路径);
    istream_iterator<string> in_it(ifs), eof;
    vector<string> svec;
    while (in_it != eof) {
        svec.push_back(*in_it++);    // 此处的++无作用,仅看上去好理解
    }

    for (string s : svec) {
        cout << s << endl;
    }
}

使用流迭代器、sort和copy从标准输入读取一个整数序列,将其排序,并将结果写到标准输出:

#include <iostream>
#include <vector>
#include <iterator>
#include <algorithm>
using namespace std;

int main() {
    istream_iterator<int> inIt(cin), eof;
    vector<int> ivec;
    while (inIt != eof) {
        ivec.push_back(*inIt++);
    }
    sort(ivec.begin(), ivec.end());

    ostream_iterator<int> outIt(cout, " ");
    copy(ivec.begin(), ivec.end(), outIt);
}

修改上面的程序,使其输出的结果不重复,只需要将copy函数改为unique_copy函数即可。

反向迭代器是从容器尾向容器首反向移动的迭代器,对于反向迭代器,递增和递减操作的含义会颠倒过来,即++it会移动到前一个元素。

除了forward_list之外,其他容器都支持反向迭代器,我们可以通过rbegin、rend、crbegin、crend成员函数来获得反向迭代器,这些成员返回指向容器尾元素和首元素之前一个位置的迭代器。与如同迭代器一样,反向迭代器也有const和非const版本。

从尾到首打印容器:

vector<int> ivec = { 0, 1, 2, 3 };
for (vector<int>::const_reverse_iterator rb = ivec.crbegin(); rb != ivec.crend(); ++rb) {
    cout << *rb << endl;
}

用于sort:

sort(ivec.begin(), ivec.end());    // 从小到大排序
sort(ivec.rbegin(), ivec.rend());    // 从大到小排序

除了forward_list之外(它不支持递减迭代器),标准容器上的其他迭代器既支持递增迭代器,也支持递减迭代器。流迭代器不支持递减操作。因此不能从一个forward_list或一个流迭代器创建反向迭代器。

假定有一个名为line的string,保存着一个逗号分隔的单词列表,我们希望打印line中的第一个单词:

    string line = "aaa,bbb,ccc,ddd";
    string::const_iterator comma = find(line.cbegin(), line.cend(), ',');    // comma的类型必须为string::const_iterator,因为输入的迭代器是const的
    cout << string(line.cbegin(), comma) << endl;    // 此处的line.cbegin也必须是const的,因为comma是const的

如希望打印最后一个单词:

    string line = "aaa,bbb,ccc,abc";
    string::const_reverse_iterator rcomma = find(line.crbegin(), line.crend(), ',');    // comma的类型必须为string::const_iterator,因为输入的迭代器是const的
    cout << string(line.crbegin(), rcomma) << endl;    // 错误,会打印cba
    cout << string(rcomma.base(), line.cend()) << endl;    // 正确

以上代码调用反向迭代器的base成员得到一个正向迭代器:
在这里插入图片描述
我们看到图中的rcmma和rcmma.base()指向不同元素,这保证了元素范围无论是正向还是反向处理都是相同的。

任何算法的基本特性都是它要求其迭代器提供哪些操作,某些算法,如find,只要求通过迭代器访问元素、递增元素以及比较两个迭代器是否相等这些能力,而其他一些算法,如sort,还要求读、写和随机访问元素的能力,算法所要求的迭代器操作可以分为5个迭代器类别,每个算法都会对它的每个迭代器参数指明须提供哪类迭代器:
在这里插入图片描述
还有一种算法分类方式是按照是否读、写或是重排序列中的元素来分类。

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

迭代器是按它们所提供的操作来分类的,这些分类形成了一种层次(上图中从上到下层级依次增加,输入和输出迭代器层级相同),一个高层别的迭代器支持低层类别迭代器的所有操作。

C++标准指明了泛型和数值算法的每个迭代器参数的最小类别,如find算法在一个序列上进行一遍扫描,对元素进行只读操作,因此至少需要输入迭代器。向算法传递一个能力更差的的迭代器会产生错误,但很多编译器不会给出任何警告。

输入迭代器:可以读取序列中的元素,它必须支持:
1.用于比较两个迭代器是否相等的运算符(==、!=)。
2.用于推进迭代器的前置和后置递增运算符。
3.用于读取元素的解引用*;解引用只会出现在赋值运算符右侧。
4.->。

输入迭代器只能用于顺序访问。对于输入迭代器,*it++保证是有效的,但递增它可能导致所有其他指向流的迭代器失效,其结果就是递增后保存下来的其他输入迭代器的值可能失效,结果就是只能单遍扫描算法。算法find、accumulate要求输入迭代器,而istream_iterator是一种输入迭代器。

输出迭代器:只写而不读元素。它必须支持:
1.用于推进迭代器的前置和后置递增运算。
2.解引用,且只出现在运算符的左侧(向一个解引用的输出迭代器赋值,就是将值写入它所指的元素)。

我们只能向一个输出迭代器赋值一次,用于单遍扫描。用作算法结果的目的位置的迭代器通常都是输出迭代器,如copy函数的第三个参数。ostream_iterator是输出迭代器。

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

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

随机访问迭代器:提供在常量时间内访问序列中任意元素的能力。此迭代器支持双向迭代器的所有功能,还支持:
1.用于比较两个迭代器相对位置的关系运算符<、<=、>、>=。
2.迭代器和一个整数的加减运算+、-、+=、-=,计算结果是迭代器在序列中前进或后退给定整数个元素后的位置。
3.用于两个迭代器上的减法运算符,得到两个迭代器的距离。
4.下标运算符it[n],与*(it + n)等价。

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

list的迭代器是双向迭代器。

copy函数的第三个参数要求输出迭代器。unique函数需要前向迭代器。

算法还有一组参数规范,多数算法具有如下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参数表示可以写入的目的位置的迭代器,算法假定要写入的元素数量是安全的,即此处的空间足够容纳所有输入。

如果dest是一个直接指向容器的迭代器,那么算法将输出数据写到容器中已存在的元素内,更常见的情况是dest被绑定到一个插入迭代器或一个ostream_iterator上,这样不管写入多少元素都是安全的。

接受第二个输入序列的算法用beg2或beg2和end2表示第二个输入范围。如果算法接受beg2和end2,这两个迭代器表示第二个范围;如果算法接受beg2(不接受end2)作为第二个输入范围的首元素,则这些算法假定从beg2开始的范围与beg和end所表示的范围至少一样大。

一些算法使用重载形式传递一个谓词,接受谓词参数来代替<或==运算符的算法和不接受谓词的版本的算法通常是一对重载函数。

_if版本的算法:第三个参数接受一个值的算法通常有一个不同名版本,该版本接受一个谓词代替元素值,接受谓词参数的算法都有附加的_if后缀:

find(beg, end, val);    // 查找val第一个出现的位置
find_if(beg, end, pred);    // 查找第一个令谓词pred为真的元素

区分拷贝元素的版本和不拷贝的版本:默认情况下,重排元素的算法将重排后的元素写回输入序列中,这些算法还提供另一个版本,将元素写到一个指定位置,写到指定位置的函数名为写回版本的函数名后面加一个_copy:

reverse(beg, end);
reverse_copy(beg, end, dest);

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

remove_if(v1.begin(), v1.end(), [](int i) { return i & 1; });    // 将奇数删除,但不是真正删除,只是移动到容器后部
remove_copy_if(v1.begin(), v1.end(), back_inserter(v2), [](int i) { return i & 1; });    //将偶数存入v2中,v1不变

根据算法和参数名猜执行什么操作:

replace(beg, end, old_val, new_val);    // 将输入范围内所有old_val改为new_val
replace_if(beg, end, pred, new_val);    // 将输入范围内所有使pred为真的值替换为new_val
replace_copy(beg, end, dest, old_val, new_val);    // 将输入范围内所有old_val改为new_val,但不写回原序列,而是写到dest处
replace_copy_if(beg, end, dest, pred, new_val);    // 将输入范围内所有使pred为真的值替换为new_val,但不写回原序列,而是写到dest处

链表类型list和forward_list定义了几个成员函数形式的算法,如下表:
在这里插入图片描述
在这里插入图片描述
特别是,list和forward_list定义了独有的sort、merge、remove、reverse和unique。通用版本的sort要求随机访问迭代器,因此不能用于list和forward_list,因为这两个类型分别提供双向迭代器和前向迭代器。链表类型定义的成员函数算法的通用版本有些也可以用于链表,但代价太高,这些算法需要交换输入序列中的元素,但对链表来说,可以通过改变元素间的链接而不是真的交换它们的值来快速交换元素。

建议对于list和forward_list容器,应优先使用成员函数版本的算法而非通用算法。

链表还定义了splice算法:
在这里插入图片描述
以上的操作中,list的splice函数的lst2参数必须是list类型,forward_list的splice函数的lst2参数必须是forward_list类型。

    forward_list<int> iflst = { 0,1,2,3,4 };
    forward_list<int> iflst2 = { 0,1,2,3,4 };
    iflst.splice_after(iflst.before_begin(), iflst2, iflst2.before_begin());
    for (int i : iflst) {
        cout << i << endl;    // 输出001234,即若使用forward_list版本的splice时,把第三个迭代器参数之后的元素放到第一个参数之后的位置
    }

list的迭代器不能支持加上一个数字的操作,只能递增,它不是随机访问迭代器,而是双向迭代器。

    list<int> ilst = { 0,1,2,3,4 };
    ilst.splice(ilst.begin(), ilst, ++ilst.begin(), ilst.end());
    for (int i : ilst) {
        cout << i << endl;    // 输出12340
    }

上例中我们可以发现splice这个版本的机制,将迭代器范围内第一个元素放到p迭代器指向的元素之前,然后再将迭代器指向之前p指向的元素,之后删除迭代器范围内第一个元素,接着处理第二个元素。

链表的特有版本和通用版本不完全相同,其区别在于,链表版本会改变底层容器,如remove的链表版本会删除指定元素,unique的链表版本会删除第二个和后续的重复元素:

    vector<int> ivec = { 0,1,2,3,4,4,5,6,7 };
    cout << ivec.size() << endl;    // 输出9
    remove(ivec.begin(), ivec.end(), 4);
    cout << ivec.size() << endl;    // 输出9
    for (int i : ivec) {
        cout << i << endl;    // 会发现输出0123567后面还有两个数字,只是移动了位置,而链表版本的会直接删除指定的元素
    }

merge和splice的链表版本会销毁其参数,但通用版本的merge将合并的序列写到一个给定的目的迭代器,两个序列是不变的。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C++ Primer 学习笔记 第十章 泛型算法 的相关文章

  • element table 中sortable排序

    在项目中做表格的排序 我使用的是自定义的排序 也就是通过后台接口进行的排序 写个文章记录一下element的三种排序方式 在列中设置sortable属性即可实现以该列为基准的排序 接受一个Boolean 默认为false 可以通过 Tabl
  • API接口整理收集常用Get-Post请求供测试使用

    百度 api集市免费接口 IP地址查询 http apistore baidu com apiworks servicedetail 114 html 频道新闻API 易源 http apistore baidu com apiworks
  • 【XML】学习笔记第四章-schema

    Schema 概述 作用 与DTD相比Schema的优势 基础命名空间 模式 引用方法 通过xsi noNamespaceSchemaLocation引入 通过xsi shemaLocation引入 Schema的语法结构 定义元素的主要语
  • shell脚本3个整数从小到大排序

    read p 请输入一个整数 num1 read p 请输入一个整数 num2 read p 请输入一个整数 num3 不管谁大谁小 最后打印 echo n u m 1 num1 num1 num2
  • Goby自定义漏洞之EXP

    前言 自定义漏洞配合EXP 提高漏洞的利用速度 简直是爽的飞起 自从HVV的时候Goby发布HVV专版 羡慕死了 就是太菜没傍上红方大佬的腿 虽然最终用上了HVV专版 但是一些只有你自己知道的漏洞 或者比较偏门的漏洞 就需要咱们自己来编写P
  • react小练习-antd Form表单和Table表格的交互-Table动态添加数据

    文章目录 功能实现效果 代码背景 代码思路 代码实现 功能实现效果 点击添加按钮 输入表单内容 表单提交后数据动态添加到Table表格里 如图 初始数据 是在dva的mock文件模拟的死数据 点击添加 输入数据 表单提交后 数据添加到表格里
  • Android签名概述

    一 Android签名概述 我们已经知道的是 Android对每一个Apk文件都会进行签名 在Apk文件安装时 系统会对其签名信息进行比对 判断程序的完整性 从而决定该Apk文件是否可以安装 在一定程度上达到安全的目的 给定一个Apk文件
  • JAVA实现蛇形矩阵算法

    JAVA实现蛇形矩阵算法 蛇形矩阵是一种特殊的二维矩阵 其元素按照一定的规律排列成蛇形 这种算法可以通过JAVA编程语言来实现 本文将介绍如何使用JAVA编写代码来生成蛇形矩阵 并给出相应的源代码 蛇形矩阵的生成规则如下 第一行从左到右依次
  • 多租户系统设计

    多租户系统设计 SaaS 的系统分级 SaaS 系统架构成熟度模型的 5 个级别 从 混乱 到 乌托邦 第 0 级 混乱 每次新增一个客户 都会新增软件的一个实例 第 1 级 受控的混乱 所有客户都运行在软件的同一个版本上 而且任何的定制化
  • 当在浏览器中输入一个域名后,会发生什么

    今天 去哪儿网Geely老师分享了他在校招时的一个面试题 当输www qunar com时发生了什么 真不是做广告哈 因为学的并没有很扎实 不能更深更广的分析这个问题 遂 百度一下 发现这篇文章并转载保存下来 同时也分享一下 希望大家可以更
  • vue-cli3.0打包时如何忽略某个第三方依赖包

    前几天接到了用vue重构某个业务模块的需求 常规开发不做讨论 但是在打包时需要排除业务中使用的某个第三方依赖包 代码如下 main js import Vue from vue import Cesium from cesium Cesiu
  • 解决Win7启动时出现“windows未能启动。原因可能是最近更改了硬件或软件”的问题

    搜索资料的时候发现个小问题 问题描述 在给ThinkPad T490做win7系统时 出现了 windows未能启动 原因可能是最近更改了硬件或软件 的问题 一直启动不来 在系统盘微PE中甚至还会出现 0x490 找不到元素 的问题 如何来

随机推荐

  • cuda安装笔记

    更新2023 3 4 我笔记本用vs编译后老是提示no kernel image is available for execution on the device 一直以为是驱动版本太高了或者cudatoolkit版本太高了 后来才知道是版
  • 【数据挖掘】知识点总结

    一 绪论 什么是数据挖掘 就是通过算法从大量的数据中搜索隐藏在其中的信息 数据挖掘的基本任务 聚类分析 异常检测 关联分析和预测建模 高维性和维灾难 随着维度的增加计算复杂度也随之增加 二 数据 不同的属性类型 标称 例如 邮政编码 定性数
  • 大数据应用——Linux常用的命令

    帮助命令 1 基本语法 help 命令 功能描述 获得shell内置命令的帮助信息 2 案例实操 1 查看cd命令的帮助信息 root hadoop01 help cd 常用快捷键 常用快捷键 功能 ctrl c 停止进程 ctrl l 清
  • Python(十五)读取Excel

    今天来实战演示如何封装读取Excel方法 第一步 准备一个Excel文件 sheet页命名为 login 学习技术交流群 704807680 第二步 编写代码找到Excel文件所在的文件夹路径 path os path dirname os
  • 机器学习算法——混淆矩阵(Confusion Matrix)之鸢尾花实例

    一 理论 什么是混淆矩阵 其实就是把所有类别的预测结果与真实结果按类别放置到了同一个表里 在这个表里我们可以清楚地看到每个类别正确识别的数量和错误识别的数量 混淆矩阵在什么情况下最好呢 答案是类别不平衡时 混淆矩阵是除了ROC曲线和AUC之
  • chatGPT高考作文

    百花齐放 春满人间 吹灭别人的灯 并不会让自己更加光明 阻挡别人的路 也不会让自己行得更远 这是一句古老而智慧的话语 告诉我们要尊重和包容他人 要与人和睦相处 要共同进步 同样的道理 也适用于文化的交流和发展 文化是一个民族的灵魂 是一个国
  • 华为OD机试真题-不爱施肥的小布

    题目描述 某农场主管理了一大片果园 fields i 表示不同果林的面积 单位 m 2 现在要为所有的果林施肥且必须在n天之内完成 否则影响收成 小布是果林的工作人员 他每次选择一片果林进行施肥 且一片果林施肥完后当天不再进行施肥作业 假设
  • C++复合类型

    1 数组 数组声明应该指出元素类型 数组名和元素个数 数组的初始化需要遵循以下规则 a 只有定义数组时才能使用初始化 此后就不能使用了 也不能将一个数组赋给另一个数组 int narray1 4 1 2 3 4 int narray2 4
  • 操作系统 实验二 银行家算法

    题目描述 已知进程 P0 P1 P2 P3 P4 有三类系统资源A B C的数量分别为10 5 7 在T0时刻的资源分配情况如下图所示 1 若进程P1请求资源 发出请求向量Request1 1 0 2 编写程序用银行家算法判断系统能否将资源
  • 【javascript-基础小练习】角度转弧度,已知角度90,转成弧度?

    角转弧度的公式为 jiao 180 pai hu 假设pai的值为 3 14
  • 数据库的事务及变量声明方法

    事务 简单来说事务就是为了保持数据一致性的一种手段 在事务中的sql语句作为一个整体一起向系统提交 要么都执行 要么都不执行 可以回滚到原来的状态 事务的语法 开始事务 begin tran 或者transaction 提交事务 commi
  • Spring为什么不推荐你使用@Autowired ?

    Spring为什么不推荐你使用 Autowired 我们总能发现当使用IDEA写代码的时候 Autowired注解会报黄 我们把鼠标悬停在上面 可以看到这个如下图所示的警告信息 当我们按住alt 回车键 idea就会帮我们修改成这样 我向来
  • 微服务架构

    3 注册中心与服务发现 前言 1 服务发现基础知识 1 1 注册中心与服务发现的联系 1 2 使用 DNS 与负载均衡器发现服务的弊端 1 3 云中的服务发现应该具备的特点 1 4 服务发现架构 1 5 服务治理的概念 1 6 服务注册的概
  • Linux服务器(centos7)中Word转换PDF,文档出现中文乱码或方格【亲测可用,已解决】

    提示 在centos服务器使用aspose word转换word文件为pdf的时候只有中文乱码或则方格 但是在win服务器上使用可以正常转换 本次文章主要解决字体缺失问题 目录 前言 一 在linux服务器上生成的pdf都是这种格式的 二
  • 信号和槽函数的扩展

    信号和槽函数的扩展 一个信号连接多个槽函数 一个槽函数连接多个信号 信号连接信号 一个信号可以连接多个槽函数 发送一个信号有多个处理动作 需要写多个connect 连接 槽函数的执行顺序和信号的发射顺序相同 QT5中 信号的接收者可以是一个
  • C++primer总结

    目录 第一章 数据处理 2 整形 2 第二章 复合类型 3 数组 3 字符串 4 枚举 5 指针 5 第三章 函数的使用 6 内联函数 6 默认参数 6 函数的重载 7 函数的模板 7 第四章 内存模型和名称空间 7 文件的单独存放 7 变
  • Transaction rolled back because it has been marked as rollback-only

    http hsyd iteye com blog 586772 错误信息 Transaction rolled back because it has been marked as rollback only 原因 事务提交多次 检查代码
  • 小程序无法获取头像和昵称(已解决)

    从基础库 2 21 2 开始支持 当小程序需要让用户完善个人资料时 可以通过微信提供的头像昵称填写能力快速完善 根据相关法律法规 为确保信息安全 由用户上传的图片 昵称等信息微信侧将进行安全检测 组件从基础库2 24 4版本起 已接入内容安
  • Python+Selenium+phantomjs实现网页模拟登录和截图

    Python Selenium phantomjs实现网页模拟登录和截图 本文全部操作均在windows环境下 安装 Python Python是一种跨平台的计算机程序设计语言 它可以运行在Windows Mac和各种Linux Unix系
  • C++ Primer 学习笔记 第十章 泛型算法

    标准库容器很小 并未给每个容器添加大量功能 而是提供了一组算法 这些算法大多数都独立于任何特定的容器 这些算法是通用的 或者说是泛型的 generic 可用于不同类型容器和元素 大多数泛型算法定义在头文件algorithm中 头文件numb