【C++ 面试真题】聊聊 C++ 中的 lambda 表达式
lambda 是 C++11 引入的"明星特性"------写起来爽、用起来多,但面试真考起来,很多人只说得出"匿名函数",却答不清"它到底是个什么类型、捕获列表干了什么、值捕获和引用捕获有啥区别"。本文用问答的方式,把 lambda 一次讲透。
一、先说结论:lambda 是个匿名的仿函数
❓ lambda 表达式到底是什么?
✅ 一句话:lambda 本质是一个"匿名的仿函数(函数对象)" ------编译器会为每个 lambda 生成一个独一无二的类,重载了 operator(),让你能像调用函数一样用它。
cpp
// 写一个 lambda
auto add = [](int a, int b) {
return a + b;
};
// 调用它
std::cout << add(2, 3); // 5
这段代码,编译器大致会翻译成这样:
cpp
// 编译器偷偷生成的类
struct __uniq_lambda {
int operator()(int a, int b) const {
return a + b;
}
};
__uniq_lambda add;
// add(2,3) 实际调 operator()
💡 核心认知 :lambda 不是"语法糖函数",而是"语法糖类 "。每个 lambda 都对应一个独一无二的、编译器生成的类型,连名字你都不知道(所以用 auto 接收)。理解了这一点,后面所有问题都好答了。
这也解释了一个现象:两个看起来"一样"的 lambda,类型却完全不同 ,不能互相赋值。比如
auto f1 = [](){}和auto f2 = [](){},f1 和 f2 是两个不同类型------哪怕函数体一模一样。因为编译器给每个 lambda 都生成了独立的类。唯一能统一存储它们的方式,就是用 std::function 做类型擦除。
二、lambda 的语法拆解
❓ lambda 的语法由哪几部分组成?
✅ 一个完整的 lambda 有 5 个部分:
cpp
[捕获] (参数) mutable -> 返回类型 { 函数体 }
cpp
int x = 10;
// 值捕获 x,参数 int,返回 int
auto f = [x](int n) -> int {
return n + x;
};
逐个看:
[捕获]------ 决定 lambda 内部怎么"看见"外部变量(最关键,见后文);(参数)------ 跟普通函数一样,可省略;mutable------ 允许修改值捕获的副本(默认不能改);-> 返回类型------ 可省略,编译器自动推导;{函数体}------ 实际执行的代码。
绝大多数场景只用 [捕获](参数){函数体} 三件套就够了。
三、捕获列表:lambda 的灵魂
❓ 捕获列表 [ ] 是干什么的?有哪些写法?
✅ 捕获列表决定 lambda 怎么使用外部变量,是 lambda 区别于普通函数的核心。主要写法:
| 写法 | 含义 |
|---|---|
[] |
不捕获 |
[=] |
按值捕获所有用到的变量 |
[&] |
按引用捕获所有用到的变量 |
[x] |
按值捕获 x |
[&x] |
按引用捕获 x |
[=, &x] |
默认按值,x 例外按引用 |
[this] |
捕获 this 指针(成员函数里用) |
cpp
int a = 1, b = 2;
// 按值捕获 a、b
auto f1 = [=]() { return a + b; };
// 按引用捕获
auto f2 = [&]() { a = 10; b = 20; };
// 混合:默认值,a 例外引用
auto f3 = [=, &a]() { a = 5; return b; };
⚠️ 高频考点(值捕获 vs 引用捕获):
- 值捕获
[=]:拷贝一份当时的值,之后外部变量改了,lambda 里还是旧值;- 引用捕获
[&]:只存引用,lambda 调用时读到的是最新值。
cpp
int n = 10;
auto byVal = [n]() { return n; };
auto byRef = [&n]() { return n; };
n = 99;
std::cout << byVal(); // 10(旧值)
std::cout << byRef(); // 99(新值)
四、值捕获为什么默认不能改?mutable 是干嘛的?
❓ 值捕获的变量在 lambda 里能改吗?
✅ 默认不能改 。因为编译器生成的 operator() 是 const 的,值捕获的副本是只读的。想改?加 mutable:
cpp
int n = 10;
// 默认 const,改不了副本
auto f1 = [n]() {
// n = 5; // ❌ 编译错
return n;
};
// mutable:去掉 const,能改副本
auto f2 = [n]() mutable {
n = 5; // ✅ 改的是副本
return n;
};
std::cout << f2(); // 5
// 注意:外部 n 仍是 10
💡 加分点 :mutable 改的是lambda 内部的副本 ,不影响外部原变量 。这跟"引用捕获"完全不同------引用捕获改的是外部变量本身。所以"
[n] mutable改 n"和 "[&n]改 n",效果天差地别。
五、lambda 是什么类型?怎么传参和存储?
❓ lambda 的类型是什么?能放进容器或当函数参数吗?
✅ lambda 的类型是编译器生成的、独一无二的匿名类 ,没有统一的名字。所以只能用 auto 接收 ,或用 std::function 包装。
cpp
// 用 auto:零开销,直接持有
auto f1 = [](int x) { return x * 2; };
// 用 std::function:能存任意可调用对象
// 但有类型擦除开销
std::function<int(int)> f2 =
[](int x) { return x * 2; };
当函数参数 时,最佳实践是用模板,避免 std::function 的开销:
cpp
// 推荐:模板参数,零开销
template<typename F>
void run(F f) { std::cout << f(5); }
// 也能用 std::function(有开销)
void run2(std::function<int(int)> f) {
std::cout << f(5);
}
run([](int x) { return x + 1; });
🎯 性能建议:
- 能用模板/auto 就别用 std::function------后者有类型擦除和可能的堆分配开销;
- 只有需要"存储一堆异构 lambda"(如事件回调容器)时,才用 std::function。
STL 算法(
std::sort、std::find_if等)的回调参数都是模板,传 lambda 是零开销内联的,比传函数指针还快。
六、捕获的陷阱:悬空引用
❓ 用 lambda 最容易踩什么坑?
✅ 头号陷阱:引用捕获的悬空引用。lambda 比外部变量活得长时,引用捕获就成了"野引用":
cpp
std::function<int()> make() {
int local = 42;
// ⚠️ 引用捕获 local
return [&local]() { return local; };
// 函数返回,local 销毁
// lambda 持有的是悬空引用!
}
auto f = make();
std::cout << f(); // ❌ 未定义行为
local 是局部变量,函数返回即销毁,但 lambda 还持有它的引用------调用就是未定义行为。
⚠️ 规则 :引用捕获
[&]只能在 lambda 的生命周期不超过被捕获变量时用 。比如在同一个函数作用域内用std::sort的回调。一旦 lambda 要"活得更久"(存储、异步、回调),优先值捕获[=],或显式捕获智能指针/共享状态。一个实战心法:凡是 lambda 要"离开当前作用域"的(返回它、存进成员、丢给异步队列),立刻警觉引用捕获。问自己一句"它捕获的东西,在 lambda 被调用时还活着吗?"答不上来就别用引用捕获。这个习惯能帮你挡掉绝大多数悬空引用的 bug------这类 bug 在异步、回调、事件系统里特别高发,而且一旦发生极难排查。
七、核心规则速查表
| 维度 | 规则 |
|---|---|
| 本质 | 匿名仿函数(编译器生成的类) |
| 类型 | 独一无二的匿名类型,用 auto 接收 |
值捕获 [=] |
拷贝当时值,之后外部改动不影响 |
引用捕获 [&] |
存引用,读最新值,注意悬空 |
| mutable | 允许改值捕获的副本(不改原变量) |
| 存储/传参 | auto/模板优先,std::function 兜底 |
八、面试高频追问
❓ Q1:lambda 和普通函数有什么区别?
✅ lambda 能捕获外部变量,普通函数不行;lambda 是匿名类型(用 auto 接收),普通函数有名字和明确类型。本质上 lambda 是个类,普通函数就是函数。
❓ Q2:lambda 的 operator() 默认是 const 的吗?
✅ 是的。所以值捕获的变量默认只读,想改要加 mutable。mutable 去掉 const,但只改 lambda 内部的副本,不影响原变量。
❓ Q3:std::function 和 auto 接收 lambda 有什么区别?
✅ auto 直接持有 lambda 的真实类型,零开销 ;std::function 做类型擦除统一存储,有开销(可能堆分配)。需要存储异构可调用对象时用 std::function,否则用 auto/模板。
❓ Q4:[=] 和 [&] 该用哪个?
✅ 看生命周期。同作用域内短期用 ,[&] 方便;lambda 要活得比当前作用域久 (回调、异步、存储),用 [=] 或显式捕获,避免悬空引用。最安全的习惯是显式列出要捕获的变量 ([x, &y]),而不是全盘 [=]/[&]。
❓ Q5:lambda 能递归吗?
✅ 能,但必须用 std::function 或显式声明类型接收,不能用 auto------因为 auto 推导时 lambda 还没定义完,无法自引用。常见写法:
cpp
std::function<int(int)> fact =
[&](int n) -> int {
return n <= 1 ? 1 : n * fact(n-1);
};
九、总结速查表
| 场景 | 推荐写法 |
|---|---|
| 一般匿名函数 | auto f = [](args){...} |
| 需要读外部变量 | 值捕获 [x] |
| 需要改外部变量 | 引用捕获 [&x] |
| 要改值捕获的副本 | 加 mutable |
| 传给 STL 算法 | 直接传 lambda,零开销 |
| 存储异构回调 | 用 std::function |
一句话回顾
lambda 的本质是匿名的仿函数------记住"值捕获拷贝快照、引用捕获读最新、mutable 只改副本、auto 优先于 std::function",再加一句"引用捕获当心悬空",这题就稳了。
如果您觉得本篇内容对你有帮助,欢迎点赞 👍、收藏 ⭐、转发 📢。面向对象篇到此完结,下期我们进入标准库篇,聊聊 vector 的扩容机制,敬请关注 👋