《C++11Primer》阅读随记 -- 十、泛型算法

第十章 泛型算法

初识泛型算法

只读算法

cbegin()cend () 是C++11新增的,它们返回一个 const 的迭代器,不能用于修改元素。

比如:find()、count()、accumulate

// 对 vec 中元素求和,和的初值为 0
int sum = accumulate(vec.cbegin(), vec.cend(), 0);

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

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

此调用将 v 中的每个元素都联结到了一个 string 上,该 string 初始时为空串。

注意:这里我们显示地创建了一个 string。而将空串当作一个字符串字面值传递给第三个参数是不可以地,会导致一个编译错误

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

操作两个序列的算法

// roster2 中的元素数目应该至少与 roster1 一样多
equal(roster1.cbegin(), roster1.cend(), roster2.cbegin());

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

写容器元素的算法

fill(vec.begin(), vec.end(), 0);	// 将每个元素重置为 0
// 将容器的一个子序列设置为 10
fill(vec.begin(), vec.begin() + vec.size() / 2, 10);

算法不检查写操作

一些算法接受一个迭代器来指出一个单独的目的位置。这些算法将新值赋予一个序列中的元素,该序列从目的位置迭代器指向的元素开始。例如,函数 fill_n 接受一个单迭代器、一个计数值和一个值。它将给定值赋予迭代器指向的元素开始的指定个元素。我们可以用 fill_n 将一个新值赋予 vector 中的元素

vector<int> vec;// 空 vector
// 使用 vec,赋予它不同值
fill_n(vec.begin(), vec.size(), 0);	// 将所有元素重置为 0

// 修改 vec 中不存在的元素是错误的,未定义的
fill_n(vec.begin(), 10, 0);
back_inserter

插入迭代器( insert iterator )。使用 back_inserter 函数操作,该函数定义在头文件 iterator 中。

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

vector<int> vec;// 空向量
auto it = back_inserter(vec);	// 通过它赋值会将元素添加到 vec 中
*it = 42;	// vec 现在有一个元素,值为 42 (赋值)

通过 back_inserter 来创建一个插入迭代器,可用来向 vec 添加元素

vector<int> vec;	// 空向量
// 正确:back_inserter 创建一个插入迭代器,可用来向 vec 添加元素
fill_n(back_inserter(vec), 10, 0);	// 添加 10 个元素到 vec
拷贝算法

拷贝算法是另一个向目的位置迭代器指向的输出序列中的元素写入数据的算法。此算法接收三个迭代器,前两个表示一个输入范围,第三个表示目的序列的起始位置。此算法将输入范围中的元素拷贝到目的序列中。

int a1[] = {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

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

replace 算法读入一个序列,并将其中所有等于给定值的元素都改为另一个值。此算法接受 4 个参数:前两个是迭代器,表示输入序列,后两个一个是要搜索的值,另一个是心智。

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

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

// 使用 back_inserter 按需要增长目标序列
replace_copy(ilist.cbegin(), ilist.cend(), back_inserter(ivec), 0, 42);

重排容器元素的算法

消除重复单词

void elimDups(vector<string>& words){
	// 按字典序排序 words,以便查找重复单词
	sort(words.begin(), words.end());
	// unique 重排输入范围,使得每个单词只出现一次
	// 排列在范围的前部,返回指向不重复区域之后一个位置的迭代器
	auto end_unique = unique(words.begin(), words.end());
	// 使用向量操作 erase 删除重复单词
	words.erase(end_unique, words.end());
}

在这里插入图片描述

定制操作(仿函数,谓词)

向算法传递参数

作为一个例子,假定希望在调用 elimDups 后打印 vector 的内容。此外害假定希望单词按其长度排序,大小相同的再按字典序排列。为了按长度重排 vector,我们将使用 sort 的第二个版本,它接受第三个参数,此参数是一个 谓词

谓词

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

// 比较函数,用来按长度排序单词
bool isShorter(const string& s1, const string& s2){
	return s1.size() < s2.size();
}

// 按长度由短知场排序 words
sort(words.begin(), words.end(), isShorter);

排序算法

可以使更稳定的排序算法 stable_sort

elimDups(words);	// 将 words 按字典序重拍,并像处重复单词
// 按长度重新排序,长度相同的单词维持字典序
stable_sort(words.begin(), words.end(), isShorter);
for(const auto& s : words)	// 无须拷贝字符串
	cout << s << " ";	// 打印每个元素
cout << endl;

lambda 表达式

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

一个例子:在一个容器或者数组中,求大于等于一个给定长度的单词由多少。并修改输出,使程序只打印大于等于给定长度的单词。
框架如下:

void biggies(vector<string>& words, vector<string::size_type sz>){
	elimDups(words);	// 将 words 按字典序排序,删除重复单词
	// 按长度排序,长度相同的单词维持字典序
	stable_sort(words.begin(), words.end(), isShorter);
	// 获取第一个迭代器,指向第一个满足 size() >= sz 的元素
	// 计算满足 size >= sz 的元素的数目
	// 打印长度大于等于给定值的单词,每个单词后面接一个空格
}

新问题是在 vector 中虚招第一个大于等于给定长度的元素。一旦找到这个而元素,根据其位置,就可以计算出有多少元素的长度大于等于给定值

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

编写一个函数,令其接受一个 string 和一个长度,并返回一个 bool 值表示该 string 的长度是否大于给定长度很容易。但是 find_if 接受一元谓词 – 我们传递给 find_if 的任何函数都必须严格接受一个参数。没有任何办法能传递给他第二个参数来表示长度。为了解决此问题,需要介绍另外一些语言特性

在这里插入图片描述

介绍 lambda

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

到目前所学,我们使用过的仅有两种可调用对象是函数和函数指针。还有其他两种可调用对象:重载了函数调用运算符的类,以及 lambda表达式

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

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

其中,capture list( 捕获列表 ) 是一个 lambda 所在函数中定义的局部变量的列表( 通常为空 ); return type、parameter list 和 function body 与任何普通函数一样,分别表示返回类型、参数列表和函数体。但是,与普通函数不同,lambda 必须使用尾置返回来指定返回类型

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

auto f = []{return 42;}; // 注意函数体后也有分号 ;

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

lambda 的调用方式与普通函数的调用方式相同。

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

lambda 中忽略括号和参数列表等价于指定一个空参数列表。在此例中,当调用 f 时,参数列表时空的。如果忽略返回类型,lambda 根据函数体中的代码推断出返回类型。如果函数体只是一个 return 语句,则返回类型从返回的表达式的类型推断而来。否则,返回类型为 void

向 lambda 传递参数

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

编写一个与 isShorter 函数体完成相同功能的 lambda:

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

空捕获列表表明此 lambda 不使用它所在函数中的任何局部变量。lambda 的参数与 isShorter 的参数类似。lambda 函数体也与其类似。

可以使用此 lambda 来调用 stable_sort:

// 按长度排序,长度相同的单词维持字典序
stable_sort(words.begin(), words.end(),
			[](const string& a, const string& b){ return a.size() < b.size() });

stable_sort 需要比较两个元素时,它会调用给定的这个 lambda 表达式

使用捕获列表

如此,编写一个可以传递给 find_if 的可调用表达式。我们希望这个表达式能将输入序列中每个 string 的长度与 biggies 函数中的 sz 参数的值进行比较

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

在本例中,lambda 捕获 sz,并只有单一的 string 参数。其函数体会将 string 的大小与捕获的 sz 值进行比较:

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

lambda 以一对 [] 开始,我们可以在其中提供一个以逗号分隔的名字列表,这些名字都是它所在函数中定义的

由于此 lambda 捕获 sz,因此 lambda 的函数体可以使用 szlambda 不捕获 words, 因此不能访问此变量。如果我们给 lambda 提供一个空捕获列表,则代码会编译错误

// 错误: sz 未捕获
[](const string& a)
	{ return a.size() >= sz; };

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

调用 find_if

调用此 lambda ,我们就可以查找第一个长度大于等于 sz 的元素

// 获取一个迭代器,指向第一个满足 size() >= sz 的元素
auto wc = find_if(words.begin(), words.end(),
				[sz](const string& a)
					{ return a.size() >= sz }; );

我们可以调用 find_if 返回的迭代器来计算从它开始到 words 的末尾一共有多少个元素

// 计算满足 size >= sz 的元素的数目
auto count = words.end() - wc;
cout << count << " " << make_plural(count, "word", "s") << " of length "
	 << " of length " << sz << " or longer" << endl;

for_each

问题的最后一部分使打印 words 中长度大于等于 sz 的元素。可以使用 for_each 算法。此算法接受一个可调用对象,并对输入序列中每个元素调用此对象

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

lambda 中得捕获列表为空,但其函数体重还是使用了两个名字: scount ,前者使它自己得参数。

捕获列表为空,是因为我们只对 lambda 所在函数中定义( 非 static )变量使用捕获列表。一个 lambda 可以直接使用定义在当前函数之外得名字。在本例中,cout 不是定义在 biggies 中得局部名字,而是定义在头文件 iostream 中。因此,只要 biggies 出现的作用域包含了头文件 iostream, 我们的 lambda 就可以使用 cout

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

完整的 biggies

void biggies(vector<string>& words,
			 vector<string>::size_type sz)
{
	elimDups(words);	// 将 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, "word", "s") << " of length "
	 << " of length " << sz << " or longer" << endl;
	// 打印长度大于等于给定值的单词,每个单词后面借一个空格
	for_each(wc, words.end(), [](const string& s){ cout << s << " ";});
	cout << endl;
}
string make_plural(size_t ctr, const string& word
							   const string& ending)
{
	return (ctr > 1) ? word = ending : word;
}

lambda捕获和返回

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

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

值捕获

类似参数传递,比那辆的捕获方式也可以是值或引用。lambda 捕获列表如下

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

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

void fcn1(){
	size_t v1 = 42;	// 局部变量
	auto f = [v1]{return v1;};
	v1 = 0;
	auto j = f();	// j为42; f 保存了我们创建它时 v1 的拷贝
}

在这里插入图片描述
由于被捕获变量的值在 lambda 创建时拷贝,因此随后对其修改不会影响到 lambda 内对应的值

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

引用捕获有时时必要的。例如,我们可能希望 biggies 函数接收一个 ostream 的引用,用来输出数据,并接收一个字符作为分隔符:

void biggies(vector<string>& words,
			 vector<string>::size_type sz,
			 ostream& os = cout, char c = ' ')
{
	// 与之前例子一样的重排 words 的代码
	...
	// 打印 count 的语句改为打印到 os
	for_each(words.begin(), words.end*(,
			[&os, c](const string& s){ os << s << c; });
}

我们也可以从一个函数返回 lambda。函数可以直接返回一个可调用对象,或者返回一个类对象,该类包含有可调用对象的数据成员。如果函数返回一个 lambda,则与函数不能返回一个局部变量的引用类似,此 lambda 也不能包含引用捕获。

隐式捕获

除了显示列出我们希望使用的来自所在函数的变量之外,还可以让编译器根据 lambda 体中的代码来推断我们要使用哪些变量。为了只是编译器推断捕获列表,应在捕获列表中写一个 &=& 告诉编译器采用捕获引用方式,= 反之。

// sz 为隐式捕获,值捕获的方式
wc = find_if(words.begin(), words.end(),
			[=](const string& a)
				{ 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 类型

void fcn4(){
	size_t v1 = 42;
	// v1 是一个非 const 变量的引用
	// 可以通过 f2 中的引用来改变它
	auto f2 = [&v1]{ return ++v1; };
	v1 = 0;
	auto j = f();	// j 为 1
}

指定 lambda 返回类型

默认情况下,如果一个 lambda 体包含 return 之外的任何语句,则编译器假定此 lambda 返回 void 。与其他返回 void 的函数类似,被推断返回 voidlambda 不能返回值。

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

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

函数 transform 接收三个迭代器和一个可调用对象。前两个迭代器表示输入序列,第三个迭代器表示目的位置。算法对数据序列中每个元素调用可调用对象,并将结果写道目的位置。如本例所示,目的位置迭代器与表示输入序列开始位置的迭代器可以是相同的。当输入迭代器和目的迭代器相同时,transform 将输入序列中每个元素替换为可调用对象操作该元素得到的结果。

在本例中,我们传递给 transform 一个 lambda ,它返回其参数的绝对值。lambda 体是单一的 return 语句,返回一个条件表达式的结果。我们无须指定返回类型,因为可以根绝条件运算符的类型推断出来

但是,如果将程序改写为看似等价的 if 语句,就会产生编译错误:

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

编译器推断这个版本的 lambda 返回类型为 void,但它返回了一个 int

当我们需要为一个 lambda 定义返回类型时,必须使用尾置返回类型:

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

在此例中,传递给 transform 的第四个参数是一个 lambda ,它的捕获列表是空的,接收单一 int 参数,返回一个 int 值。它的函数体是一个返回其参数的绝对值的 if 语句。

参数绑定

对于捕获局部变量的 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 的可调用对象必须接收单一参数。biggies 传递给 find_iflambda 使用捕获列表来保存 sz。为了用 check_size 来代替此 lambda,必须解决如何向 check_size 传递一个参数的问题

标准库 bind 函数

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 表示生成的可调用对象中参数的位置: _1newCallable 的第一个参数,以此类推。

绑定 check_size 的 sz 参数

// check6 是一个可调用对象,接收一个 string 类型的参数
// 并用此 string 和值 6 来调用 check_size
auto check6 = bind(check_size, _1, 6);

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

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

使用 bind ,可以将原来基于 lambdafind_if 调用替换为如下使用 check_size 的版本

auto wc = find_if(words.begin(), words.end(),
				  	bind(check_size, _1, sz));
使用 placeholders 名字

名字 _n 都定义在一个名为 placeholders 的命名空间中,而这个命名空间本身定义在 std 命名空间中。为了使用这些名字,两个命名空间都要写上。对 bind 的调用代码假定之前已经恰当地使用了 using 声明。例如,_1 对应地 using 声明为:

using std::placeholders::_1;

对每个占位符名字,都必须提供一个单独的 using 声明。容易出错,可以使用另外一种不同形式的 using 语句,而不是分别声明:

using namespace namespace_name;

例如:

using namespace std::placeholders;

在这里插入图片描述

bind 的参数

如前,我们可以用 bind 修正参数的只。更一般的,可以用 bind 绑定给定可调用对象中的参数或重新安排其顺序。例如,假定 f 是一个可调用对象,它有 5 个参数,则下面对 bind 的调用:

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

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

传递给 g 的参数按位置绑定到占位符。即,第一个参数绑定到 _1,以此类推。实际上,这个 bind 调用会将

g(_1, _2)

映射为

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 类似,有时对有些绑定的参数我们希望以引用的方式传递,或是要绑定参数的类型无法拷贝。
例如,为了替换一个引用方式捕获 ostreamlambda

// 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){
	return os << s << c;
}

但是,不能直接用 bind 来代替对 os 的捕获,原因在于 bind 拷贝其参数,而不能拷贝一个 ostream。如果我们希望传递给 bind 一个对象而又不拷贝它,就必须使用标准库 ref 函数

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

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

再探迭代器

插入迭代器

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

要注意容器是否支持 push_frontpush_back

当调用 inserter(c, iter) 时,我们得到一个迭代器,接下来使用它时,会将元素插入到 iter 原来所指向的元素之前的位置。即如果 it 是由 inserter 生成的迭代器,则下面的赋值语句

it = inserter(c, iter);
*it = val;

在这里插入图片描述

等价于

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

在这里插入图片描述

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

front_inserter 生成的迭代器的行为与 inserter 生成的迭代器完全不一样。

list<int> lst = {1,2,3,4};
list<int> lst2, lst3;
// 拷贝完成之后,lst2 包含 4 3 2 1
copy(lst.cbegin(), lst.cend(), front.inserter(lst2));

// 拷贝完成之后,lst3 包含 1 2 3 4
copy(lst.cbegin(), lst.cend(), inserter(lst3, lst3.begin()));

iostream 迭代器

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" 读取字符串

从标准输入读取数据,存入一个 vector

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(int), eof;
vector<int> vec(in_iter, eof);

使用算法操作流迭代器

istream_iterator<int> in(cin), eof;
cout << accumulate(in, eof, 0) << endl;
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

使用流迭代器处理类类型

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){
	if( item_iter->isbn() == sum.isbn() )
		sum += *item_iter++;
	else{
		out_iter = sum;	// 输出 sum 当前值
		sum = *item_iter++;	// 读取吓一跳记录
	}
}
out_iter = sum;	// 打印最后一组记录的和

反向迭代器

// 在一个逗号分隔的列表中查找最后一个元素
auto rcomma = find(line.crbegin(), line.crend(), ',');

当我们试图打印找到的单词时,会错误输出

// 错误:将逆序输出单词的字符
cout << string(line.crbegin(), rcomma) << endl;
// FIRST, MIDDLE, LAST
// 将输出 TSAL

解决办法,通过调用 reverse_iteratorbase 成员函数完成转换,此成员函数会返回其对应的普通迭代器

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

在这里插入图片描述

算法命名规范

_if 版本的算法

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

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

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

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

remove_copy_if(v1.begin(), v1.end(), inserter(v2, v2.begin()),
        [](int i) { return i % 2; }); // 和上面是等价的

特定容器的算法

listforward_list 定义了独有的 sort、merge、remove、reverse 和 unique

// 这些操作都返回 void
lst.merge(lst2)			// 将来自 lst2 的元素合并入 lst。lst 和 lst2 都必须是有序的
lst.merge(lst2, comp)	// 元素将从 lst2 中删除。在合并之后,lst2 变为空。第一个版本
						// 使用 < 运算符; 第二个版本使用给定的比较操作

lst.remove(val)			// 调用 eerase 删除掉与给定值相等或令一元谓词为真的每个元素
lst.remove_if(pred)

lst.reverse()			// 反转 lst 中元素的顺序
lst.sort()				// 使用 < 或给定的比较操作排序元素
lst.sort(comp)

lst.unique()			// 使用 erase 删除同一个值得连续拷贝。第一个版本使用 ==; 
lst.unique(pred)		// 第二个版本使用给定得二元谓词

splice 成员

链表类型还定义了 splice 算法。

lst.splice(args)flst.splice_after(args)作用
(p, lst2)p 是一个指向 lst 中元素的迭代器,或一个指向 flst 首前位置得迭代器。函数将 lst2 的所有元素移动到 lstp 之前的位置或是 flstp 之后的位置。将元素从 lst2 中删除。lst2 的类型必须与 lstflst 相同,且不能是同一个链表
(p, lst2, p2)p2 是一个指向 lst2 中位置的有效的迭代器。将 p2 指向的元素移动到 lst 中,或将 p2 之后的元素移动到 flst 中。lst2 可以是与 lstflst 相同的链表
(p, lst2, b, e)be 必须表示 lst2 中的合法范围。将给定范围中的元素从 lst2 移动到 lstflstlst2lstflst 可以是相同的链表,但 p 不能指向给定范围中元素

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Artintel

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值