在学习 C++ 和 Qt 的过程中,经常会看到这样的代码:
auto bitsum = [](int x) -> int { int res = 0; while (x) { res += x % 10; x /= 10; } return res; };或者在 Qt 的connect()中看到:
connect(button, &QPushButton::clicked, this, [sender]() { sender->sendData(); });刚开始看到[]、[sender]很容易疑惑。
其实这两段代码使用的都是同一个 C++ 特性:Lambda 表达式。
本文从 Lambda 的基本语法开始,重点介绍最容易混淆的捕获列表。
一、什么是 Lambda 表达式?
Lambda 可以简单理解为:
在代码中直接定义一个匿名函数。
例如普通函数:
int add(int a, int b) { return a + b; }如果这个函数只使用一次,其实没有必要专门给它起一个函数名。
可以直接写成 Lambda:
auto add = [](int a, int b) -> int { return a + b; }; cout << add(3, 5);输出:
8这里的add是一个变量,它保存了这个 Lambda。
因此可以通过:
add(3, 5);调用它。
二、Lambda 的基本语法
Lambda 的常见完整形式是:
[capture](parameters) -> return_type { // 函数体 };例如:
auto add = [](int a, int b) -> int { return a + b; };可以拆成:
[] (int a, int b) -> int │ │ │ 捕获列表 参数列表 返回值类型最后:
{ return a + b; }是 Lambda 的函数体。
三、[]是什么?
Lambda 最前面的[]叫做:
捕获列表(Capture List)
这是 Lambda 与普通函数一个非常重要的区别。
捕获列表主要解决一个问题:
Lambda 如果需要使用外部作用域中的局部变量,该怎么办?
例如:
void test() { int x = 10; auto func = []() { cout << x; }; }这段代码会出错。
因为x是test()中的局部变量,而 Lambda 默认不能直接访问这个外部局部变量。
可以通过捕获列表解决:
void test() { int x = 10; auto func = [x]() { cout << x; }; }这里:
[x]表示:
把外面的
x捕获到 Lambda 中。
四、[x]:按值捕获
最基本的捕获方式是:
[x]表示按照值捕获x。
例如:
int x = 10; auto func = [x]() { cout << x; }; func();输出:
10可以把它简单理解成 Lambda 保存了一份x的副本。
因此:
int x = 10; auto func = [x]() { cout << x; }; x = 20; func();输出仍然是:
10因为 Lambda 中保存的是捕获时的x副本。
五、[&x]:按引用捕获
如果写成:
[&x]则表示:
按引用捕获
x。
例如:
int x = 10; auto func = [&x]() { cout << x; }; x = 20; func();输出:
20因为 Lambda 访问的是外部原来的x。
甚至可以直接修改它:
int x = 10; auto func = [&x]() { x = 100; }; func(); cout << x;输出:
100所以可以简单记忆:
[x] → 按值捕获 [&x] → 按引用捕获六、[]:不捕获任何外部变量
例如:
auto func = []() { cout << "Hello"; };这里:
[]表示:
不捕获任何外部局部变量。
前面 LeetCode 中的数位和代码就是这种情况:
auto bitsum = [](int x) -> int { int res = 0; while (x) { res += x % 10; x /= 10; } return res; };这里的x并不是外部变量。
它来自:
(int x)也就是说:
x → Lambda 的参数而不是:
x → 外部变量所以根本不需要捕获。
七、[=]:默认按值捕获
如果外面有多个变量:
int a = 10; int b = 20; int c = 30;可以写:
auto func = [=]() { cout << a << endl; cout << b << endl; cout << c << endl; };[=]表示:
Lambda 中使用到的外部局部变量,默认按照值捕获。
这样就不需要写:
[a, b, c]八、[&]:默认按引用捕获
同理:
auto func = [&]() { a++; b++; c++; };表示:
Lambda 中使用到的外部局部变量,默认按照引用捕获。
因此:
[=] → 默认按值捕获 [&] → 默认按引用捕获这是非常常用的两种写法。
九、捕获列表可以混合使用
捕获列表并不只能全部按值或者全部按引用。
例如:
int a = 10; int b = 20; auto func = [a, &b]() { cout << a << endl; b++; };这里:
[a, &b]表示:
a → 按值捕获 b → 按引用捕获也可以使用默认捕获方式,再单独指定某个变量。
例如:
[=, &b]表示:
默认按值捕获,但是
b按引用捕获。
而:
[&, a]表示:
默认按引用捕获,但是
a按值捕获。
十、Lambda 中的“参数”和“捕获”不要混淆
这是学习 Lambda 时非常容易混淆的地方。
例如:
auto func = [x](int y) -> int { return x + y; };这里有三个不同的部分:
[x] → 捕获列表 (int y) → 参数列表 -> int → 返回值类型其中:
x是外部变量,通过捕获列表进入 Lambda。
而:
y是调用 Lambda 时传入的参数。
例如:
int x = 10; auto func = [x](int y) -> int { return x + y; }; cout << func(5);结果:
15所以:
捕获列表解决的是“Lambda 如何使用外部变量”,参数列表解决的是“调用 Lambda 时如何传入参数”。
十一、Lambda 在 Qtconnect()中的使用
Lambda 在 Qt 中非常常见,尤其是connect()。
例如:
connect(button, &QPushButton::clicked, this, []() { qDebug() << "按钮被点击"; });这里第四个参数就是一个 Lambda:
[]() { qDebug() << "按钮被点击"; }因为这个 Lambda 不需要使用外部局部变量,所以捕获列表为空:
[]十二、Qt 中为什么会出现[sender]?
再看一个实际例子:
Sender *sender = new Sender; connect(button, &QPushButton::clicked, this, [sender]() { sender->sendData(); });这里:
Sender *sender = new Sender;定义了一个局部变量:
sender而 Lambda 中需要使用这个变量:
sender->sendData();因此需要在捕获列表中写:
[sender]表示:
按值捕获外部的
sender变量。
这里尤其要注意:
Sender *sender中的sender是一个指针变量。
所以:
[sender]捕获的是这个指针变量,而不是Sender类型。
可以简单理解为:
外部: sender ─────────→ Sender对象 │ │ 按值捕获 ↓ Lambda: sender ─────────→ 同一个Sender对象指针本身复制了一份,但是两个指针仍然指向同一个对象。
十三、[sender]和[&sender]的区别
假设:
Sender *sender = new Sender;那么:
[sender]表示:
按值捕获指针变量。
而:
[&sender]表示:
按引用捕获指针变量。
也就是说,两者的区别是:
[sender] ↓ 复制一份指针 [&sender] ↓ 引用原来的指针变量在 Qt 的很多场景中,如果只是需要 Lambda 使用这个指针,常见写法就是:
[sender]十四、为什么 Lambda 在 Qt 中特别常见?
因为 Qt 的connect()可以直接接收 Lambda。
例如:
connect(button, &QPushButton::clicked, this, [this]() { ui->label->setText("Hello"); });这里:
[this]也是一种捕获。
它表示:
捕获当前对象的
this指针。
于是 Lambda 中可以使用:
ui->label等当前对象的成员。
因此你在 Qt 项目中看到:
[this]()[sender]()[&data]()本质上都是在使用 Lambda 的捕获列表。
十五、Lambda 常见写法总结
可以先记住下面这张表:
| 写法 | 含义 |
|---|---|
[] | 不捕获外部变量 |
[x] | 按值捕获x |
[&x] | 按引用捕获x |
[=] | 外部变量默认按值捕获 |
[&] | 外部变量默认按引用捕获 |
[a, &b] | a按值,b按引用 |
[=, &b] | 默认按值,但b按引用 |
[&, a] | 默认按引用,但a按值 |
十六、回到最开始的 LeetCode 代码
现在再看这段代码:
auto bitsum = [](int x) -> int { int res = 0; while (x) { res += x % 10; x /= 10; } return res; };就很好理解了。
auto bitsum ↓ 用一个变量保存 Lambda [] ↓ 不捕获外部变量 (int x) ↓ Lambda 的参数 -> int ↓ 返回 int { ... } ↓ Lambda 函数体而 Qt 中:
[sender]() { sender->sendData(); }则是:
[sender] ↓ 按值捕获外部的 sender () ↓ 没有参数 { sender->sendData(); } ↓ 使用捕获进来的 sender因此,理解 Lambda 最重要的就是先分清楚这三个部分:
[capture](parameters) -> return_type其中:
捕获列表决定 Lambda 能不能使用外部变量,以及如何捕获这些变量;参数列表决定调用 Lambda 时接收什么参数;返回值类型决定 Lambda 返回什么类型。
掌握这几个基本概念后,再看 C++ STL、Qtconnect()中的大量 Lambda 写法,就不会再觉得[]、[this]、[sender]很神秘了。