第十章 泛型算法
初识泛型算法
只读算法
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
的函数体可以使用 sz
。lambda
不捕获 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
中得捕获列表为空,但其函数体重还是使用了两个名字: s
和 count
,前者使它自己得参数。
捕获列表为空,是因为我们只对 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
的函数类似,被推断返回 void
的 lambda
不能返回值。
使用便准库 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_if
的 lambda
使用捕获列表来保存 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
表示生成的可调用对象中参数的位置: _1
为 newCallable
的第一个参数,以此类推。
绑定 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
,可以将原来基于 lambda
的 find_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
表示。这个新的可调用对象将它自己的参数作为第三个和第五个参数传递给 f
。f
的第一个、第二个和第四个参数分别被绑定到给定的值 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
类似,有时对有些绑定的参数我们希望以引用的方式传递,或是要绑定参数的类型无法拷贝。
例如,为了替换一个引用方式捕获 ostream
的 lambda
:
// 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
一样,函数 ref
和 cref
也定义在头文件 functional
中。
再探迭代器
插入迭代器
back_inserter
创建一个使用push_back
的迭代器front_inserter
创建一个使用push_front
的迭代器inserter
创建一个使用insert
的迭代器。此函数接收第二个参数,这个参数必须是一个指向给定容器的迭代器。元素将被插入到给定迭代器所表示的元素之前。
要注意容器是否支持
push_front
或push_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_iterator
的 base
成员函数完成转换,此成员函数会返回其对应的普通迭代器
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; }); // 和上面是等价的
特定容器的算法
list
和 forward_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 的所有元素移动到 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 不能指向给定范围中元素 |
链表特有的操作会改变容器