在这篇博客中,我会尽量用简单易懂的语言和相关的例子来解释C++11中的lambda表达式的用法以及为什么要学习它们,内容不难,多多应用才是关键。
1. 什么是Lambda表达式?
Lambda表达式是一种匿名函数,也就是说,它们没有名字,可以在代码中被直接定义和使用。它们通常用于简化代码,特别是在需要传递小块逻辑(比如排序、过滤、或遍历集合)的时候。这么说还是很抽象,不如直接看例子。
例子:
// 使用命名函数
bool isEven(int n) {return n % 2 == 0;
}
std::vector<int> v = {1, 2, 3, 4, 5};
vec.erase(std::remove_if(vec.begin(), vec.end(), isEven), vec.end());// 使用lambda表达式
std::vector<int> vec = {1, 2, 3, 4, 5};
vec.erase(std::remove_if(vec.begin(), vec.end(), [](int n)
{ return n % 2 != 0; }), vec.end());
【解释】
这几行代码的目的是从vec
(一个std::vector<int>
类型的容器)中移除所有奇数元素。
std::remove_if(vec.begin(), vec.end(), [](int n) { return n % 2 != 0; })
:
std::remove_if
是标准库中的一个算法,它将所有满足给定条件的元素移到容器末尾,并返回一个新的“末尾”迭代器。[](int n) { return n % 2 != 0; }
是一个lambda表达式,表示一个匿名函数。这个lambda表达式接收一个整数参数n
,并返回n % 2 != 0
的结果,即当n
是奇数时返回true
,否则返回false
。
所以,std::remove_if
会遍历vec
中的每个元素,并将所有奇数元素移到容器末尾,返回指向第一个被移除的奇数元素的位置。在这里,std::remove_if
返回的新“末尾”迭代器作为vec.erase
的第一个参数,vec.end()
作为第二个参数。这样,vec.erase
就会移除所有被std::remove_if
标记为奇数的元素。
2. 使用思路一:直接调用
注意:lambda表达式是一种匿名函数,没有名称,因此不能像普通函数那样直接调用。为了使用lambda表达式,需要将其赋值给一个变量,以便可以通过这个变量调用它。此外,lambda表达式的类型是编译器生成的一个匿名类型,通常无法显式声明,所以需要使用auto
关键字进行类型推断。
Lambda表达式在C++中的语法可以分为几个部分,每个部分都有其特定的作用。让我们来看一下它的基本语法:
[capture-list](parameters) -> return_type { statement }
[捕获列表](参数列表) -> 返回类型 { 函数体 }
a. 捕获列表 [ ]
捕获列表用于捕获作用域内的变量,使得lambda表达式可以访问这些变量。可以有以下几种方式:
- [ ]:不捕获任何变量。
- 按值捕捉(=):捕捉外部作用域中的变量的副本。
- 按引用捕捉(&):捕捉外部作用域中的变量的引用。
- 捕捉特定变量:可以按值或按引用捕捉特定的变量。
- 捕捉所有外部变量(= 或 &):分别按值或按引用捕捉所有外部变量。
- 混合捕捉:按值和按引用混合捕捉特定的变量。
b. 参数列表 ( )
参数列表与普通函数的参数列表类似,可以为空,也可以包含多个参数。
c. 返回类型 ->
返回类型可以省略,编译器会自动推断。如果明确指定返回类型,可以使用->
关键字。
d. 函数体 { }
函数体包含lambda表达式的实际代码逻辑。
示例
以下是几个简单的例子来展示lambda表达式的不同用法:
例子1:基本的lambda表达式
auto func = []() { return 42; };std::cout << func() << std::endl; // 输出 42
这里的lambda表达式没有捕获任何变量,没有参数,返回整数42
。
例子2:捕捉列表的使用
- 按值捕捉:
int x = 10;auto lambda = [=]() {std::cout << "x = " << x << std::endl; // 按值捕捉 x};lambda();
- 按引用捕捉:
int x = 10;auto lambda = [&]() {x = 20; // 可以修改外部的 xstd::cout << "x = " << x << std::endl;};lambda();std::cout << "Outside lambda, x = " << x << std::endl; // x 已被修改
- 捕捉特定变量:
int x = 10;int y = 20;auto lambda = [x, &y]() {// 按值捕捉 x,按引用捕捉 y// x 不能被修改,y 可以被修改std::cout << "x = " << x << ", y = " << y << std::endl;y = 30;};lambda();std::cout << "Outside lambda, y = " << y << std::endl; // y 已被修改
- 捕捉所有外部变量:
int x = 10;int y = 20;auto lambda = [=, &y]() {// 按值捕捉所有变量,但 y 按引用捕捉std::cout << "x = " << x << ", y = " << y << std::endl;y = 30;};lambda();std::cout << "Outside lambda, y = " << y << std::endl; // y 已被修改
- 混合捕捉:
int x = 10;int y = 20;auto lambda = [=, &y, this]() {// 按值捕捉所有变量,但 y 按引用捕捉,捕捉 this 指针(在类中使用时有效)std::cout << "x = " << x << ", y = " << y << std::endl;y = 30;};lambda();std::cout << "Outside lambda, y = " << y << std::endl; // y 已被修改
- Lambda表达式本质上是一个可调用对象(类似于一个带有
operator()
的类)。将lambda表达式赋值给变量后,可以通过该变量调用这个可调用对象,就像调用一个普通函数一样。
3. 使用思路二:配套STL
就回到刚刚引入的那个例子:
在使用标准库算法时,可以直接在函数调用中定义lambda表达式:
#include <iostream>
#include <vector>
#include <algorithm>int main() {std::vector<int> vec = {1, 2, 3, 4, 5, 6};// 直接在std::remove_if调用中定义lambda表达式vec.erase(std::remove_if(vec.begin(), vec.end(), [](int n) { return n % 2 == 0; }),vec.end());return 0;
}
在这个例子中,lambda表达式[](int n) { return n % 2 == 0; }
直接作为参数传递给std::remove_if
,不需要赋值给变量。
为什么要使用Lambda?
-
代码简洁:Lambda表达式让你可以在需要的地方直接定义和使用函数,而不需要单独定义一个命名函数,这使得代码更加紧凑和易读。
-
灵活性:Lambda表达式可以捕获周围的变量,这让它们非常灵活,能够在局部范围内使用外部变量,而不需要通过参数传递。
-
性能:由于lambda表达式是内联的,它们可能会比普通函数调用更高效,因为编译器可以对它们进行优化。
-
标准库兼容性:C++标准库(如STL)中很多算法和函数都支持使用lambda表达式,这使得你可以更好地利用标准库的强大功能。
什么时候使用Lambda表达式?
- 当你需要传递小块逻辑时,如排序、过滤或遍历集合。
- 当函数只在局部范围内使用,不需要全局访问时。
- 当你希望保持代码简洁和易读,避免定义过多的命名函数时。
lambda和仿函数
Lambda表达式和仿函数(函数对象)在C++中都是可调用对象,主要用于传递逻辑到算法或容器中。它们在功能上有许多相似之处,但在使用方式和实现细节上有一些重要的区别。
共同点
-
可调用对象:
- 仿函数和lambda表达式都是可调用对象,可以像函数一样调用。
- 它们都可以传递给标准库算法,如
std::sort
、std::for_each
等。
-
状态:
- 仿函数和lambda表达式都可以保存状态(即捕获变量或成员变量),这使得它们比普通函数指针更强大。
区别
-
语法和定义方式:
- 仿函数:仿函数通常是一个类或结构体,重载了
operator()
。定义比较繁琐,需要单独定义一个类型。 - lambda表达式:lambda表达式是一个匿名函数,可以在需要的地方直接定义和使用。定义更加简洁。
仿函数示例:
struct Add {int operator()(int a, int b) const {return a + b;} };Add add; int result = add(3, 4); // 调用仿函数
lambda表达式示例:
auto add = [](int a, int b) { return a + b; }; int result = add(3, 4); // 调用lambda表达式
- 仿函数:仿函数通常是一个类或结构体,重载了
-
捕获变量:
- 仿函数:仿函数可以通过成员变量保存状态,但需要显式定义和初始化这些成员变量。
- lambda表达式:lambda表达式可以使用捕获列表捕获作用域内的变量,捕获方式包括按值捕获和按引用捕获。
-
类型:
- 仿函数:仿函数有明确的类型,可以通过类名来引用。
- lambda表达式:lambda表达式的类型是匿名的、编译器生成的类型,通常需要使用
auto
关键字进行类型推断。
-
编译器支持:
- 仿函数:仿函数在C++98及以后版本都可以使用。
- lambda表达式:lambda表达式是C++11引入的新特性,只有在支持C++11及以上标准的编译器中才能使用。
性能和用途
- 性能:在大多数情况下,lambda表达式和仿函数在性能上是相似的。由于lambda表达式通常是内联的,编译器可以对它们进行优化,有时甚至比仿函数更高效。
- 用途:lambda表达式更适合定义短小、临时的可调用对象,而仿函数更适合定义复杂的、重用的逻辑。
总结
- 仿函数:适用于需要定义复杂逻辑和保存状态的场景,需要单独定义类型。
- lambda表达式:适用于需要简洁地定义和使用匿名函数的场景,语法更加简洁,支持捕获局部变量。
选择使用仿函数还是lambda表达式,主要取决于具体需求和代码的复杂性。如果你需要定义一个复杂的、带状态的可调用对象,仿函数可能是更好的选择;如果你需要简洁地传递一小段逻辑,lambda表达式则更加方便。