1.概述
标准库容器定义的操作集合惊人的小。标准库并为给每个容器添加大量功能,而是提供了一组算法,这些算法中的大多数都独立于任何特定的容器。这些算法是通用的(或称泛型的):它们可用于不同类型的容器和不同类型的元素。
大多数算法都定义在头文件algorithm中,标准库还在头文件numeric中定义了一组数值泛型算法。
这些算法给容器提供了更多的操作。
一般情况下,这些算法并不直接操作容器,而是遍历由两个迭代器指定的一个元素范围来进行操作。
迭代器令算法不依赖于容器,但算法依赖于元素类型的操作。
例如:find用元素类型的==运算符完成每个元素与给定值的比较。其他算法可能要求元素类型支持<运算符。
不过,大多数算法提供了一种方法,允许我们使用自定义的操作来代替默认的运算符。
2.初识泛型算法
只读算法:
一些算法只会读取其输入范围内的元素,从不改变元素,例如:
find:查找符合的元素
count::查找符合的元素的数量
accumulate:对范围元素求和,第三个参数是和的初值,第三个参数的类型决定了使用哪个加法运算符和返回值类型。
int sum = accumulate(vec.begin(),vec.end(),0);
equal:确定两个序列是否保存相同的值
equal(vec.cbegin(),vec.cend(),vec2.cbegin());
equal基于一个重要的假设:第二个序列至少与第一个序列一样长。
*****那些只接受单一迭代器来表示第二个序列的算法,都假定第二个序列至少与第一个序列一样长*****
对于只读取而不改变元素的算法,通常最好使用cbegin()和cend()。
写容器元素的算法
一些算法将新值赋予序列中的元素,当使用这类算法时,必须确保序列原大小不小于我们要求算法写入的元素数目。
记住:算法不会执行容器操作,因此它们自身不可能改变容器大小。
fill(vec.begin(),vec.end(),0); //将每个元素重置为0
关键概念:迭代器参数
一些算法从两个序列中读取元素,构成这两个序列的元素可以来自于不同类型的容器。例如:
第一个序列可能保存于一个vector中,第二个序列可能保存于一个list、deque、内置数组或其他容器中,而且,两个序列转给你元素的类型
也不要求严格匹配。算法要求的只是能够比较两个序列中的元素。例如,对于equal算法,元素类型不要求相同,但是我们必须能使用==来比较
来自两个序列中的元素。
*****算法不检查写操作*****
函数fill_n假定写入指定个元素是安全的,即,如下形式的调用:
fill_n(dest,n,val);
fill_n假定dest指向一个元素,而从dest开始的序列至少包含n个元素。
在一个空容器上调用fill_n:
vector<int> vec; //空向量
fill_n(vec.begin(),10,0); //错误:vector中没有10个元素
*****介绍back_inserter*****
一种保证算法有足够元素空间来容纳输出数据的方法是使用插入迭代器。
插入迭代器是一种向容器中添加元素的迭代器。
back_inserter接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器。当我们通过此迭代器赋值时,赋值运算符会调用push_back
将一个具有给定值的元素添加到容器中:
vector<int> vec;
auto it = back_inserter(vec);
*it = 666; //现在vec中有一个值为666的元素
我们常常使用back_inserter来创建一个迭代器,作为算法的目的位置来使用。例如:
vector<int> vec;
fill_n(back_inserter(vec),10,0); //通过插入迭代器,添加十个元素到vec
在每步迭代中,fill_n向给定序列的一个元素赋值,由于我们传递的参数是back_inserter返回的迭代器,因此每次赋值都会在vec上调用
push_back,最终,这条fill_n调用语句向vec的末尾添加了10个元素,每个元素值都是0
*****拷贝算法*****
拷贝(copy)算法是另一个向目的位置迭代器指向的输出序列中的元素写入数据的算法。
int a1[] = {1,2,3};
int a2[sizeof(a1)/sizeof(*a1)];
auto ret = copy(begin(a1),end(a1),a2); ret指向拷贝到a2的尾元素之后的位置
replace(begin(a1), end(a1), 1, 66); 把a1中的1替换为66
replace_copy(begin(a1), end(a1), a2, 1, 666); 把a1拷贝到a2,把a2中的1替换为666
*****重排容器元素的算法*****
sort:利用元素类型的<运算符来实现排序的
unique:重排输入序列,将相邻的重复项“消除”,并返回一个指向不重复值范围末尾的迭代器。
例如:
std::vector<int> vec{ 2,3,1,2,6,3 };
std::sort(vec.begin(), vec.end()); //先排序:vec:1 2 2 3 3 6
auto end = std::unique(vec.begin(), vec.end()); //再去重:end指向vec[4]==3 vec:1 2 3 6 3 6
vec.erase(end, vec.end()); //最后删除多余的元素:vec:1 2 3 6
注:unique操作前vec==1 2 2 3 3 6
unique并不是删除,而是覆盖相邻重复元素,如上,用vec[3]==3覆盖vec[2]==2,vec[5]==6覆盖vec[3]==3
所以执行unique后,最后两个还是3和6,最后在删除后面多余的元素。
因为算法不能直接添加删除元素,所以最后调用容器的删除操作。
3.定制操作
很多算法都会比较输入序列中的元素,默认情况下,这类算法使用元素类型的<或==运算符完成比较。标准库还为这些算法定义了额外的版本,允许我们提供自己定义的操作来代替默认运算符。
例如:sort算法默认使用元素类型的<运算符,但可能我们希望的排序顺序与<所定义的顺序不同,或是我们的序列可能保存的是未定义<运算符的元素类型。在这两种情况下,都需要重载sort的默认行为。
向算法传递函数
谓词:是一个可调用的表达式,其返回结果是一个能用作条件的值。
一元谓词:只接受一个参数
二元谓词:有两个参数
//比较函数
bool isShorter(const string& s1,const string& s2)
{
return s1.size() < s2.size();
}
vector<string> vec = {"abc","hello","aoe","bbb"};
sort(vec.begin(),vec.end(),isShorter); //使得长度短的排在前面
stable_sort(vec.begin(),vec.end(),isShorter); //稳定排序,不会改变相同大小元素在相对位置
lambda表达式
有时我们希望进行的操作需要更多的参数,超出谓词参数的限制。
可调用对象:对于一个对象或一个表达式,如果可以对其使用调用运算符,则称它为可调用的。
四种可调用对象:
1.函数
2.函数指针
3.重载了函数调用运算符的类
4.lambda表达式
一个lambda表达式表示一个可调用的代码单元,可以将其理解为一个未命名的内联函数,与函数不同的是lambda可以定义在函数内部。
形式如下:
[capture list] (parameter list) -> return type { function body}
capture list(捕获列表):是一个lambda所在函数中定义的局部变量的列表(通常为空)
return type:返回类型,parameter list:参数列表,function body:函数体
***lambda必须使用尾置返回来指定返回类型***
可以忽略参数列表(没有参数)和返回类型(自动推断),但必须包含捕获列表和函数体:
auto f = []{return 666;};
和函数调用相同:f();
*****向lambda传递参数*****
lambda不能有默认实参。
编写与上面的isShorter函数相同功能的lambda:
[](const string& a,const string& b)
{ return a.size() < b.size(); }
sort(vec.begin(),vec.end(),[](const string& a,const string& b){ return a.size() < b.size(); });
*****使用捕获列表*****
bool Fun(const std::string& s)
{
return s.size() == 3;
}
void main()
{
std::vector<std::string> vec = { "hello" ,"bbb","aoe","abc" };
std::find_if(vec.begin(), vec.end(), Fun);
}
由于find_if第三个参数是一元谓词,所以不能把大小传进去,这时就可以用lambda。
void main()
{
int size = 3;
std::vector<std::string> vec = { "hello" ,"bbb","aoe","abc" };
lambda捕获size,所以函数体内可以使用size
find_if(vec.begin(), vec.end(), [size](const string& a){return a.size()==size};
lambda可以直接使用定义在当前函数之外的名字,下面的std::cout不是定义在main中的局部名字,只要包含了相应的头文件即可使用
std::for_each(vec.begin(), vec.end(), [] (const std::string& s) { std::cout << s << " "; });
}
***捕获列表只用于局部非static变量,lambda可以直接使用局部static变量和在它所在函数之外声明的名字。
*****lambda捕获和返回*****
当定义一个lambda时,编译器生成一个与lambda对应的新的(未命名的)类类型。
可以这样理解:当向一个函数传递一个lambda时,同时定义了一个新类型和该类型的一个对象:传递的参数就是此编译器生成的类类型的未命名
对象,类似的,当使用auto定义一个用lambda初始化的变量时,定义了一个从lambda生成的类型的对象。
默认情况下,从lambda生成的类都包含一个对应 该lambda所捕获的变量的数据成员。类似任何普通类的数据成员,lambda的数据成员也在lambda
对象创建时被初始化。
*****值捕获*****
类似参数传递,变量的捕获方式也可以是值或引用。采用值捕获的前提是变量可拷贝,被捕获的变量的值是在lambda创建时拷贝,不是调用时拷贝
void fun()
{
size_t v = 6;
auto f = [v]{return v;}; //将v拷贝到名为f的可调用对象
v = 0;
auto j = f(); //j==6,因为是值拷贝
}
*****引用捕获*****
void fun()
{
size_t v = 6;
auto f = [&v]{return v;}; //f包含v的引用
v = 0;
auto j = f(); //j==0,f保存v的引用,而非拷贝
}
采用引用捕获一个变量,要确保被引用的对象在lambda执行的时候是存在的。
有些类型不允许拷贝,只能采用引用,例如:ostream
可以从一个函数返回lambda,函数可以直接返回一个可调用对象,或者返回一个类对象,如果函数返回一个lambda,则与函数不能返回一个局部变
量的引用类似,此lambda也不能包含引用捕获。
***建议:尽量保持lambda的变量捕获简单化
*****隐式捕获*****
为了指示编译器推断捕获列表,应在捕获列表中写一个&或=:
&:告诉编译器采用捕获引用方式
=:表示采用值捕获方式
void main()
{
int size = 3;
std::vector<std::string> vec = { "hello" ,"bbb","aoe","abc" };
find_if(vec.begin(), vec.end(), [=](const string& a){return a.size()==size}); //隐式值捕获
}
也可以混合使用隐式捕获和显示捕获
可变lambda
void fun()
{
size_t v = 1;
auto f = [v] () mutable {return ++v;}; //f可以改变图所捕获的变量的值
v = 0;
auto j = f(); //j==2
}
void fun()
{
size_t v = 1;
auto f = [&v] {return ++v;}; //v是非const变量的引用,通过f中的引用可以修改它
v = 0;
auto j = f(); //j==1
}
指定lambda返回类型
不是单一的return语句,返回void,但实际上返回int,编译错误
transform(vec.begin(),vec.end(),vec.begin(), [](int i) {if(i<0) return -i;else return i});
有多个return语句时,必须指定返回类型
transform(vec.begin(),vec.end(),vec.begin(),
[](int i) -> int
{if(i<0) return -i;else return i});
参数绑定:
find_if接受一个一元谓词,因此传递给find_if的可调用对象必须接受单一参数.
bool check_size(const string& s,string::size_type sz)
{
return s.size()>=sz;
}
不能把上面的函数传递给find_if,因为它有两个参数
find_if(vec.begin(),vec.end(),check_size); //错误
*****标准库bind函数*****
可以使用一个新的名为bind的标准库函数解决向check_size传递一个长度参数的问题。
bind定义在functional头文件中,可以将bind函数看作一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来适应
源对象的参数列表。调用bind的一般形式:
auto newCallable = bind(callable,arg_list);
当我们调用newCallable时,newCallable会调用callable,并传递给它arg_list中的参数。
arg_list中的参数可能包含如_n的名字,其中n是一个整数,这些参数是“占位符”,表示newCallable的参数,它们占据了传递给newCallable的
参数位置,数值n表示生成的可调用对象中参数的位置:_1为newCallable的第一个参数,_2为第二个参数.....
如上的例子:使用bind生成一个调用check_size的对象:用一个定制作为其大小参数来调用check_size
auto check6 = bind(check_size,_1,6);
此bind调用只有一个占位符,表示check6只接受单一参数,占位符出现在arg_list的第一个位置,表示check6的此参数对应check_size的第一个
参数:
string s = "hello";
bool b1 = check6(s); //check6会调用check_size(s,6);
用bind重写之前的程序:
void main()
{
int size = 3;
vector<string> vec = { "hello" ,"bbb","aoe","abc" };
find_if(vec.begin(), vec.end(), bind(check_size,_1,size);
}
*****使用placeholders名字*****
名字_n都定义在一个名为placeholders的命名空间中,而这个命名空间本身的定义在std命名空间中,为了使用这些名字,两个命名空间都要写上。
placeholders定义在头文件functional中。
之前的程序都假设已经恰当的使用了using声明,_1对应的声明:using std::placeholders::_1;
每一个都这样声明很麻烦,使用另一种形式的using语句:
using namespace namespace_name;
using namespace std::placeholders; //placeholders定义的所有名字都可用
*****bind的参数*****
可以用bind绑定给定可调用对象中的参数或重新安排其顺序:
auto g = bind(f,a,b,_2,c,_1); //g是一个有两个参数的可调用对象
这个新的可调用对象将它自己的参数作为第三个和第五个参数传递给f。f的第一个、第二个和第四个参数分别被绑定到给定的值a、b、c上。
传递给g的参数按位置绑定到占位符。即第一个参数绑定到_1,第二个参数绑定到_2,因此,当我们调用g时,其第一个参数将被传递给f作为最后
一个参数,第二个参数将被传递给f作为第三个参数。
这个bind调用将 g(_1,_2) 映射到 f(a,b,_2,c,_1)
*****用bind重排参数顺序*****
可以用bind颠倒isShorter的含义:
bool isShorter(const string& s1,const string& s2)
{
return s1.size()<s2.size();
}
sort(vec.begin(),vec.end(),isShorter);
sort(vec.begin(),vec.end(),bind(isShorter,_2,_1); //排序和上面相反,颠倒了参数的顺序
*****绑定引用参数*****
默认情况下,bind的那些不是占位符的参数被拷贝到bind返回的可调用对象中。但与lambda类似,有时对有些绑定的参数我们希望以引用的方式
传递,或是要绑定参数的类型无法拷贝。例如:
为了替换一个引用方式捕获ostream的lambda:
//os是一个局部变量,引用一个输出流
//c是一个局部变量,类型为char
for_each(vec.begin(),vec.end(),[&os,c] (const string& s) {os<<s<<c;}
可以很容易地编写一个函数,完成相同的工作:
ostream& print(ostream& os,const string& s,char c)
{
return os<<s<<c;
}
但是不能直接用bind来代替对os的捕获:
for_each(vec.begin(),vec.end(),bind(print,os,_1,' ')); //错误:不能拷贝os
我们希望传递给bind一个对象而又不拷贝它,必须使用标准库ref函数:
for_each(vec.begin(),vec.end(),bind(print,ref(os),_1,' '));
函数ref返回一个对象,包含给定的引用,此对象是可以拷贝的。
标准库还有一个cref函数,生成一个保存const引用的类。与bind一样,ref和cref也定义在头文件functional中。
4.再探迭代器
除了为每个容器定义的迭代器之外,标准库在头文件iterator中还定义了额外的几种迭代器:
1.插入迭代器:被绑定到一个容器上,用来向容器插入元素。
2.流迭代器:被绑定到输入或输出流上,用来遍历所关联的IO流。
3.反向迭代器:这些迭代器向后而不是向前移动。除了forward_list之外的标准容器都有反向迭代器。
4.移动迭代器:不是拷贝其中的元素,而是移动它们。(13章讲)
插入迭代器
插入迭代器有三种类型,差异在于元素插入的位置:
1.back_inserter:创建一个使用push_back的迭代器
2.front_inserter:创建一个使用push_front的迭代器
3.inserter:创建一个使用insert的迭代器,此函数接受第二个参数,这个参数必须是指向给定容器的迭代器。元素将被插入到给定迭代器所
表示的元素之前
***只有在容器支持push_front的情况下,才可以使用front_inserter。类似的只有容器支持push_back的情况下,才能使用back_inserter
插入器的工作过程:
当调用inserter(c,iter)时,我们得到一个迭代器,接下来使用它时,会将元素插入到iter原来指向的元素之前的位置。如下:
*it = val; //it是由inserter生成的迭代器,赋值后,it还是指向原来的元素
其效果与下面代码一样:
it = c.insert(it,val); //it指向了新加入的元素
++it; //递增it,使它指向原来的元素
front_inserter生成的迭代器的行为与inserter生成的迭代器完全不一样。使用front_inserter时,元素总是插入到容器第一个元素之前。
front_inserter生成的迭代器会将插入的元素序列的顺序颠倒过来,inserter和back_inserter则不会。
iostream迭代器
虽然iostream类型不是容器,但标准库定义了可以用于这些IO类型对象的迭代器。
1.istream_iterator:读取输入流
2.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);
istream_iterator<int> eof;
while(in_iter != eof) //遇到文件尾(window:ctrl+z)或遇到IO错误,等于尾后迭代器
ver.push_back(*in_iter++);
重写上面的程序,体现了istream_iterator更有用的地方:
istream_iterator<int> in_iter(cin),eof;
vectro<int> vec(in_iter,eof); //从迭代器范围构造vec,直到遇到文件尾或不是int的数据为止
使用算法操作流迭代器:
例如:
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<int> out_iter(cout,' ');
for(auto e:vec)
*out_iter++ = e; //赋值语句实际上将元素写到cout
此程序将vec中的每个元素写到cout,每个元素后加一个空格。每次向out_iter赋值时,写操作就会被提交。
当我们向out_iter赋值时,可以忽略解引用和递增运算。即,循环可以重写成下面的样子:
for(auto e:vec)
out_iter = e;
运算符*和++实际上对ostream_iterator对象不做任何事情,因此忽略它们对我们的程序没有任何影响。
但还是推荐第一种形式,这种写法中,流迭代器的使用与其他迭代器的使用保持一致,如果要将此循环改为操作其他迭代器类型,修改起来非常
容易,对读者来说,此循环的行为也更为清晰。
可以通过copy来打印vec中的元素,比编写循环更简单:
copy(vec.begin(),vec.end(),out_iter);
使用流迭代器处理类类型
可以为任何定义了输入运算符(>>)的类型创建istream_iterator对象
可以为任何定义了输出运算符(<<)的类型创建ostream_iterator对象
举例:之前Sales_item既有输入运算符也有输出运算符,因此可以重写
istream_iteartor<Sales_item> item_iter(cin),eof;
ostream_iteartor<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++; //相同的isbn,累加,并读取下一条记录
else
{
out_iter = sum; //输出sum的当前值
sum = *item_iter++; //去读下一条记录
}
}
out_iter = sum; //打印最后一组sum和
反向迭代器
反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器。对于反向迭代器,递增操作的含义会颠倒过来:
递增一个反向迭代器会移动到前一个元素,递减一个迭代器会移动到一下元素。
sort(vec.begin(),vec.end()); //正常排序
sort(vec.rbegin(),vec.rend(); //逆序排序
反向迭代器和其他迭代器间的关系
string line = "abc,aoe";
假定有一个名为line的string,保存着一个逗号分隔的单词列表,我们希望打印line中的第一个单词,使用find可以很容易完成:
auto comma = find(line.cbegin(),line.cend(),',');
cout<<string(line.cbegin(),comma)<<endl;
如果没有逗号comma等于line.cend(),打印整个string
如果希望打印最后一个单词,可以改用反向迭代器:
auto rcomma = find(line.crbegin(),line.crend(),',');
如果有逗号,rcomma指向最后一个逗号,如果line中没有逗号,rcomma指向line.crend()
如果我们按下面打印:
cout<<string(line.crbegin(),rcomma); //输出:eoa,因为迭代器是反向迭代器,所以反向输出
如果希望按正常顺序打印从rcomma开始到line末尾间的字符,不能直接使用rcomma,意味它是一个反向迭代器,需要做的是,将rcomma转换会一个
普通迭代器,能在line中正向移动,通过reverse_iterator的base成员函数来完成转换,此函数会返回其对应的普通迭代器:
cout<<string(rcomma.base(),line.cend();
5.泛型算法结构
算法形参模式
算法命名规范
6.特定容器算法
与其他容器不同,链表类型list和forward_list定义了几个成员函数形式的算法,如上图,特别是,它们定义了独有的sort、merge、remove
和unique,通用版的sort要求随机访问迭代器,因此不能用于list和forward_list,因为这两个类型分别提供双向迭代器和前向迭代器。
链表类型定义的其他算法的通用版本可以用于链表,但代价太高,这些算法需要交换输入序列中的元素,因此,这些链表版本的算法的性能比
对应的通用版本好得多。
***对于list和forward_list应优先使用成员函数版本算法而不是通用算法。
splice成员:链表类型特有的