← 目录 / 第八章 · 函数 / 8.4 Lambda 表达式

8.4 Lambda 表达式

有些函数只会在一个地方用一次,专门给它取个名字、写在别处,反而显得麻烦。本节介绍一种可以"就地"写出来、不需要名字的函数——lambda 表达式。

为什么需要 lambda 表达式

还记得 11.13.1 节里,给 sort 传第三个参数来自定义排序规则吗?如果想按降序排序,一种办法是专门写一个具名函数:

🏷️ 专门取名字、写在外面
bool Cmp(int a, int b)
{
  return a > b;
}

sort(v.begin(), v.end(), Cmp);
✏️ 就地写一个 lambda
sort(v.begin(), v.end(),
  [](int a, int b) {
    return a > b;
  });

Cmp 这个函数只在这一次 sort 调用里用得上,别的地方根本不会再调用它。为它专门取一个名字、写在主函数外面,读代码的人还得跳过去才能看懂它是干什么的——有点"杀鸡用牛刀"。

Lambda 表达式(Lambda Expression)就是 C++11 引入的"匿名函数"写法:不需要取名字,可以直接写在使用它的地方,逻辑和调用挨在一起,一眼就能看懂。

📝
便签条 vs 正式文件:具名函数像是把一份说明正式归档——起标题、放进文件夹,方便以后反复查阅。Lambda 表达式则像是在需要的时候随手写一张便签条,写完就贴在当场用,不需要标题,也不打算留着以后用。两种方式都有用,关键是看这段逻辑要不要被"反复使用/单独查阅"。

lambda 的语法结构

Lambda 表达式看起来和普通函数不太一样,但拆开来看,组成部分是相通的:

[a, &b] (int x)  ->  int   { return a + b + x; }
捕获列表 — 允许这个 lambda 使用外部的哪些变量
参数列表 — 和普通函数参数完全一样
返回类型 — 可省略,编译器会自动推断
函数体 — 和普通函数体写法完全一样
组成部分说明是否必须
捕获列表 [ ]决定这个 lambda 能不能、以及怎样使用外部(lambda 所在函数里)的变量,详见 8.4.3✅ 必需(不需要用外部变量时写 [])
参数列表 ( )和普通函数的参数列表写法完全一样,调用时传入的数据可选(无参数时可以省略括号里的内容,甚至连括号也可以省略)
返回类型 -> 类型函数体只有一条 return 语句时,编译器能自动推断出返回类型,通常可以省略;逻辑复杂、有多条不同类型的 return 时才需要手动写明通常可省略
函数体 { }花括号内要执行的具体代码,写法和普通函数体完全一样✅ 必需
C++ · lambda 基础写法
1int main()
2{
3 auto add = [](int a, int b) { return a + b; }; // 把 lambda 存进变量 add
4 cout << add(3, 4); // 像调用普通函数一样调用,输出 7
5 return 0;
6}
🎯
为什么存 lambda 的变量要用 auto?每一个 lambda 表达式,编译器背地里都会为它生成一个独一无二、没有名字的类型,你没办法把这个类型显式地写出来。auto(见 2.1 节)能让编译器自动帮你推断出这个类型,这也是存储 lambda 时几乎总是搭配 auto 使用的原因。

捕获列表详解

普通函数写在其他函数外面,本来就看不到别的函数里的局部变量,这一点很容易理解(见 2.6 节作用域)。但 lambda 表达式比较特殊:它是写在另一个函数内部的,按理说应该能"顺手"看到周围的变量——但 C++ 出于安全考虑,要求你必须在捕获列表 [ ] 里明确写出"我要用外面的哪个变量",没有写的变量,lambda 内部一律看不到。

写法含义
[ ]不捕获任何外部变量,lambda 内部只能用自己的参数和局部变量
[x]按值捕获变量 x(拷贝一份快照进来,见下方对比)
[&x]按引用捕获变量 x(和外部的 x 是同一份数据)
[=]用到的外部变量全部按值捕获,不用逐个列出来
[&]用到的外部变量全部按引用捕获,不用逐个列出来
[x, &y]混合写法:x 按值捕获,y 按引用捕获
[this]捕获当前对象(12 章学到类之后,在成员函数里写 lambda 时会用到)
📦 按值捕获 [x]
int x = 10;
auto f = [x]() { return x; };
x = 99; // 修改外部 x
f(); // 结果仍是 10
lambda 创建那一刻就把 x 的值拷贝了一份"快照"进来,之后外部 x 再怎么变,都不影响 lambda 内部这份独立的拷贝。
🔗 按引用捕获 [&x]
int x = 10;
auto f = [&x]() { return x; };
x = 99; // 修改外部 x
f(); // 结果变成 99
lambda 内部的 x 和外部的 x 是同一份数据,没有任何拷贝,外部改了、里面读到的也跟着变。
🚫
引用捕获的高危场景——悬空引用:如果一个函数把 lambda 作为返回值返回出去,而这个 lambda 又按引用捕获了该函数内部的局部变量,那么函数一返回,这些局部变量就已经被销毁了(本质和 9 章要讲的"指针指向已释放内存"是同一类陷阱)——之后再调用这个 lambda,读到的就是已经不存在的数据,属于未定义行为。经验法则:如果 lambda 的生命周期可能比它捕获的变量更长,优先用按值捕获 [x],更安全。
💡
进阶:想在 lambda 内部修改按值捕获的变量?默认情况下,按值捕获进来的变量在 lambda 内部是只读的(相当于自带 const),直接赋值会编译报错。如果确实需要修改这份内部拷贝(注意:改的只是 lambda 内部的副本,不会影响外部原变量),需要在参数列表后面加上 mutable 关键字:[x]() mutable { x++; return x; }。竞赛中较少用到,了解即可。

lambda 与算法结合使用

Lambda 表达式最常见的用途,就是给 STL 算法函数临时提供一条规则——大多数场景根本用不到复杂的捕获,一个 [] 就够了:

C++ · 降序排序
1sort(v.begin(), v.end(),
2 [](int a, int b) { return a > b; }); // 从大到小

有些场景排序规则要参考另一个数组,这时捕获列表就派上用场了——正是 11.13.4 节"间接排序"用到的手法:

C++ · 按 data 的值对下标数组 idx 排序
1vector<int> data = {40, 10, 30};
2vector<int> idx = {0, 1, 2}; // 待排序的是下标,不是 data 本身
3sort(idx.begin(), idx.end(),
4 [&](int i, int j) { return data[i] < data[j]; }); // 引用捕获 data
5// 排序后 idx = {1, 2, 0},因为 data[1]=10 < data[2]=30 < data[0]=40

这里必须用 [&] 引用捕获 data:如果按值捕获,lambda 内部会拷贝一份 data 的副本,虽然结果依然正确,但对于较大的数组来说是一笔不必要的开销;引用捕获则直接复用外部的 data,不产生额外拷贝。

类似地,priority_queue(见 11.7 节)自定义比较规则、unordered_map 自定义 find_if 查找条件等场景,都是 lambda 大展身手的地方——它们的共同点是:逻辑简单、只用一次,不值得专门写一个具名函数。

易错点

1
忘记把外部变量写进捕获列表:写了 [](或者压根没写捕获列表里想用的变量),却在函数体里直接用了外面的变量,编译器会直接报错——提示这个变量"没有被捕获"。解决办法很直接:把它加进 [ ] 里,或者偷懒用 [=] / [&] 一次性捕获所有用到的变量。
2
误以为按值捕获会"实时同步":按值捕获 [x] 拷贝的是创建 lambda 那一刻的值,之后外部 x 再变化,lambda 内部看到的还是旧值(见 8.4.3 的对比示例)。如果需要 lambda 始终读到最新值,要改用引用捕获 [&x]。
3
返回按引用捕获局部变量的 lambda:函数返回后,函数内的局部变量已经被销毁,被引用捕获的 lambda 再被调用就是在读已经不存在的数据(8.4.3 节的悬空引用警告)。函数内部使用的 lambda 引用捕获很安全,但如果要把 lambda 作为返回值传出函数,优先用按值捕获。
4
想显式声明 lambda 变量的类型,却不知道该写什么:lambda 的类型是编译器生成的匿名类型,写不出来,只能用 auto。(进阶:如果确实需要把 lambda 存成类的成员、或者作为函数参数类型传递,可以用标准库提供的 std::function<返回类型(参数类型...)> 包装它,需要 <functional> 头文件——竞赛中较少用到,了解即可。)
✅ 适合用 lambda
· 逻辑简短,只在一处使用
· 给 sort / priority_queue 等提供临时规则
· 需要顺手用到周围的局部变量
❌ 更适合具名函数
· 同一段逻辑要在多处反复调用
· 逻辑复杂、写出来很长,写在调用处反而影响阅读
· 需要在多个文件之间共享