在 C++ 中,Lambda 表达式也叫做 “匿名函数”。简单来说,它就是一段没有函数名,但依然可以像普通函数一样接收参数并返回结果的代码块。
在前面学习函数时,我们需要先到 main() 外面起好名字、写完定义,再回来调用。但如果某段比较或筛选逻辑只有短短一两行,而且只在一个地方临时使用一次,直接使用 Lambda 表达式在当前位置就地编写会方便得多。
Lambda 的基本语法
普通函数由返回类型、函数名、参数列表和函数体 4 个部分组成。Lambda 表达式省去了函数名,它的基本结构由 3 对括号组成。
语法:
[捕获列表](参数列表) {
// 函数体
}说明:
上面这 3 个部分各自的作用如下:
- 捕获列表 []:最前面的方括号是 Lambda 表达式的标志,用来指定函数体内部如何使用外面的局部变量。如果不使用任何外部局部变量,直接写成空的
[]即可。 - 参数列表 ():和普通函数的参数列表一样,用来接收调用时传入的数据,比如
(int a, int b)。如果不需要接收任何参数,保留一对空圆括号()即可。 - 函数体 {}:需要执行的代码都写在花括号内部。如果里面只有单一类型的
return语句,编译器会自动根据返回值推导出返回类型,不需要我们手动写出返回类型。
示例 1:定义并调用 Lambda
#include <iostream>
int main() {
auto add = [](int a, int b) {
return a + b;
};
std::cout << add(10, 20) << '\n';
return 0;
}运行结果如下。
30分析:
在这个例子中,我们定义了一个接收两个整数并返回两数之和的 Lambda 表达式,并通过变量 add 把它保存起来。随后就可以像调用普通函数一样,写出 add(10, 20) 传入参数并得到计算结果 30。
为什么保存 Lambda 时要使用 auto 关键字呢?因为每一个 Lambda 表达式在底层都会由编译器自动生成一个独一无二的匿名类型,这个类型名我们没法手写出来。所以只要想把 Lambda 保存到变量里,直接使用 auto 让编译器自动推导即可。
注意 当我们将 Lambda 表达式赋值给一个变量时,整条语句本质上是一条变量定义语句。因此在函数体的右花括号 } 后面,小伙伴们千万不要漏写末尾的英文分号(;)。
如果 Lambda 不需要接收参数,参数列表直接写 () 即可。此外,当函数体里包含多个返回类型不一致的 return 分支,或者我们希望强制指定返回类型时,可以在参数列表后面加上 -> 类型 显式标出。请看下面例子。
示例 2:无参 Lambda 与返回类型
#include <iostream>
int main() {
// 无参数的 Lambda
auto sayHello = []() {
std::cout << "欢迎来到绿叶网!\n";
};
// 显式指定返回类型为 double
auto half = [](int n) -> double {
if (n <= 0) {
return 0; // 返回 int 类型
}
return n / 2.0; // 返回 double 类型
};
sayHello();
std::cout << half(15) << '\n';
return 0;
}运行结果如下。
欢迎来到绿叶网!
7.5分析:
sayHello 没有接收任何参数,我们调用时直接写 sayHello() 即可。虽然在没有参数时语法上也允许把定义处的 () 省掉,但平时写代码更推荐保留 (),这样能直观看出参数列表是空的。
再看下面的 half,它的函数体里有两个 return 分支:第 1 个分支返回整数 0(int),第 2 个分支返回小数 n / 2.0(double)。如果不写 -> double,编译器看到前后两个返回值的类型不一致就会直接报错。
显式加上 -> double 后,两个分支的返回值都会统一转换为 double 类型。平时如果函数体里的返回类型单一明确,我们直接省略 -> 类型 交给编译器自动推导就可以了。
Lambda 捕获外部变量
在编写 Lambda 表达式时,我们有时不仅要处理传入的参数,还需要用到外面已经定义好的局部变量。默认情况下,Lambda 的函数体是一个相对独立的小空间,不能直接使用当前函数里的其他局部变量,只有像 std::cout 这样的全局对象或全局变量可以直接使用。
你可以把最前面的方括号 [] 看作一份 “通行证名单”,只有把外部局部变量写进方括号里,Lambda 内部才能访问它。

这个把外部局部变量引入 Lambda 内部的过程,就叫做 “捕获”。在 C++ 中,捕获外部变量一般分为 2 种基本方式:一种是 “值捕获”,另一种是 “引用捕获”。
1. 值捕获:[变量名]
如果我们希望 Lambda 拿到外部变量在创建那一刻的一份 “拷贝副本”,可以直接把变量名写进方括号中,比如 [limit]。
语法:
[变量名](参数列表) {
// 函数体
}说明:
采用 [变量名] 值捕获时,具有以下 2 个特点:
- 创建时复制副本:在 Lambda 创建的那一刻,它会把外部变量当前的值复制一份保存起来。之后哪怕外面的原变量再被修改,Lambda 内部保存的那份副本也不会跟着变。
- 默认只读:默认情况下,值捕获得到的这份副本在 Lambda 内部只能读取,不能直接修改。
示例 3:使用值捕获
#include <iostream>
int main() {
int limit = 60;
// 按值捕获 limit
auto showLimit = [limit]() {
std::cout << limit << '\n';
};
limit = 80; // 修改外部的原变量
std::cout << limit << '\n';
showLimit();
return 0;
}运行结果如下。
80
60分析:
在定义 showLimit 的那一刻,外面的 limit 是 60,[limit] 复制保存的副本就是 60,就像给当时的变量拍了一张照片。
需要注意的是,值捕获复制数据发生在 “定义 Lambda 时”,而不是 “调用时”。因此后面哪怕执行 limit = 80; 改了原变量,再调用 showLimit() 打印出来的依然是照片里的 60。
另外还要留意,值捕获得到的副本默认是只读的,下面这种在内部直接修改它的写法会编译报错。
int limit = 60;
auto fn = [limit]() {
limit = 100; // 编译报错
};如果小伙伴们确实需要在 Lambda 内部修改外面的原变量,就应该使用下面介绍的 “引用捕获”。
2. 引用捕获:[&变量名]
如果我们不希望复制副本,而是希望在 Lambda 内部直接操作外面的那个原变量,只需要在变量名前面加上 & 符号,比如 [&total]。
语法:
[&变量名](参数列表) {
// 函数体
}说明:
这里的 & 和前面学过的引用是同一个概念。采用 [&变量名] 引用捕获后,Lambda 内部访问的就是外部的原变量本身。外部变量后来改了值,Lambda 内部读到的就是最新值。反过来,Lambda 内部修改了它,外面的原变量也会直接跟着改变。
示例 4:使用引用捕获
#include <iostream>
int main() {
int total = 10;
// 按引用捕获 total
auto addScore = [&total](int val) {
total += val;
};
total = 20;
addScore(5);
std::cout << total << '\n';
return 0;
}运行结果如下。
25分析:
[&total] 表示按引用捕获外部变量 total,addScore 自始至终关联的都是外面的原变量。因此外面执行 total = 20; 后,紧接着我们调用 addScore(5) 执行 total += 5;,就会直接把外面的 total 修改成 25。
对比示例 3 和示例 4 可以看出,值捕获 [x] 定格的是创建那一刻的拷贝数值。而引用捕获 [&x] 则是直接关联原变量本身,无论外面怎么变、里面怎么改,操作的都是同一个变量。
3. 混合捕获与默认捕获
当 Lambda 内部需要同时用到多个外部局部变量时,我们可以在 [] 中用英文逗号将它们隔开,分别指定值捕获或引用捕获。此外,如果用到的外部变量较多,C++ 还支持通过 [=] 或 [&] 进行默认捕获。
语法:
[x, &y](参数列表) { ... }
[=](参数列表) { ... }
[&](参数列表) { ... }说明:
这 3 种捕获写法的含义如下:
- [x, &y]:混合捕获,表示变量
x按值捕获、变量y按引用捕获,多个变量之间用英文逗号隔开。 - [=]:默认值捕获,表示函数体内部用到的外部局部变量全部按值捕获。
- [&]:默认引用捕获,表示函数体内部用到的外部局部变量全部按引用捕获。
需要注意的是,写成 [=] 或 [&] 并不是把当前函数里定义的所有变量全部打包进 Lambda。只有当 Lambda 函数体内部真正用到了某个外部变量时,编译器才会自动捕获它。
示例 5:混合捕获外部变量
#include <iostream>
#include <vector>
int main() {
std::vector<int> scores = {55, 80, 45, 90, 60};
int limit = 60;
int passCount = 0;
// limit 按值捕获,passCount 按引用捕获
auto checkScore = [limit, &passCount](int s) {
if (s >= limit) {
passCount++;
}
};
for (int s : scores) {
checkScore(s);
}
std::cout << passCount << '\n';
return 0;
}运行结果如下。
3分析:
在这个例子中,及格线 limit 只需读取、不用修改,因此采用值捕获 limit。而 passCount 每次遇到及格分数都要加 1 并把结果带回外面,因此采用引用捕获 &passCount,最终统计出 3 人及格。
初学的小伙伴如果分不清 [] 和 () 的分工,可以对比这个例子来记。limit 和 passCount 是外面已有的局部变量,要写进捕获列表 [];s 是每次调用时传入的参数,要写进参数列表 ()。
接下来,我们再通过一个例子直观对比一下默认值捕获 [=] 与默认引用捕获 [&] 的区别。
示例 6:默认捕获 [=] 与 [&]
#include <iostream>
int main() {
int a = 10;
int b = 20;
// 默认按值捕获
auto sumByVal = [=]() {
return a + b;
};
// 默认按引用捕获
auto sumByRef = [&]() {
return a + b;
};
a = 100;
std::cout << sumByVal() << '\n';
std::cout << sumByRef() << '\n';
return 0;
}运行结果如下。
30
120分析:
从运行结果可以看出,sumByVal 使用了 [=],相当于 [a, b]。它在定义那一刻就已经把 10 和 20 复制保存好了,因此算出来是 30。
而 sumByRef 使用了 [&],相当于 [&a, &b],直接关联外面的原变量。当我们在外部执行 a = 100; 后再调用它,算出来的就是最新的 120。
将 Lambda 直接作为函数参数
在前面的例子中,为了方便分步观察,我们都先通过 auto 变量把 Lambda 表达式保存起来,然后再去调用它。
不过在实际开发中,我们更常把 Lambda 直接写在其他函数的参数括号里当场使用,连变量名都省了,这也是它叫 “匿名函数” 的原因。比如调用 <algorithm> 中的 std::sort() 时,就可以直接传入 Lambda 来指定排序规则。
示例 7:Lambda 直接作函数参数
#include <algorithm> // 提供 std::sort()
#include <iostream>
#include <vector>
int main() {
std::vector<int> nums = {30, 10, 40, 20};
// 传入 Lambda 按从大到小排序
std::sort(nums.begin(), nums.end(), [](int a, int b) {
return a > b;
});
for (int n : nums) {
std::cout << n << ' ';
}
std::cout << '\n';
return 0;
}运行结果如下。
40 30 20 10分析:
在这个例子中,我们直接把 Lambda 表达式写在了 std::sort() 的第 3 个参数位置上。排序时,函数会自动取出待比较的两个元素传给 a 和 b,return a > b; 表示让较大的数排在前面,最终输出 40 30 20 10。
关于自定义排序的更多用法,我们会在下一节 “C++ sort 排序” 中展开介绍。平时在项目中使用 Lambda 表达式时,大家还需要留意以下 2 点:
- 优先使用显式捕获:虽然
[=]和[&]写起来省事,但很难直观看出用到了哪些外部变量,尤其是[&]容易误改数据。实际开发中更推荐写成[limit, &passCount]这种显式形式,数据来源更清晰。 - 注意引用捕获的生命周期:使用
[&x]或[&]时,必须确保调用 Lambda 时原局部变量仍然存在。如果把带引用捕获的 Lambda 返回到函数外部延后调用,原局部变量早已销毁,就会引发未定义行为,导致读出乱码或程序崩溃。
常见问题
1. 在实际开发中,什么时候使用 Lambda,什么时候定义普通函数?
如果某段逻辑只有短短几行,比如给排序或查找函数传比较规则,或者需要直接捕获当前函数里的局部变量,优先使用 Lambda 表达式就地编写最省事。
反过来,如果函数体逻辑比较复杂,或者需要在多个地方反复调用,我们就应该单独定义一个普通函数。给函数起一个清晰的名字,后面维护起来会容易得多。
2. 值捕获的变量副本在 Lambda 内部完全不能修改吗?
默认不能修改。如果确实想修改这份拷贝副本,可以在 () 后面加上 mutable 关键字,比如写成 [limit]() mutable { limit = 100; },修改后也不会影响原变量。这种写法很少用到,小伙伴们简单了解一下即可。
