7. Lambda 基础语法:[捕获列表](参数列表) -> 返回类型 { 函数体 }
Lambda 表达式,说白了就是 C++11 引入的一种「匿名函数对象」。你不需要给它起名字,也不需要单独写一个函数或者仿函数类。直接在需要的地方写一段逻辑,编译器会帮你生成一个对应的函数对象。
我第一次接触 Lambda 时,觉得这玩意儿不就是语法糖吗?后来在项目里重构回调代码,才发现它有多香。嗯,咱们今天就把它的骨架拆开看看。
7.1 语法结构总览
Lambda 的完整语法长这样:
[捕获列表](参数列表) -> 返回类型 { 函数体 }
四个部分,缺一不可?其实不是。有些部分可以省略。我习惯把它拆成三块来看:
- 捕获列表:决定 Lambda 能访问外部哪些变量
- 参数列表 + 返回类型:决定调用时的接口
- 函数体:具体执行的逻辑
你想想看,这跟普通函数最大的区别在哪?普通函数没有捕获列表。Lambda 可以「记住」它定义时的上下文变量,这是它最核心的能力。
7.2 捕获列表详解
捕获列表是 Lambda 的灵魂。我见过不少新手在这里踩坑,包括我自己早期也犯过迷糊。
| 捕获方式 | 写法 | 说明 |
|---|---|---|
| 值捕获 | [x] |
捕获变量 x 的副本,Lambda 内部修改不影响外部 |
| 引用捕获 | [&x] |
捕获变量 x 的引用,内部修改会反映到外部 |
| 隐式值捕获 | [=] |
按值捕获所有用到的外部变量 |
| 隐式引用捕获 | [&] |
按引用捕获所有用到的外部变量 |
| 混合捕获 | [=, &x] |
除 x 按引用外,其余按值捕获 |
| 初始化捕获 | [y = 42] |
C++14 起支持,捕获一个表达式的结果 |
注意:引用捕获要特别小心生命周期。我曾经在项目里写过一段代码,Lambda 里捕获了一个局部变量的引用,结果 Lambda 被延迟执行时,那个变量已经销毁了。嗯,调试了整整一个下午。
7.3 参数列表与返回类型
参数列表跟普通函数一样,支持默认参数、模板参数(C++20)。返回类型可以省略,让编译器自动推导。
// 自动推导返回类型
auto lambda1 = [](int a, int b) { return a + b; };
// 显式指定返回类型
auto lambda2 = [](int a, int b) -> double { return a / b; };
// 无参数时可以省略括号
auto lambda3 = []{ return 42; };
我个人习惯:如果函数体只有一行 return,就让编译器推导。如果逻辑复杂,我会显式写出返回类型。这样代码更清晰,也避免推导出意外的类型。
7.4 函数体与 mutable 关键字
默认情况下,值捕获的变量在 Lambda 内部是 const 的。你不能修改它们。为什么?因为 Lambda 生成的函数对象,其 operator() 默认是 const 的。
如果你非要修改值捕获的变量,加个 mutable 就行:
int count = 0;
auto counter = [count]() mutable {
return ++count;
};
std::cout << counter(); // 输出 1
std::cout << counter(); // 输出 2
std::cout << count; // 输出 0,外部不受影响
小技巧:mutable 只影响值捕获的副本。引用捕获的变量本来就能修改,不需要 mutable。我刚开始学的时候搞混过,后来记住一句话:「值捕获是只读的,想写就加 mutable」。
7.5 知识结构图
下面这张图把 Lambda 基础语法的核心脉络梳理了一遍。你可以把它当作一个快速索引。
7.6 完整示例:从简单到复杂
咱们看几个实际例子。这些代码我都跑过,你可以直接复制去验证。
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
// 最简单的 Lambda
auto greet = []{ std::cout << "Hello\n"; };
greet();
// 带参数的 Lambda
auto add = [](int a, int b) { return a + b; };
std::cout << add(3, 4) << "\n"; // 7
// 捕获外部变量
int multiplier = 10;
auto times = [multiplier](int x) { return x * multiplier; };
std::cout << times(5) << "\n"; // 50
// 引用捕获 + 修改外部变量
int sum = 0;
auto accumulator = [&sum](int x) { sum += x; };
accumulator(10);
accumulator(20);
std::cout << sum << "\n"; // 30
// 混合捕获
int factor = 2;
auto compute = [=, &sum](int x) {
sum += x * factor;
};
compute(5);
std::cout << sum << "\n"; // 40
// 初始化捕获(C++14)
auto lambda = [value = 100](int x) { return x + value; };
std::cout << lambda(50) << "\n"; // 150
return 0;
}
核心要点:
- 捕获列表决定 Lambda 能「看到」什么
- 值捕获是副本,引用捕获是别名
- mutable 只影响值捕获的副本
- 返回类型能省略就省略,复杂逻辑再显式写
7.7 避坑指南
我总结几个自己踩过的坑,你遇到了可以少走弯路。
- 引用捕获悬空:Lambda 被存储或延迟执行时,引用捕获的变量可能已经销毁。解决方案:要么用值捕获,要么确保 Lambda 生命周期不超过变量。
- 值捕获的误解:值捕获是在 Lambda 定义时拷贝的。如果你在定义后修改了外部变量,Lambda 内部的值不会变。这其实是预期行为,但新手容易搞混。
- 隐式捕获的副作用:
[=]和[&]很方便,但会捕获所有用到的变量。代码维护时,新增一个变量可能无意中被捕获,导致性能或语义问题。我建议显式列出要捕获的变量。
嗯,Lambda 基础语法就这些。说白了就是四个部分,但每个部分都有讲究。你写多了自然就熟练了。
公众号:蓝海资料掘金营,微信deep3321