在学习lambda表达式之前,咱们先来盘点一下C++中的那些可调用对象。
C++中的可调用对象有哪些?如下所示:
为什么要有这么多种的可调用对象呢? 举个例子:可调用对象的发展史就好比手机的发展史;座机->按键手机->智能手机,他们都具有打电话的功能,为什么要不断地完善发展呢?说白了,就是为了方便,为了满足当今生活的需求。(博主我曾经向换回按键手机,发现根本做不到,现如今的手机和生活早已高度绑定)编程语言中特性的发展也是如此,在编程语言的不断使用和发展中,总会产生这样或那样的新需求,有了新需求,就要有新的解决措施,不然,就成历史遗留问题了。
变量指针指向一个变量,数组指针指向一个数组,那,函数指针就是指向一个函数的漏喽
声明函数指针的格式:返回类型 (*指针名称)(参数类型列表);
使用举例:使用函数指针调用add函数,完成两个整数的相加。
代码如下:
#include
using namespace std;
int add(int a, int b)
{
return a+b;
}
int main()
{
// 声明函数指针
int (*ptr)(int,int);
// 初始化函数指针
ptr = add;
// 通过函数指针调用add函数
int ret = ptr(1,2);
cout << ret;
return 0;
}
你可能会说,函数指针挺好用的呀,在上述例子中确实是这样,但是,如果函数稍微复杂一点,使用场景复杂一点,那就是另一个故事了~ 更何况,函数指针的声明较为复杂,不方便使用,容易写错。于是,在C++的STL库中添加了仿函数。
什么是仿函数呢?仿函数又称函数对象,就是可以像函数一样使用的对象。
如何做到的呢?在该类中重载了operator(),使得该类的对象可以像函数一样使用。
举个例子:我们有一个自定义类型Date类,使用仿函数的方式比较Date类对象是否相等;
代码如下:
#include
using namespace std;
struct Date
{
Date(int year,int month,int day)
:_year(year)
,_month(month)
,_day(day)
{}
int _year;
int _month;
int _day;
};
struct cmp
{
bool operator()(Date& d1, Date& d2)
{
if(d1._year == d2._year && d1._month == d2._month && d1._day == d2._day)
return true;
return false;
}
};
int main()
{
Date d1(2024,8,29);
Date d2(2020,10,30);
cmp c;
if(c(d1,d2))
cout << "相等";
else
cout << "不相等";
return 0;
}
仿函数更加符合面向对象的编程思想,使用上也比函数指针更简单,但还是存在不足之处。比如:我们想要比较一个自定义类型的数据,假设这个自定义类型的手机类型把,我们可以按照其价格比较,也可以按照手机的内存大小比较,也可以根据用户的评价比较……比较的需求会有很多,此时,我们应该如何写代码呢?
按照仿函数的使用方式来看,如果想要使用仿函数来进行比较,我们就需要定义多个类,在每个类中重载operator(),每个operator() 中按照不同的逻辑比较。(比较逻辑有很多,而一个类中只能重载一个 () ,所以需要多个类)
代码如下:
struct cmpWithPrice
{
bool operator()(Phone& p1,Phone& p2)
{
return p1.price > p2.price;
}
};
struct cmpWithComment
{
bool operator()(Phone& p1,Phone& p2)
{
return p1.Comment > p2.Comment;
}
};
// ...
使用仿函数的话,每当我们有新的比较需求,都需要实现一个类,成本太大。于是lambda表达式应运而生。
终于降到 lambda 表达式了~
格式:[捕捉列表] (参数) mutable -> 返回类型 { 函数体 }
(额,怎么感觉lambda表达式 “相貌丑陋”,好像挺难的?非也非也!lambda表达式并不难。)
lambda表达式使用示例代码:
int main()
{
vector v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,
3 }, { "菠萝", 1.5, 4 } };
sort(v.begin(), v.end(), [](const Goods& f1, const Goods& f2){
return f1._price < f2._price; });
sort(v.begin(), v.end(), [](const Goods& f1, const Goods& f2){
return f1._price > f2._price; });
sort(v.begin(), v.end(), [](const Goods& f1, const Goods& f2){
return f1._evaluate < f2._evaluate; });
sort(v.begin(), v.end(), [](const Goods& f1, const Goods& f2){
return f1._evaluate > f2._evaluate; });
}
可以看出lambda表达式能够代替函数对象作为参数使用。
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
lambda表达式到底是什么?从上面lambda表达式的使用示例代码可以看出,lambda表达式其实就是一个局部的匿名函数。说白了lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量。
示例代码:
int main()
{
auto f = []{cout << "hello world" << endl; };
f();
return 0;
}
函数对象和lambda表达式的使用对比
看一段代码:
struct add
{
public:
int operator()(int a, int b)
{
return a+b;
}
};
int main()
{
// 函数对象
add a1;
a1(1, 2);
// lambda
auto a2 = [=](int a, int b)->int{return a+b;}
a2(2, 2);
return 0;
};
上面这段代码,main函数中的汇编如下:
可以看到 仿函数对象 和 lambda表达式 都调用operator()了。嗯,等等,仿函数对象调用operator()我懂,但是lambda表达式怎么也会调用operator() 呢?它的operator() 是哪来的呢?我们没写,不代表编译器没写~
没错,实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义一个lambda表达式,编译器会自动生成一个类,并在类中重载operator()。
从 函数对象 和 lambda表达式 的使用来看,我不禁想起一句话 “那有什么岁月静好,只不过有人在负重前行”,lambda表达式使用上确实简单方便了不少,但是实际上,该创建类还得创建类,该重载还得重载,该做的工作一样没少,只不过原来应该由我们做的事情,编译器替我们做了。使用上越来越简单。
lambda表达式类型的探究
前面说过,仿函数的缺点是一个类只能进行一个比较逻辑的判断,当比较逻辑多了,就需要我们写多个类来区分不同的仿函数对象。那lambda表达式也是按照仿函数的方式处理的,它的类型是如何区分的呢?
编译器会采用算法生成一个uuid作为lambda表达式的类型,uuid是唯一的标识符,所以,即使是书写完全相同的两个lambda表达式,类型也是不同的。需要注意的是,在语法层,lambda表达式是没有类型的。