序言
在上一篇文章中,我们主要介绍了 C++11 中的新增的关键词,以及 范围for循环
这类语法糖的使用和背后的逻辑。在这篇文章中我们会继续介绍一个特别重要的新特性分别是 lambda表达式
。
1. lambda表达式
1.1 lambda的定义
C++11 中的 lambda表达式
是一种定义 匿名函数对象
的方式。它们可以捕获它们所在作用域中的变量,并且可以在需要函数对象的地方使用,如作为算法的参数。lambda表达式
自 C++11 标准引入后,大大简化了编码并增强了代码的灵活性和可读性。我们在这里提到 匿名函数对象
,说直白点就是该函数没有具体名字嘛,那是真的没有吗?请大家记住这个疑问。
1.2 lambda表达式的基本语法
lambda表达式
的基本语法如下:
[capture](parameters) mutable -> return_type { body }
- capture:捕获列表,指定
lambda表达式
体内部可以访问的外部变量。 - parameters:参数列表,与普通函数的参数列表类似,但
lambda表达式
也可以没有参数。 - mutable:可选的,表示代码可以修改以值捕获的外部变量。默认情况下,这些变量是只读的。
- return_type:返回类型,可选。编译器可以根据内容 自动推导返回类型。
- body:表达式的函数体,可以包含任意有效的C++语句。
我们简单的举一个示例:
void test_1() {
auto func1 = [](int A, int B) ->int { return A + B; };
cout << func1(1, 2) << endl;
}
我们返回值在没有什么特殊需求下完全是可以省略的,编译器会自动推导,所以我们还可以表示为:
void test_1() {
auto func1 = [](int A, int B) { return A + B; };
cout << func1(1, 2) << endl;
}
在这里我们的代码逻辑还是比较简单的,当我们的代码逻辑比较复杂时,我们还可以表示为:
void test_1() {
auto func1 = [](int A, int B)
{
...... //body
};
}
1.3 参数详解
1. capture 捕获列表
捕捉列表描述了上下文中那些数据可以被 lambda
使用,以及使用的方式传值还是传引用。
举个栗子:
int A = 1, B = 2, C = 1;
auto func2 = [A]() { cout << A << endl; };
func2();
这里就是告诉编译器你可以使用 A变量
,当然了,你想让他用谁就放谁到捕获列表中:
int A = 1, B = 2;
auto func2 = [A,B]()
{
cout << A << endl;
cout << B << endl;
};
func2();
在这里 []
的用法可以简单总结为 3 种:
- [var]:表示值传递方式捕捉变量 var
- [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
- [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
注意:父作用域指包含 lambda函数 的语句块
上述的方法甚至还可以混合使用:
int A = 1, B = 2, C = 3;
// 除了 C 都是值传递,而 C 是引用传递
auto func2 = [=, &C]()
{
//
};
// 除了 C 都是引用传递,而 C 是值传递
auto func3 = [&, C]()
{
//
};
2. parameters 参数列表
参数列表的用法和函数的参数列表都是一致的,没有什么区别。但是如果你没有需要传递的参数时,甚至可以省去:
auto func3 = [A] { cout << A << endl; };
3. mutable 可变的
当我们的表达式尝试改变 以值捕获 的外部变量时,编译器会报错,比如:
auto func3 = [A] () { ++A; };
这是因为编译器规定这些变量只是可读的,如果你想要进行相应的修改,需要加上 mutable
:
auto func3 = [A] () mutable { ++A; };
但是这个修改并不会影响 A
本身的的大小,因为是 深拷贝,所以你需要修改 A
本身的大小的话使用引用会更好。
4. return_type 返回类型
如果你没什么需要特殊需要的话,完全可以省去,因为编译器会自动推导,那什么时候是特殊的需求呢,就比如:
double A = 1.5, B = 2.5, C = 3;
auto func2 = [A, B] () -> int
{
return A * B;
};
cout << func2() << endl;
在这里两个 double
变量相乘推导出肯定是返回 double
,但是我想要返回 int
这就可以指定返回类型。
5. body 函数体
在函数体中,你可以使用参数列表以及捕获列表中的变量。
1.4 lambda 背后的逻辑
我们是怎么使用仿函数的呢?就比如一下比较大小的仿函数:
template<class T>
struct Less{
bool operator()(const T& left, const T& right){
left < right;
}
};
Less<int> less;
int A = 1, B = 0;
less(A, B);
我们在使用该仿函数之前,先使用创建了一个相应的对象,然后再使用。
再看看我们的 lambda 表达式
,我们也是先创建一个变量再通过该变量来调用:auto func = [](){};
。 但是我们在前面说过, lambda表达式
是一种定义 匿名函数对象
的方式。所有他是真的没有名字吗,不是的,他在背后其实编译器给他取了个名字的,只是我们不需要知道,我们只需要使用 auto
接受接好了,不需要管他叫什么。
那怎么证明呢?我们看看汇编层的逻辑:
很明显是由他是有名字的,只是只有编译器知道而已。
所以,定义了一个 lambda表达式
,编译器会自动生成一个类,在该类中重载了 operator()
。
2. lambda 的使用场景
2.1 sort 函数
如果我们需要对自定义类型进行排序,举个例子:
class Man {
public:
Man(string name, int age) {
_name = name;
_age = age;
}
string _name;
int _age;
};
void test_2() {
vector<Man> v;
v.emplace_back("L", 10);
v.emplace_back("M", 11);
v.emplace_back("N", 12);
sort(v.begin(), v.end());
}
这样会报错,因为自定义类型不可以直接比较,我们在以前的解决方案可以是写一个仿函数,告诉编译器怎么比较呀:
struct LessForMansAge {
bool operator()(const Man& left, const Man& right) {
return left._age < right._age;
}
};
现在的话,我们可以直接写一个仿函数:
auto Less = [](const Man& left, const Man& right) { return left._age < right._age; };
sort(v.begin(), v.end(), Less);
2.2 for_each 函数
for_each
函数是 C++ 标准库中的一个算法,它定义在头文件 中。这个函数用于对给定范围内的每个元素执行一个指定的操作。for_each
算法提供了一个便利的、统一的方法来遍历容器(或其他支持迭代器的范围),并对每个元素执行某个操作,而不需要显式地编写循环代码。
举个例子,我想要让数组内的元素 * 2 ,并打印结果:
void test_3() {
vector<int> v = { 1, 2, 3, 4, 5, 6 };
for_each(v.begin(), v.end(), [](int& x) { x *= 2; });
for_each(v.begin(), v.end(), [](int& x) { cout << x << endl; });
}
3. 总结
lambda表达式
的语法更加直观和灵活,特别是在处理简单函数或回调函数时。它允许开发者直接在表达式中捕获外部变量,并定义函数体,这使得代码更加易于编写和理解。