Lambda 表达式
定义与作用
Lambda 表达式(C++11)允许在代码中内联定义匿名函数对象,语法如下:
[capture](params) mutable -> Ret { body }
| 组件 | 说明 | 是否可省略 |
|---|---|---|
[capture] | 捕获列表:声明如何访问外部变量 | 不可省略(可为空 []) |
(params) | 参数列表 | 无参时可省略 |
mutable | 允许修改值捕获的副本 | 默认不可修改 |
-> Ret | 后置返回类型 | 可推导时省略 |
{ body } | 函数体 | 不可省略 |
Lambda 的核心价值:
- 就地定义:回调逻辑写在调用点附近,可读性远高于分散的函数对象
- 上下文捕获:通过
[=]或[&]自动捕获外部变量,无需手动传递 - 零开销:闭包类型是编译器生成的匿名类,性能等同手写函数对象
核心原理
Lambda 底层实现 —— 编译器生成的闭包类型
// 用户写的 Lambda
int x = 10;
auto lambda = [x](int y) { return x + y; };
// 编译器等价生成(简化)
class __anonymous {
int x; // 捕获的副本
public:
__anonymous(int x_) : x(x_) {}
auto operator()(int y) const { return x + y; }
};
捕获方式总览
| 捕获 | 语法 | 效果 |
|---|---|---|
| 空捕获 | [] | 不访问外部变量 |
| 值捕获 | [x] | 拷贝 x |
| 引用捕获 | [&x] | 引用 x |
| 全部值捕获 | [=] | 拷贝所有使用的外部变量 |
| 全部引用捕获 | [&] | 引用所有使用的外部变量 |
| 混合捕获 | [=, &x] / [&, x] | 默认全部值/引用,特定变量例外 |
| this 捕获 | [this] | 捕获当前对象指针 |
完整示例
示例一:Lambda 在 STL 算法中的应用
场景说明:学生成绩管理系统,用 Lambda 替代传统函数对象完成筛选和排序。
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <numeric>
struct Student {
std::string name;
int score;
std::string major;
};
int main() {
std::vector<Student> students = {
{"张三", 92, "计算机科学"},
{"李四", 78, "数学"},
{"王五", 85, "计算机科学"},
{"赵六", 88, "物理"},
{"钱七", 95, "计算机科学"},
{"孙八", 60, "数学"}
};
std::cout << "===== 原始成绩单 =====" << std::endl;
for (const auto& s : students) {
std::cout << s.name << " | " << s.score << " | " << s.major << std::endl;
}
// ---- 1. 筛选:计算机科学专业且成绩 > 80 ----
std::vector<Student> cs_high;
std::copy_if(students.begin(), students.end(), std::back_inserter(cs_high),
[](const Student& s) {
return s.major == "计算机科学" && s.score > 80;
});
std::cout << "\n===== 计算机科学,成绩 > 80 =====" << std::endl;
for (const auto& s : cs_high) {
std::cout << s.name << " | " << s.score << std::endl;
}
// ---- 2. 排序:先按专业,再按成绩降序 ----
std::sort(students.begin(), students.end(),
[](const Student& a, const Student& b) {
if (a.major != b.major) return a.major < b.major;
return a.score > b.score;
});
std::cout << "\n===== 按专业 + 成绩排序 =====" << std::endl;
for (const auto& s : students) {
std::cout << s.major << " | " << s.name << " | " << s.score << std::endl;
}
// ---- 3. 带上下文:统计某个专业的平均分 ----
std::string target_major = "计算机科学";
auto count = std::count_if(students.begin(), students.end(),
[&target_major](const Student& s) {
return s.major == target_major;
});
auto sum = std::accumulate(students.begin(), students.end(), 0,
[&target_major](int acc, const Student& s) {
return acc + (s.major == target_major ? s.score : 0);
});
std::cout << "\n" << target_major << " 专业:" << std::endl;
std::cout << " 人数: " << count << std::endl;
std::cout << " 平均分: " << (count > 0 ? sum / count : 0) << std::endl;
return 0;
}
预期输出:
===== 原始成绩单 =====
张三 | 92 | 计算机科学
李四 | 78 | 数学
王五 | 85 | 计算机科学
赵六 | 88 | 物理
钱七 | 95 | 计算机科学
孙八 | 60 | 数学
===== 计算机科学,成绩 > 80 =====
张三 | 92
王五 | 85
钱七 | 95
===== 按专业 + 成绩排序 =====
数学 | 李四 | 78
数学 | 孙八 | 60
物理 | 赵六 | 88
计算机科学 | 钱七 | 95
计算机科学 | 张三 | 92
计算机科学 | 王五 | 85
计算机科学 专业:
人数: 3
平均分: 90
逐段分析:
std::copy_if中的 Lambda 是纯函数,不需要捕获任何外部变量std::sort中的 Lambda 实现多级排序——先比较major,相等时比较scorestd::count_if和std::accumulate中的 Lambda 引用捕获target_major——这是 Lambda 的核心优势:回调可以访问局部上下文[&target_major]只引用捕获需要的变量,而非[&]全部捕获——最佳实践
示例二:捕获方式深度对比
场景说明:逐一演示各种捕获方式的行为差异和生命周期陷阱。
#include <iostream>
#include <functional>
#include <memory>
// 计算器工厂:返回不同的计算 Lambda
auto make_adder(int x) {
// 值捕获 x —— 安全,x 的副本存在于闭包中
return [x](int y) { return x + y; };
}
auto make_counter() {
// 值捕获 counter 的副本 + mutable 使其可修改
int counter = 0;
return [counter]() mutable {
return ++counter;
};
}
// 危险:返回引用捕获局部变量的 Lambda
std::function<int()> make_dangerous_counter() {
int counter = 0;
return [&counter]() { return ++counter; }; // 悬挂引用!
}
int main() {
std::cout << "===== 值捕获 =====" << std::endl;
auto add5 = make_adder(5);
std::cout << "add5(10) = " << add5(10) << std::endl;
std::cout << "\n===== mutable 值捕获 =====" << std::endl;
auto cnt = make_counter();
std::cout << "第 1 次: " << cnt() << std::endl;
std::cout << "第 2 次: " << cnt() << std::endl;
std::cout << "第 3 次: " << cnt() << std::endl;
// cnt 内部维护了独立的 counter 副本
std::cout << "\n===== 引用捕获的陷阱 =====" << std::endl;
// 在同一个作用域内,引用捕获是安全的
int local = 100;
auto ref_lambda = [&local]() { return local; };
std::cout << "ref_lambda: " << ref_lambda() << std::endl;
local = 200;
std::cout << "修改后 ref_lambda: " << ref_lambda() << " ← 反映了变化" << std::endl;
// 但离开作用域后引用捕获会悬空
auto dangerous = make_dangerous_counter();
// std::cout << dangerous() << std::endl; // 未定义行为!
std::cout << "\n===== 混合捕获 =====" << std::endl;
int a = 10, b = 20, c = 30;
// 默认值捕获,但 b 用引用捕获
auto mixed = [=, &b]() {
// a 和 c 是副本,b 是引用
// a++; // 编译错误:默认值捕获 + 非 mutable = 不可修改
return a + b + c;
};
std::cout << "a + b + c = " << mixed() << std::endl;
b = 999;
std::cout << "修改 b 后: " << mixed() << " ← b 是引用" << std::endl;
return 0;
}
预期输出:
===== 值捕获 =====
add5(10) = 15
===== mutable 值捕获 =====
第 1 次: 1
第 2 次: 2
第 3 次: 3
===== 引用捕获的陷阱 =====
ref_lambda: 100
修改后 ref_lambda: 200 ← 反映了变化
===== 混合捕获 =====
a + b + c = 60
修改 b 后: a + b + c = 1039 ← b 是引用
逐段分析:
make_adder值捕获参数x——返回的 Lambda 持有x的独立副本,生命周期安全make_counter值捕获counter+mutable——每次调用修改自己的副本(独立的计数器)make_dangerous_counter引用捕获局部变量——函数返回后局部变量被销毁,调用 Lambda 是 UB- 同作用域内的引用捕获是安全的,因为它不跨越局部变量的生命周期
- 混合捕获
[=, &b]:a和c是副本不可修改(非 mutable),b是引用可反映外部变化
示例三:泛型 Lambda(C++14)
#include <iostream>
#include <string>
#include <vector>
int main() {
// C++14 泛型 Lambda:参数类型用 auto
auto generic_add = [](auto a, auto b) { return a + b; };
std::cout << "int + int: " << generic_add(3, 4) << std::endl;
std::cout << "double + int: " << generic_add(3.14, 2) << std::endl;
std::cout << "string: "
<< generic_add(std::string("Hello, "), "World!") << std::endl;
// 泛型 Lambda 配合 vector 数据累加
std::vector<int> values = {1, 2, 3, 4, 5};
auto sum = 0;
auto fold = [&sum](const auto& container) {
for (const auto& item : container) {
sum += item;
}
};
fold(values);
std::cout << "累加结果: " << sum << std::endl;
return 0;
}
易错场景与面试考点
易错场景
| 场景 | 错误表现 | 正确做法 |
|---|---|---|
| 引用捕获的变量已销毁(悬挂引用) | 未定义行为 | Lambda 生命周期不超出所捕获引用的变量作用域 |
默认值捕获的 this 指向已销毁对象 | 未定义行为 | 使用 shared_from_this 或拷贝所需成员 |
默认值捕获 = 无法捕获静态变量 | 行为与预期不符 | 静态变量不需要捕获,可直接访问 |
mutable 值捕获的修改在多次调用间累积 | 意外状态 | 理解 mutable 使闭包具有状态 |
不同 Lambda 类型赋值给 auto vs std::function | 类型不同 | auto 保留具体类型(零开销),std::function 类型擦除(有开销) |
常见面试问题
Lambda 表达式的底层实现原理是什么?——编译器生成一个匿名的函数对象类,捕获的变量成为其成员变量,
operator()执行 Lambda 体。本质是语法糖,无运行时开销。[=]和[&]分别捕获什么?各自的陷阱?——[=]值捕获所有使用的外部变量,生成副本;[&]引用捕获所有。[=]无法捕获静态变量和全局变量,[&]有悬挂引用风险。mutable关键字在 Lambda 中的作用?——允许修改值捕获的副本变量(默认operator()是const的)。不写mutable则值捕获的副本不可修改。Lambda 可以转换为函数指针吗?什么条件?——无捕获(
[])的 Lambda 可以隐式转换为函数指针。有捕获的 Lambda 不能。auto lambda = ...vsstd::function的区别?——auto保留闭包的具体类型,零开销;std::function使用类型擦除,有间接调用和堆分配开销。优先用auto。泛型 Lambda(C++14)的实现原理?——
[](auto x){}的operator()是一个模板成员函数,auto被替换为模板参数。
小结
- Lambda = 编译器生成的匿名函数对象,零开销抽象
- 优先值捕获(
[x]/[=])保证生命周期安全 - 引用捕获(
[&x]/[&])仅在 Lambda 生命周期短于被捕获变量时使用 mutable允许修改值捕获副本,使闭包成为有状态对象- 泛型 Lambda(C++14)通过
auto参数实现模板化 - 用
auto存储 Lambda(零开销),而非std::function(有类型擦除开销)