本文默认读者对泛型算法有一定的认识和基础 ,另外 , 本文不探究lambda算法和函数式编程的思想 , 只是对算法应用作简要介绍 。
首先我们知道 , 在algorithm头文件中包含了很多的算法 , 而且这些算法全都是泛型 , 只需要与容器的迭代器接口即可 。而且这些泛型算法还可以让程序员自己定义:
bool bigger(const string& s1 , const string& s2){ //默认已导入头文件
return s1.size() > s2.size(); //长的排在前面
}
int main(){
vector<string> vs = {"c++" , "vb" , "c" , "java"};
sort(vs.begin() , vs.end() , bigger); //使用函数bigger排序
for(size_t i = 0 ; i < vs.size() ; i++){
cout << vs[i] << " ";
}
cout << endl;
sort(vs.begin() , vs.end()); //使用默认顺序排序
for(size_t i = 0 ; i < vs.size() ; i++){
cout << vs[i] << " ";
}
}
输出结果为: java c++ vb c
c c++ java vb
像sort这种对内部进行排序的算法因为必须对内部两个元素进行比较 , 所以它接受的函数除了返回一个bool值之外 , 还必须接受两个容器内类型相同的参数 , 用于排序 。像这种需要接受两个参数的函数叫做二元谓词 , 而sort这个算法必须要接受一个二元谓词的函数参数 , 不然无法进行排序 。 下面我们再来看一个接受一元谓词(即该函数参数只接受一个类型相同的参数)的函数参数的算法:
bool middle(const string& s){
return s.size() > 3 && s.size() <10;
}
int main(){
vector<string> vs = {"c++" , "vb" , "c" , "java"};
auto loc = find_if(vs.begin() , vs.end() , middle);
cout << *loc << endl;
vs = {"It's just a test" , "bye bye"};
loc = find_if(vs.begin() , vs.end() , middle);
cout << *loc << endl;
}
输出:
java
bye bye
不知道有没人注意到上述程序的一个硬性问题 , 就是当我们想切换一个比较范围的时候 , 我们可能会想到把比较范围当成参数传到middle中 , 但那样做必然是失败的 , 因为前面说了 , middle必须是一个只有一个参数的一元谓词 , 否则无法传入find_if中 , 编译器在这里可是很严格的 , 为了避免这种硬编码(即把需要的比较量写成字面常量折叠到代码中而无法根据实际情况自由修改) , 就有了今天的lambda表达式 。lambda表达式的标准形式是 : [ 捕获列表 ] ( 参数列表 ) -> 返回类型 { 函数体 }
这种模式叫后置返回类型。但是一般情况下我们都会忽略返回类型或参数列表, 所以会写成下面形式 [ ] ( ) { }或 [ ] { } , 下面我们来看一个例子:
void find(vector<string>& vs , int min , int max){
auto ans = find_if(vs.begin() , vs.end() , [min , max] (const string& s1) {
return s1.size() > min && s1.size() < max;
});
cout << *ans << endl;
}
int main(){
vector<string> vs = {"c++" , "vb" , "c" , "java"};
find(vs , 3 , 10);
vs = {"It's just a test" , "bye bye"};
find(vs , 3 , 10);
}
输出与上个例子是完全一样的 , 不过我们现在就可以自由更改范围了。捕获列表负责把局部变量捕获到lambda表达式中 , 如果没有捕获 , lambda表达式将无法使用局部变量 。 而且lambda表达式只能是内联到代码中的函数 , 不能在外面定义一个lambda函数 ,即不能把一个函数定义为lambda表达式。
下面我们来看一些lambda表达式的特性:
void find(vector<string>& vs , int max){
static int min = 3;
// int min = 3;
auto ans = find_if(vs.begin() , vs.end() , [max] (const string& s1) {
return s1.size() > min && s1.size() < max;
});
cout << *ans << endl;
}
上面的例子想要说明的是lambda表达式在不捕获的情况下 , 无法使用局部变量 , 但对于static 修饰的变量是可以使用的 , 因为这样的变量在程序运行时都会存在 , 不需要捕获 。还有一个类似的例子:
void print(vector<string>& vs){
for_each(vs.begin() , vs.end() , [] (const string& s){
cout << s << endl;
});
}
for_each函数是对每一个元素都调用后面那个一元谓词函数 , 这里我们注意到 , 我们并没有捕获cout 和 endl 却能够使用他们 。 这是因为他们都是定义在iostream头文件中的 , 而我们一开始就导入了该头文件 , 并且使用了std命名空间 , 相当于它们在这里是有效的名字 , 所以不需要捕获 , 所以我们可以总结出局部静态变量和定义在文件外被包含的变量不需要捕获就能使用。
在lambda表达式中 , 捕获列表中的元素与参数列表的元素相识 , 也可以选择捕获值或者引用(指针) 。 现在我们先来看捕获值的情况:
void f1(){
int i = 88;
auto f = [i] {return i;};
cout << f() << endl;
i = 0;
cout << f() << endl;
auto g = [i] {return i;};
cout << g() << endl;
}
运行函数程序输出:
88
88
0
从输出来看 , lambda表达式在建立时捕获的值就把这个变量固定下来了(其实就是把它复制了), 在这之后这个值就和表达式中捕获的值没有关系了 。下面我们再来看一下对于一个在表达式中会变化的值:
void f1(){
int i = 88;
auto f = [i] () mutable {return ++i;};
cout << f() << endl;
i = 0;
cout << f() << endl;
auto g = [i] () mutable {return ++i;};
cout << g() << endl;
cout << i << endl;
}
输出结果:
89
90
1
0
从上述例子我们看出 , 当我们建立一个lambda表达式时 , 就自动在内部复制了一个捕获列表的值 , 这个值就是用捕获列表中的值进行赋值初始化的 , 但在这之后这两个值就没有任何关系了 , 另外 , 捕获值默认把该值设为const常量 , 所以我们需要把它设为mutable才能改变它 , 但使用了关键字后 , 就不能忽略参数列表 , 即使它为空 。接下来我们看一看捕获引用的情况:
void f2(){
int i = 88;
auto f = [&i] {return i;};
cout << f() << endl;
i = 0;
cout << f() << endl;
auto g = [&i] {return i;};
cout << g() << endl;
}
输出结果:
88
0
0
这次的情况就和捕获值的情况完全不同 , 因为这次在表达式中捕获的是i的引用 , 它是随时都与i值相等的 。 下面我们再看一个变化的引用值:
void f2(){
int i = 88;
auto f = [&i] {return ++i;};
cout << f() << endl;
i = 0;
cout << f() << endl;
auto g = [&i] {return ++i;};
cout << g() << endl;
cout << i << endl;
}
输出结果:
89
1
2
2
跟捕获值的情况不一样 , 捕获引用默认的是与引用本身的状态一样 , 如果该值是可变的 , 则捕获来的引用也是可变的 , 反之亦然 。 也能从最后一条看出引用的变化会引起值的变化。
接下来我们看看一看捕获指针的情况:
void f3(){
int i = 88;
int* p = &i;
auto f = [p] {return p;};
cout << *f() << endl;
p = 0;
cout << *f() << endl;
i = 0;
cout << *f() << endl;
cout << i << endl;
}
输出:
88
88
0
0
在表达式中我们返回了捕获的指针 , 所以返回值是一个int型指针 , 在这个程序中 , 我们在捕获了指针之后 , 改变了原始指针的值(变为空指针) , 而表达时仍然能正确输出结果 , 可见情况与调用函数时传递指针做参数是一样的 , 指针本身被复制了 , 所以外部指针与其无关 , 但因为地址是一样的 , 所以i 清零后表达式返回的指针指向的值也就是零。
当我们传递的是指针时 , 指针本身的特性与传值是相同的 , 若要让指针具有引用的特征 , 可以传递引用指针:
void f3(){
int i = 88 , j = 32;
int* p = &i;
auto f = [&p] {return p;};
cout << *f() << endl;
p = &j;
cout << *f() << endl;
j = 0;
cout << *f() << endl;
}
输出:
88
32
0
这样当我们改变指针所指向的对象时 , 表达式中的指针相应也会发生变化 。
其余的我们在下一篇文章中继续分析。