拒绝死记硬背!一文快速上手Lambda匿名函数与 std::function
文章目录
第一讲:为什么要引入 Lambda?(引子与初探)
1. 前提回顾:传统的“外包模式”
在 C++ 中,如果我们想执行一段逻辑,通常需要定义一个普通函数。
打个比方:你是一个工厂的老板(main 函数),你需要完成一项任务:“对产品进行打包”。
传统做法是:你必须在工厂外面专门成立一个“打包部门”(在 main 函数外定义一个新函数),然后再在工厂里呼叫这个部门。
// 传统做法:在 main 函数外部定义
void PackageProduct() {
// 打包逻辑
}
int main() {
PackageProduct(); // 老板呼叫打包部门
return 0;
}
痛点在哪里?
如果这个“打包”任务非常简单,而且全公司只有今天、只有在这个角落会用一次。为了这一次极简的任务,专门去外面挂牌成立一个“打包部门”,不仅名字难起,而且老板看代码的时候,还得把目光跳到工厂外面去看这个部门到底在干嘛,非常打断思路。
2. 引入 Lambda:随用随贴的“便利贴”
为了解决上面“大炮打蚊子”的痛点,C++11 引入了 Lambda 表达式。
什么是 Lambda? 简单来说,它就是一个没有名字的、写在函数内部的临时小函数。
它就像是一张“便利贴”。老板不需要去外面成立新部门,只需要在流水线旁边贴一张便利贴,上面写着“把这个盒子封上”,工人看到便利贴立刻就执行了,用完即阅,不占编制(不需要起名字)。
字符画演示:传统函数 vs Lambda
【传统函数:部门外包】 【Lambda:便利贴】
+-------------------+ +-------------------+
| 全局空间 (厂区外) | | main函数 (厂区内) |
| | | |
| void Package() { | | // 直接贴便利贴 |
| 封箱; | | auto task = |
| } |<---跳跃---> | [](){ 封箱; }; |
+-------------------+ | | |
| | task(); // 执行 |
+-------------------+ | +-------------------+
| main函数 (厂区内) | | (逻辑紧凑,目光无需跳跃)
| | |
| Package(); -------+
+-------------------+
3. Lambda 的最简骨架
让我们来看看这张“便利贴”最基本的写法。你只需要记住三个符号:[]、()、{}。
[] () {
// 这里写你要做的事情
}
[]:起手式。告诉编译器:“注意啦,我接下来要写一张便利贴(Lambda)了!”(我们称之为捕获列表,后面会深挖)。():接收材料。如果完成这个任务需要外界提供材料,就写在这里(这和普通函数的参数列表一模一样)。{}:任务清单。具体的执行逻辑(函数体)。
4. 代码 Demo 演示
让我们用 C++ 代码看看传统函数和 Lambda 在执行一个简单打印任务时的区别:
#include <iostream>
// 传统方式:写在外面
void SayHello() {
std::cout << "Hello from traditional function!" << std::endl;
}
int main() {
// 1. 调用传统函数
SayHello();
// 2. 使用 Lambda 表达式(便利贴)
// 我们把这张便利贴赋值给了一个变量叫 myLambda,方便调用它
auto myLambda = []() {
std::cout << "Hello from Lambda!" << std::endl;
};
// 执行这张便利贴
myLambda();
return 0;
}
注意:我们用 auto 来接收 Lambda。因为每个 Lambda 在编译器眼里都是独一无二的奇怪类型,你无法直接写出它的真实类型名字,所以统一用 auto 让编译器自己去推导。
第一讲 课后练习
题目:
请在一个虚构的 main 函数中,使用 Lambda 表达式写一张“便利贴”,这张便利贴不需要任何外界传入参数,执行的任务是向屏幕输出一句 "I am learning Lambda!"。请将这个 Lambda 赋值给一个变量 studyTask,并紧接着调用执行它。
(请先在脑海中或纸上写出你的答案,然后再看下方的解析)
详细解析与答案
解析思路:
- 首先,我们要定义一个变量来装这半张“便利贴”,由于 Lambda 类型未知,我们要使用
auto关键字:auto studyTask = - 接着,写出 Lambda 的起手式,告诉编译器这是个 Lambda:
[] - 题目要求不需要外界传入参数,所以参数列表为空:
() - 题目要求的任务是打印字符串,所以大括号内写入代码:
{ std::cout << "I am learning Lambda!" << std::endl; } - Lambda 定义完成后,不要忘记末尾需要分号
;结束这个赋值语句。 - 最后,像调用普通函数一样,加上括号调用它:
studyTask();
标准答案代码:
#include <iostream>
int main() {
// 定义 Lambda 并赋值给 studyTask
auto studyTask = []() {
std::cout << "I am learning Lambda!" << std::endl;
};
// 执行它
studyTask();
return 0;
}
上一讲我们了解了 Lambda 就像是一张随用随贴的“便利贴”,解决了为了极小任务而去外面大张旗鼓成立“外包部门”(定义全局函数)的痛点。
今天我们要把这张便利贴放到显微镜下,看看它的完整解剖图,并且重点解决一个问题:如果执行这个任务需要外界提供“原材料”(参数),做完后还需要“交付成品”(返回值),该怎么写?
第二讲:Lambda 的解剖学与参数传递
1. 显微镜下的 Lambda(完整形态)
在第一讲中,我们看到的 [](){} 只是 Lambda 的“素颜”最简形态。实际上,Lambda 的全貌包含五个部分。
字符画演示:Lambda 完整解剖图
[捕获列表] (参数列表) mutable -> 返回类型 { 函数体 }
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
【起手式】 【原材料】 【特殊权限】【成品标签】 【执行清单】
(第三讲核心) (本讲重点) (暂不深入) (本讲重点) (具体逻辑)
不要被这个长长的公式吓到。除了起手式 [] 和执行清单 {} 是必须的,其他部分在很多时候都可以省略。我们今天聚焦于 () 和 ->。
2. 传入原材料:参数列表 ()
生活比喻:
假设老板写了一张便利贴:“把这两个零件组装起来”。
工人拿到便利贴后会问:“老板,哪两个零件?”
这就意味着,这张便利贴在执行时,需要外界临时递给它“原材料”。这个用来接收原材料的托盘,就是 ()。
它的用法和普通函数一模一样,需要在括号里定义好变量的类型和名字。
代码片段对比:
// 普通函数接收材料
void Assemble(int partA, int partB) { ... }
// Lambda 接收材料
auto assembleTask = [](int partA, int partB) { ... };
3. 交付成品:返回类型 ->
生活比喻:
如果老板不仅要求工人组装零件,还要求工人把组装好的成品交回给老板。
在 Lambda 中,我们用 ->(一个箭头)加上类型名字,来贴上“成品标签”,告诉老板我会交还什么类型的东西。这叫做尾置返回类型。
不过,C++ 编译器非常聪明!
在绝大多数情况下,编译器会“察言观色”。如果你的大括号 {} 里写了 return a + b;,编译器知道 a 和 b 都是整数,它就会自动推断出返回值也是整数。所以,-> 返回类型 这一部分通常是可以省略不写的! 除非逻辑特别复杂,编译器猜不出来,才需要你手动写上。
4. Code Demo 演示:带参数与返回值的 Lambda
让我们用一个“计算器”的例子,把参数和返回值结合起来:
#include <iostream>
int main() {
// 场景:老板需要一个能计算两个整数相加的便利贴任务
// 完整写法(包含 -> 返回类型)
// 接收两个 int,明确告诉外界我会返回一个 int
auto addStrict = [](int a, int b) -> int {
return a + b;
};
// 常用省略写法(省略 -> 返回类型)
// 接收两个 int,编译器看到 return a + b,自动猜出返回值是 int
auto addSmart = [](int a, int b) {
return a + b;
};
// 执行任务并拿到成品
int result1 = addStrict(10, 20);
int result2 = addSmart(100, 200);
std::cout << "严格写法的计算结果: " << result1 << std::endl;
std::cout << "省略写法的计算结果: " << result2 << std::endl;
return 0;
}
逻辑链梳理:
当你写下 [](int a, int b) { return a + b; } 时,你定义了一个规则:“只要你给我两个 int,我就还你它们的和”。然后你把它存在了 addSmart 这个名字下,随时可以像普通函数一样调用 addSmart(100, 200)。
第二讲 课后练习
题目:
请在 main 函数中编写一个 Lambda 表达式,将其赋值给变量 calculateArea。
任务要求:
- 该 Lambda 接收两个
double类型的参数:length(长)和width(宽)。 - 函数体内的逻辑是计算长方形的面积(长乘宽),并将结果返回。
- 请省略
->尾置返回类型,让编译器自动推导。 - 调用这个 Lambda,传入长为
5.5,宽为2.0,并将结果存储在一个变量中输出到屏幕上。
(请反复推敲代码逻辑,先自行构思后再查看解析)
详细解析与答案
解析思路:
- 我们需要一个变量接收 Lambda:
auto calculateArea = - 写起手式和参数列表(接收原材料):
[](double length, double width) - 题目要求省略
->返回类型,所以直接写大括号(执行清单):{ return length * width; } - 不要忘记末尾的分号。
- 调用时,就像调用函数一样传入数据,并用一个
double类型的变量接收成品:double area = calculateArea(5.5, 2.0); - 最后打印输出。
标准答案代码:
#include <iostream>
int main() {
// 定义带参数且自动推导返回值的 Lambda
auto calculateArea = [](double length, double width) {
return length * width;
};
// 传入原材料 5.5 和 2.0,接收成品
double area = calculateArea(5.5, 2.0);
// 输出结果
std::cout << "长方形的面积是: " << area << std::endl;
return 0;
}
上一讲我们学习了 (),知道了如何给 Lambda 这张“便利贴”传递临时的“原材料”。
但现实开发中,我们往往会遇到一种更常见的情况:需要的材料已经摆在老板的桌子上了(在 main 函数里已经定义好的变量),工人(Lambda)能不能直接拿来用呢?
这就是整个 Lambda 表达式中最核心、也是最神奇的部分——捕获列表 [] 的奥秘。今天我们必须把它彻底讲透。
第三讲:捕获列表的奥秘(深入 [])
1. 为什么需要捕获列表?(打破次元壁)
在 C++ 的规矩里,函数和函数之间是有“次元壁”的。
通常情况下,你在 main 函数里定义了一个变量 int apple = 5;,如果把任务外包给外面的普通函数,那个函数是绝对看不见这个 apple 的。
但是 Lambda 不一样,它就贴在 main 函数的里面(流水线旁边)。明明苹果就摆在旁边,如果还要通过 () 当作参数传进去,是不是太麻烦了?
为了让 Lambda 能直接使用它周围已经存在的变量,C++ 设计了 **起手式 []**,这相当于老板给工人签发的“物品使用授权书”。
2. 授权模式一:值捕获(复印件模式)
生活比喻:
老板桌上有一份重要机密文件(变量 data)。老板允许工人看,但怕工人不小心把原件弄坏了。于是,老板用复印机复印了一份交给工人。
在 [] 中写下变量名,或者直接写个 = 号,就是值捕获。
[apple]:只复印apple这一份文件。[=]:霸气模式,把老板桌上所有能看到的文件,统统复印一份给工人!
核心特点: 工人拿到的只是“复印件”。他可以看,但是默认情况下不能修改(编译器会报错),而且哪怕他真修改了复印件,老板桌上的原件也绝对不会变。
3. 授权模式二:引用捕获(远程遥控器模式)
生活比喻:
老板桌上有一个银行账户(变量 money),这次任务就是让工人去花钱采购。给复印件没用啊,复印的钱花不出去!老板必须给工人一个“远程遥控器”(或者说牵一根线直接连到账户上)。
在 [] 中写下 & 符号,就是引用捕获。
[&money]:只给money这一个变量配发遥控器。[&]:终极霸气模式,给老板桌上所有的变量都配发遥控器!
核心特点: 工人没有拿走原件,但是他通过遥控器直接操作原件。工人在 Lambda 里面做出的任何修改,老板桌上的原件都会立刻跟着改变!
4. 字符画演示:复印件 vs 遥控器
老板的办公桌 (main函数的作用域)
int apple = 5;
【模式一:值捕获 [=] 或 [apple]】
[复印机] ============> 得到一张写着 "5" 的纸条
工人 (Lambda) 只能看着纸条工作,不能改,也改不到真苹果。
【模式二:引用捕获 [&] 或 [&apple]】
[一根无形的线] ======> 直接绑在老板桌上的真苹果上
工人 (Lambda) 咬一口 (apple -= 1),老板桌上的真苹果就变成了 4!
5. Code Demo 演示
让我们用代码来看看“复印件”和“遥控器”的巨大威力差异:
#include <iostream>
int main() {
int money = 100; // 老板桌上的资金
// 1. 值捕获(复印件): [money] 或 [=]
auto checkBalance = [money]() {
// money -= 20; // 错误!复印件默认是只读的,编译器会阻止你修改
std::cout << "值捕获查账:我看到的余额是 " << money << std::endl;
};
// 2. 引用捕获(遥控器): [&money] 或 [&]
auto spendMoney = [&money]() {
money -= 20; // 成功!通过遥控器直接扣老板的钱
std::cout << "引用捕获消费:花了20,余额还剩 " << money << std::endl;
};
// 执行任务
checkBalance();
spendMoney();
// 老板亲自确认桌上的真钱
std::cout << "老板确认最终余额: " << money << std::endl;
return 0;
}
运行结果:
- 值捕获查账:我看到的余额是 100
- 引用捕获消费:花了20,余额还剩 80
- 老板确认最终余额: 80
第三讲 课后练习
题目:
在一个 main 函数中,记录了一个学生的初始分数 int score = 60;。
现在由于该学生表现优异,老师决定给他加分。
请你编写一个 Lambda 表达式,命名为 addBonus。
任务要求:
- 这个 Lambda 不需要任何外部传入参数(即
()为空)。 - 使用合适的捕获方式,让 Lambda 能够直接访问并修改外部的
score变量。 - 在 Lambda 函数体内,将
score增加 15 分。 - 调用这个 Lambda。
- 在
main函数的最后,打印输出score的最终值,验证它是否成功变成了 75。
(请仔细思考应该用 [=] 还是 [&],构思后再查看解析)
详细解析与答案
解析思路:
- 我们要在 Lambda 内部修改外部的变量,并且希望这个修改能真实反映到原变量上。
- 显然,“复印件模式”(值捕获
[=]或[score])是不行的,它连修改都通不过编译。 - 我们必须使用“遥控器模式”(引用捕获
[&]或[&score])。 - 起手式写为
[&score](推荐,精确授权)或者[&](全部授权)。 - 参数列表
()保持为空。 - 函数体写
{ score += 15; }。
标准答案代码:
#include <iostream>
int main() {
int score = 60;
// 必须使用引用捕获 [&score] 或者 [&] 才能修改原件
auto addBonus = [&score]() {
score += 15; // 直接操作 main 函数中的 score
};
// 执行加分任务
addBonus();
// 验证结果
std::cout << "最终分数: " << score << std::endl; // 输出将是 75
return 0;
}
我们已经拿下了 Lambda 表达式最核心的“捕获列表”!现在,你手里有一堆非常好用的“便利贴”(Lambda)。
但随着工程变大,老板(main 函数)遇到了一个新的麻烦:如何把这些便利贴、连同以前成立的传统部门(普通函数),统一管理起来呢?
现在我们就来引入一个超级神器——std::function,万能包装盒(或者叫万能插座)。
第四讲:万能包装盒 std::function
1. 痛点:五花八门的“插头”
在 C++ 中,能够被当做函数调用的东西有很多:
- 普通函数(我们在第一讲里说的外包部门)。
- Lambda 表达式(随用随贴的便利贴)。
- 仿函数(重载了
()运算符的类,这里不展开,你知道有这个东西即可)。
它们就像是不同形状的“插头”。有的 Lambda 甚至因为捕获了不同的变量,导致它们在编译器眼里的“物种”都不一样。
问题来了: 如果老板想买一个盒子,把一个普通函数和一个 Lambda 装在一起(比如放进同一个数组里轮流执行),或者要求你写一个接口,既能接收普通函数,又能接收 Lambda,你怎么规定这个盒子的“类型”?
普通函数有函数指针,Lambda 的类型只能用 auto 猜,它们根本尿不到一个壶里去!
2. 破局:引入万能插座 std::function
为了统一江湖,C++11 在 <functional> 头文件中提供了 std::function。
它是一个“万能插座”(也是一个包装盒)。
它根本不关心你是普通函数还是 Lambda,它只看重一点:你的接口规格是什么?(即:你需要什么原材料,你会交出什么成品)。
只要接口规格对得上,不管是哪种插头,统统可以插到这个万能插座上!
字符画演示:万能插座匹配原则
接口规格要求:需要 2个int,返回 1个int
【普通函数插头】 【Lambda 便利贴插头】
int Add(int, int) [](int, int) -> int
│ │
└─────────┐ ┌─────────┘
▼ ▼
+-------------------+
| 万能插座 / 包装盒 |
| std::function |
+---------+---------+
│
执行任务!
3. 如何定义这个万能插座?
它的语法非常直观,把“成品”和“原材料”写在尖括号 <> 里,像这样:
std::function< 返回类型 (参数类型1, 参数类型2...) > 变量名;
打个比方,如果我们想要一个插座,它接收两个 int 零件,返回一个 int 成品,写出来就是:
std::function<int(int, int)>
4. Code Demo 演示:统一江湖
让我们用代码看看 std::function 是如何把普通函数和 Lambda 统一装进一个盒子里的:
#include <iostream>
#include <functional> // 必须包含这个头文件!
// 1. 这是一个传统的普通函数
int NormalMultiply(int a, int b) {
return a * b;
}
int main() {
// 2. 这是一个 Lambda 表达式
auto lambdaAdd = [](int a, int b) {
return a + b;
};
// 3. 定义万能包装盒!
// 规格:接收两个 int (原材料),返回一个 int (成品)
std::function<int(int, int)> taskWrapper;
// --- 开始插拔测试 ---
// 第一次:把普通函数装进包装盒
taskWrapper = NormalMultiply;
std::cout << "包装盒执行乘法: " << taskWrapper(5, 4) << std::endl; // 输出 20
// 第二次:拔下来,换成 Lambda 装进包装盒
taskWrapper = lambdaAdd;
std::cout << "包装盒执行加法: " << taskWrapper(5, 4) << std::endl; // 输出 9
return 0;
}
逻辑链梳理:
你看,taskWrapper 就像一个可以换枪管的武器。因为 NormalMultiply 和 lambdaAdd 都是“接收两个 int 返回一个 int”,所以它们都能完美赋值给 taskWrapper。从此以后,老板只需要拿着 taskWrapper 去执行就行了,底层到底是普通函数还是 Lambda,老板根本不用操心!
第四讲 课后练习
题目:
请在一个 main 函数中,利用 std::function 实现以下逻辑:
- 包含必要的头文件。
- 声明一个名为
printer的万能包装盒 (std::function)。
- 规格要求: 接收一个
std::string类型的参数,不返回任何东西(即返回void)。
- 编写一个 Lambda 表达式,接收一个字符串并在屏幕上打印它(加上一句前缀,比如
"正在打印: ")。 - 将这个 Lambda 赋值给
printer包装盒。 - 通过
printer传入字符串"Hello std::function!"并执行调用。
(仔细思考 std::function 尖括号里的格式应该怎么写,构思后再查看解析)
详细解析与答案
解析思路:
- 用到了
std::function,所以顶部必须有#include <functional>;用到了字符串,需要#include <string>。 - 定义万能插座的规格:不需要返回值,所以返回类型是
void;需要传入一个字符串,所以参数是std::string。组合起来就是:std::function<void(std::string)> printer; - 写 Lambda:
[](std::string text) { std::cout << "正在打印: " << text << std::endl; } - 将 Lambda 赋值给
printer。 - 执行调用:
printer("Hello std::function!");
标准答案代码:
#include <iostream>
#include <string>
#include <functional> // std::function 必须的头文件
int main() {
// 1. 定义万能包装盒,规格:返回 void,接收 std::string
std::function<void(std::string)> printer;
// 2. 将 Lambda 赋值给包装盒
printer = [](std::string text) {
std::cout << "正在打印: " << text << std::endl;
};
// 3. 通过包装盒执行任务
printer("Hello std::function!");
return 0;
}
前面四讲,我们分别掌握了 Lambda 的“骨架”、“参数”、“捕获”以及打包它们的“万能包装盒”。
现在你手里已经有了各种强大的武器,但如果不把它们放到真实的战场上,就体会不到它们真正的威力。
最后一讲,我们将把前面学的所有知识融会贯通,看看在实际的 C++ 开发中,Lambda 和 std::function 是如何打出精妙配合的。
第五讲:实战与大融合
在实际开发中,Lambda 表达式最经典的两个落地场景是:配合 STL 算法(标准模板库) 以及 实现回调机制(Callback)。
1. 场景一:STL 算法的“外置大脑”
生活比喻:
C++ 提供了很多非常强大的“算法指挥官”,比如 std::sort(排序指挥官)。
指挥官非常擅长排兵布阵(快速排序、归并排序等底层算法写得极其高效),但是他有一个致命弱点:他不知道你们具体的业务规则是什么。
比如,你交给他一份体测登记表,让他排序。指挥官会懵圈:“是按学号排?还是按成绩排?还是按身高排?”
这时候,你就需要用一张 Lambda 便利贴,贴在指挥官脑门上,充当他的“外置大脑”,告诉他比较的规则!
字符画演示:
+------------------+
| std::sort 指挥官 |
| (掌握高效排序法) |
+--------+---------+
|
[遇到两个学生 A 和 B,不知道谁该排前面]
|
▼
(查看贴在脑门上的 Lambda 便利贴)
[](Student a, Student b) { return a.score > b.score; }
|
▼
[指挥官恍然大悟:哦!分数高的排前面!继续高效工作!]
Code Demo:处理体测登记表
#include <iostream>
#include <vector>
#include <algorithm> // 必须包含,因为指挥官们都在这里面
// 我们的数据模型
struct Student {
int id; // 学号
double score; // 体测分数
};
int main() {
// 准备一份班级男生的体测登记表数据
std::vector<Student> classRoster = {
{101, 85.5},
{102, 92.0},
{103, 78.5},
{104, 60.0}
};
// 任务:将名单按照体测成绩【从高到低】排序
// 传入三个参数:开头、结尾、Lambda 便利贴(比较规则)
std::sort(classRoster.begin(), classRoster.end(), [](const Student& a, const Student& b) {
return a.score > b.score;
});
// 打印看看结果
std::cout << "--- 体测成绩排行榜 ---" << std::endl;
for (const auto& stu : classRoster) {
std::cout << "学号: " << stu.id << ", 分数: " << stu.score << std::endl;
}
return 0;
}
你会发现,因为有了 Lambda,你不需要在外面去专门写一个 bool CompareStudent(...) 的函数,代码紧凑且直观。
2. 场景二:回调机制(异步的“锦囊妙计”)
生活比喻:
假设你去办理一项复杂的业务(比如下载一个大文件),这个过程很慢。你不可能一直傻站在柜台前等。
于是你递给柜员一个信封(std::function 包装盒),信封里装着一张便利贴(Lambda)。你嘱咐柜员:“等业务办完后,打开信封照着里面的纸条做(比如发个短信通知我)。”
这种“你留下任务,别人在未来某个时刻帮你执行”的机制,就叫回调(Callback)。
Code Demo:任务完成后的通知
#include <iostream>
#include <functional> // 为了使用万能包装盒
// 柜员类(执行耗时任务的人)
class Worker {
public:
// 接收一个信封:里面装的是不需要参数、也不返回结果的任务
void doHeavyWork(std::function<void()> callback) {
std::cout << "柜员:正在拼命处理业务中..." << std::endl;
// ... 假设这里经过了漫长的计算 ...
std::cout << "柜员:业务处理完毕!" << std::endl;
// 业务办完,打开信封,执行里面的任务!
callback();
}
};
int main() {
Worker worker;
std::string myName = "张三"; // 老板桌上的变量
// 老板留下信封:要求把 myName 捕获进去(复印件),事后通知
worker.doHeavyWork( [myName]() {
std::cout << "叮咚!短信提醒:" << myName << ",您的业务已办妥!" << std::endl;
});
return 0;
}
第五讲(终极)课后练习
题目:
依然是那份体测登记表。现在有一个新任务:统计出成绩低于及格线的学生人数。
请你结合本讲的“STL 算法”和第三讲的“捕获列表”来完成。
任务要求:
- 包含必要的头文件(
<vector>,<algorithm>等)。 - 在
main函数中定义一个及格线变量:double passMark = 75.0; - 登记表数据已经给你准备好:
std::vector<double> scores = {85.5, 92.0, 72.5, 60.0, 78.0, 59.5}; - 使用 STL 提供的统计指挥官:
std::count_if。
(提示:std::count_if(起始点, 结束点, 规则)会遍历数组,并返回符合规则的数量) - 核心: 编写一个 Lambda 作为规则传入。这个 Lambda 需要捕获外部的及格线变量
passMark,并接收一个double score参数。如果score < passMark,则返回true。 - 将统计出的人数打印出来。
(这是真正的实战大融合!仔细思考应该怎么捕获?怎么传参?怎么写返回值?构思后再查看解析!)
详细解析与答案
解析思路:
- 我们需要用到统计算法
std::count_if。它需要一个比较规则。 - 我们要写的 Lambda 是用来判断“某一个分数是否不及格”。
- 显然,我们需要从数组里接收每一次遍历到的分数,所以参数列表是:
(double score)。 - 我们要把分数和
passMark比较。因为passMark是定义在main函数里的,我们要用[]把它捕获进来。只读就可以,所以用值捕获[passMark]或[=]。 - 函数体内执行比较并返回布尔值:
{ return score < passMark; }。 - 将这个 Lambda 传给
std::count_if,并用一个变量接收最终统计出的人数。
标准答案代码:
#include <iostream>
#include <vector>
#include <algorithm> // count_if 在这里面
int main() {
// 1. 外部变量:及格线
double passMark = 75.0;
// 2. 模拟的体测分数表
std::vector<double> scores = {85.5, 92.0, 72.5, 60.0, 78.0, 59.5};
// 3. 使用 count_if 搭配 Lambda
// 捕获 passMark (复印件),接收 score (原材料)
int failCount = std::count_if(scores.begin(), scores.end(), [passMark](double score) {
return score < passMark; // 如果小于及格线,就算作 1 个
});
// 4. 打印结果
std::cout << "体测不达标的人数为: " << failCount << " 人" << std::endl;
return 0;
}
拓展讲:Lambda 如何实现递归?
痛点:被卡住的 auto 悖论
如果我们尝试用最直觉的方式,借助 auto 来写一个计算阶乘(
n
!
=
n
×
(
n
−
1
)
!
n! = n \times (n-1)!
n!=n×(n−1)!)的递归 Lambda,编译器会直接报错:
// ❌ 错误示范:编译器会报错
auto factorial = [](int n) {
if (n <= 1) return 1;
// 报错:在此处 factorial 还没定义完,编译器不知道 factorial 是什么!
return n * factorial(n - 1);
};
原因在于: auto 的工作原理是“等号右边的东西完全确定了,我再根据右边的结果推导左边变量的类型”。但在等号右边还没写完的时候,你就在内部提前使用了左边的名字 factorial,这就形成了“鸡生蛋、蛋生鸡”的死锁。
针对 Lambda 怎么调用自己(递归),C++ 的演进历史中给出了三种极其精简的写法。
我们直接用最简单的“计算阶乘( n ! n! n!)”作为例子,来把这三种写法扒得干干净净。
1. 经典老手:std::function 法 (C++11)
这是我们之前讲过的,核心思想是“先造空盒,再装内容”。
- 比喻:给一张没名字的便利贴强行套上一个有名字的“工牌”(
std::function),便利贴在里面就能看着工牌呼叫自己了。 - 写法:提前声明变量,配合捕获列表
[&]。
#include <iostream>
#include <functional>
int main() {
// 1. 造空盒(挂工牌)
std::function<int(int)> factorial;
// 2. 捕获空盒,装入逻辑
factorial = [&factorial](int n) {
if (n <= 1) return 1;
return n * factorial(n - 1); // 直接呼叫工牌名字
};
std::cout << "C++11 写法结果: " << factorial(5) << std::endl;
return 0;
}
缺点:需要引入 <functional> 头文件,运行效率有极其微小的损耗。
2. 泛型俄罗斯套娃:auto& self 法 (C++14)
到了 C++14,编译器变聪明了,支持了“泛型 Lambda”(参数可以写 auto)。
这时候程序员发明了一种非常巧妙的黑客写法:“把我自己当成第一个参数,传给我自己”。
- 比喻:你是一个克隆人(Lambda),你不知道自己的名字。但是老板在布置任务时,把你本人作为第一件工具递给了你。当你需要重复工作时,你只需要让手里的“自己”再去干活就行了。
- 写法:参数列表的第一个参数写
auto& self,调用时写self(self, n-1)。
#include <iostream>
int main() {
// 不需要提前造空盒,直接写!
auto factorial = [](auto& self, int n) -> int {
if (n <= 1) return 1;
// 注意:调用的时候,必须把 self 本身再传给下一个自己
return n * self(self, n - 1);
};
// 第一次启动时,必须把自己传进去
std::cout << "C++14 写法结果: " << factorial(factorial, 5) << std::endl;
return 0;
}
缺点:调用起来比较反人类,每次都要写 self(self, ...),看起来很啰嗦。
3. 终极无名之剑:this auto&& self 法 (C++23)
这就是你听说的那个“能直接用自身的函数名递归”的神器!
到了最新的 C++23 标准,官方终于看不下去了,引入了一个新特性叫做 “Deducing this”(推导的 this)。它完美解决了 C++14 那种 self(self, ...) 的啰嗦写法。
- 比喻:便利贴突然觉醒了自我意识。只要你在参数前面加上一个魔法前缀
this,它就能瞬间指代自己。 - 写法:在第一个参数前加上
this,写成this auto&& self。调用时,直接当成普通函数用,不需要再传一遍自己!
#include <iostream>
int main() {
// C++23 的魔法:起手参数加 this 关键字
auto factorial = [](this auto&& factorial, int n) -> int {
if (n <= 1) return 1;
// 爽了爽了,直接用 factorial(n-1),就像调用普通函数一样!
return n * factorial(n - 1);
};
// 启动时,也不需要传自己了,直接传数字!
std::cout << "C++23 写法结果: " << factorial(5) << std::endl;
return 0;
}
这是目前最现代、最高效、最优雅的 Lambda 递归写法!
核心对比字符画
为了让你一眼看穿它们的区别,我们把核心调用行抽出来对比:
【三种写法的演进】
C++11 (std::function) : return n * factorial(n - 1); <-- 需要借助外部包装盒
C++14 (auto& self) : return n * self(self, n - 1); <-- 啰嗦,自己传自己
C++23 (this auto&&) : return n * factorial(n - 1); <-- 终极优雅,天然支持
课后练习
题目:
请使用 C++23 的 this auto&& self 语法,编写一个 Lambda 表达式。
任务要求:
- 传入一个整数 n n n,计算从 1 累加到 n n n 的总和(即 1 + 2 + . . . + n 1 + 2 + ... + n 1+2+...+n)。
- 在
main函数中定义这个 Lambda。 - 直接调用它计算从 1 加到 100 的结果,并打印。
(这个语法极其清爽,请大胆写出代码)
详细解析与答案
解析思路:
- 不需要包含
<functional>,也不需要提前声明包装盒。 - 直接用
auto sum = [](this auto&& self, int n) -> int { ... };即可。 - 递归终止条件:如果
n <= 1,返回 1。 - 递归逻辑:返回
n + self(n - 1)(注意,C++23 语法下不需要写self(self))。 - 直接调用
sum(100)。
标准答案代码:
#include <iostream>
int main() {
// 使用 C++23 的推导 this 语法
auto sum = [](this auto&& sum, int n) -> int {
if (n <= 1) {
return 1;
}
return n + sum(n - 1); // 直接优美地调用自己
};
// 像普通函数一样调用
std::cout << "1 加到 100 的和是: " << sum(100) << std::endl;
return 0;
}
更多推荐



所有评论(0)