在 C++ 编程中,我们常常需要编写一些 “短小精悍” 的函数 —— 它们可能只在某个场景下被调用一次,单独定义成普通函数又显得冗余。这时,C++11 引入的 Lambda 表达式 就成了 “救星”。它允许我们在代码中直接定义 “匿名函数”,既能简化代码,又能让逻辑更紧凑。
一、Lambda 是什么?
Lambda 表达式,本质是 “可以在代码中就地定义的匿名函数”。所谓 “匿名”,是指它不需要像普通函数那样起一个固定的名字;“就地定义” 则意味着我们可以在需要调用它的地方直接写函数逻辑,无需单独声明。
举个最直观的例子:如果我们想打印一句 “Hello Lambda”,用普通函数需要写两行(声明 + 调用),而用 Lambda 只需一行:
// 普通函数
void printHello() {
std::cout << "Hello Lambda" << std::endl;
}
printHello(); // 调用
// Lambda 表达式(定义后直接调用)
[]() { std::cout << "Hello Lambda" << std::endl; }();
是不是瞬间简洁了?这就是 Lambda 最核心的价值:为 “一次性小逻辑” 提供极简的实现方式。
二、Lambda 基础语法
Lambda 的语法看起来有点 “特殊”,但只要记住固定结构,很快就能上手。完整语法如下:
[capture] (parameters) mutable -> return_type {
// 函数体(function body)
}
这 5 个部分中,只有 [capture](捕获列表)和 {函数体} 是必须的,其他部分可根据需求省略。我们逐个拆解:
部分 | 作用 | 是否省略 |
---|---|---|
[capture] | 捕获外部变量(让 Lambda 能使用函数外部的变量) | 否 |
(parameters) | 参数列表 | (和普通函数的参数列表完全一致) |
mutable | 允许修改按值捕获的变量(默认按值捕获的变量是 “只读” 的) | 是 |
-> return_type | 返回值类型(编译器可自动推导,除非函数体有多个 return 分支且类型不同) | 是 |
{} | 函数体(存放具体的逻辑代码) | 否 |
1. 最简化 Lambda:无参数、无返回值
当我们不需要参数、不需要返回值,也不需要使用外部变量时,Lambda 可以简化到极致:
// 无捕获、无参数、无返回值的 Lambda
[]() {
std::cout << "这是一个极简 Lambda" << std::endl;
}(); // 末尾加 () 表示“定义后立即调用”
甚至连 ()(参数列表)都能省略(因为没有参数):
[] { std::cout << "参数列表为空时,() 可以省" << std::endl; }();
2. 带参数的 Lambda:像普通函数一样传参
如果需要给 Lambda 传参,只需在 () 中定义参数列表,用法和普通函数完全一致。比如写一个 “两数相加” 的 Lambda:
int main() {
// 定义带参数的 Lambda,并赋值给变量 add(auto 自动推导类型)
auto add = [](int a, int b) {
return a + b; // 编译器自动推导返回值为 int
};
// 调用 Lambda(像调用普通函数一样)
int result = add(3, 5);
std::cout << "3 + 5 = " << result << std::endl; // 输出:3 + 5 = 8
}
这里用 auto 接收 Lambda 是因为它的类型是编译器内部生成的 “匿名类型”,我们无需关心具体名字。
3. 返回值类型:什么时候需要显式声明?
大多数情况下,编译器会根据 return 语句自动推导 Lambda 的返回值类型,因此 -> return_type 可以省略。但如果函数体中有 多个 return 分支,且返回值类型不同,就必须显式声明返回值类型。
比如下面的 Lambda,根据条件返回 int 或 double,必须用 -> double 指定返回值:
int main() {
// 错误写法:返回值类型不统一,编译器无法推导
// auto calculate = [](int a, bool isDouble) {
// if (isDouble) return a * 2.0; // 返回 double
// else return a * 2; // 返回 int
// };
// 正确写法:显式声明返回值为 double
auto calculate = [](int a, bool isDouble) -> double {
if (isDouble) return a * 2.0;
else return a * 2; // 自动转为 double
};
std::cout << calculate(5, true) << std::endl; // 输出:10.0
std::cout << calculate(5, false) << std::endl; // 输出:10.0
}
4. 微练手
有没有感觉很像三目运算符?好像它们具有同样的逻辑,先判断,然后返回值,那我们就用lambda实现一个:
#include <iostream>
int main() {
// 接收条件和两个Lambda,执行符合条件的逻辑
auto conditional_exec = [](bool condition, auto true_func, auto false_func) {
if (condition) {
true_func(); // 执行条件为true时的逻辑
} else {
false_func(); // 执行条件为false时的逻辑
}
};
int x = 10;
conditional_exec(
x > 5,
[]() { std::cout << "x大于5,执行逻辑A" << std::endl; },
[]() { std::cout << "x小于等于5,执行逻辑B" << std::endl; }
); // 输出:x大于5,执行逻辑A
return 0;
}
三、核心难点:
Lambda 之所以强大,关键在于它能通过 捕获列表 访问函数外部的变量。捕获列表的写法灵活,核心是控制 “如何捕获外部变量”(按值还是按引用)。
1. 捕获的 4 种常见场景
我们用一个具体例子理解:假设函数中有两个外部变量 x = 10 和 y = 20,Lambda 如何使用它们?
捕获方式 | 语法 | 作用说明 | 示例代码 |
---|---|---|---|
不捕获任何变量 | [] Lambda | 内部无法使用外部变量 | [] { std::cout << x; }(); // 错误:x 未被捕获 |
按值捕获 | [x, y] 或 [=] | 拷贝外部变量到 Lambda 内部,内部修改不会影响外部变量(默认只读) | [x, y] { std::cout << x + y; }(); // 输出 30 |
按引用捕获 | [&x, &y] 或 [&] | 引用外部变量,内部修改会直接影响外部变量 | [&x] { x++; }(); // 外部 x 变为 11 |
混合捕获 | [x, &y] | 部分变量按值捕获,部分按引用捕获 | [x, &y] { y = x + 5; }(); // 外部 y 变为 15,x 不变 |
2. 易错点:按值捕获的 “只读” 特性
按值捕获的变量,默认在 Lambda 内部是 只读 的,不能修改。如果需要修改,必须加上 mutable 关键字:
int main() {
int x = 10;
// 错误:按值捕获的 x 是只读的,不能修改
// [x] { x++; }();
// 正确:加上 mutable 允许修改按值捕获的变量(但外部 x 仍不变)
[x]() mutable {
x++;
std::cout << "Lambda 内部 x = " << x << std::endl; // 输出:11
}();
std::cout << "外部 x = " << x << std::endl; // 输出:10(外部变量未被修改)
return 0;
}
注意:mutable 只允许修改 Lambda 内部的 “拷贝版” 变量,不会影响外部原变量。如果想修改外部变量,还是得用 按引用捕获。
四、实战场景
学了基础语法,我们更关心:Lambda 在实际开发中能用到哪里?最常见的场景是 配合 C++ 标准库算法(如排序、遍历),替代繁琐的函数对象或全局函数。
场景 1:简化排序逻辑(std::sort)
std::sort 是排序函数,默认升序排序。如果想按降序、或者自定义规则排序,用 Lambda 无需单独定义比较函数:
#include <vector>
#include <algorithm>
#include <iostream>
int main() {
std::vector<int> nums = {3, 1, 4, 1, 5, 9};
// 1. 降序排序(替代单独定义比较函数)
std::sort(nums.begin(), nums.end(), [](int a, int b) {
return a > b; // 降序规则:a 大于 b 时,a 排在前面
});
// 2. 遍历输出(用 std::for_each 配合 Lambda)
std::cout << "降序排序后:";
std::for_each(nums.begin(), nums.end(), [](int num) {
std::cout << num << " "; // 输出每个元素
});
// 输出:9 5 4 3 1 1
return 0;
}
如果不用 Lambda,我们需要单独写一个比较函数,代码会多出几行:
// 不用 Lambda 的写法:单独定义比较函数
bool compareDesc(int a, int b) {
return a > b;
}
std::sort(nums.begin(), nums.end(), compareDesc);
场景 2:自定义数据结构的排序
如果要排序的是自定义结构体(如 Person),Lambda 可以轻松实现按 “年龄”“姓名长度” 等规则排序:
#include <vector>
#include <algorithm>
#include <string>
#include <iostream>
struct Person {
std::string name;
int age;
};
int main() {
std::vector<Person> people = {
{"Alice", 25},
{"Bob", 20},
{"Charlie", 30}
};
// 按年龄升序排序(年轻的在前)
std::sort(people.begin(), people.end(), [](const Person& p1, const Person& p2) {
return p1.age < p2.age;
});
// 输出排序结果
for (const auto& p : people) {
std::cout << p.name << " (" << p.age << "岁) ";
}
// 输出:Bob (20岁) Alice (25岁) Charlie (30岁)
return 0;
}
场景 3:捕获外部变量做过滤
假设我们有一个数字列表,想筛选出 “大于某个外部阈值” 的元素,用 Lambda 捕获阈值变量即可:
cpp
#include <vector>
#include <algorithm>
#include <iostream>
int main() {
std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8};
int threshold = 5; // 外部阈值:筛选大于 5 的数
// 用 Lambda 捕获 threshold,筛选符合条件的元素
std::vector<int> result;
std::for_each(nums.begin(), nums.end(), [&result, threshold](int num) {
if (num > threshold) {
result.push_back(num); // 按引用捕获 result,修改外部容器
}
});
// 输出结果
std::cout << "大于 " << threshold << " 的数:";
for (int num : result) {
std::cout << num << " "; // 输出:6 7 8
}
return 0;
}
五、避坑指南
问题 | 具体原因 | 解决办法 |
---|---|---|
忘记捕获外部变量 | 在 Lambda 中使用了外部变量,但没写捕获列表(如 [] { std::cout << x; }),编译直接报错。 | 根据需求添加捕获方式(如 [x] 或 [&x])。 |
按值捕获后想修改变量 | 按值捕获的变量默认只读,直接修改会编译错误。 | 如果只需修改内部拷贝,加 mutable;如果要修改外部变量,改用按引用捕获。 |
捕获列表过度使用 [=] 或 [&] | [=] 会捕获所有外部变量(按值),[&] 会捕获所有外部变量(按引用),可能导致不必要的性能开销或意外修改。 | 按需捕获,只捕获需要用到的变量(如 [x, &result])。 |
Lambda 不是 “花里胡哨” 的语法糖,而是能真正提升开发效率的工具。