文章目录

第一讲:为什么要引入 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,并紧接着调用执行它。

(请先在脑海中或纸上写出你的答案,然后再看下方的解析)


详细解析与答案

解析思路:

  1. 首先,我们要定义一个变量来装这半张“便利贴”,由于 Lambda 类型未知,我们要使用 auto 关键字:auto studyTask =
  2. 接着,写出 Lambda 的起手式,告诉编译器这是个 Lambda:[]
  3. 题目要求不需要外界传入参数,所以参数列表为空:()
  4. 题目要求的任务是打印字符串,所以大括号内写入代码:{ std::cout << "I am learning Lambda!" << std::endl; }
  5. Lambda 定义完成后,不要忘记末尾需要分号 ; 结束这个赋值语句。
  6. 最后,像调用普通函数一样,加上括号调用它: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;,编译器知道 ab 都是整数,它就会自动推断出返回值也是整数。所以,-> 返回类型 这一部分通常是可以省略不写的! 除非逻辑特别复杂,编译器猜不出来,才需要你手动写上。

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
任务要求:

  1. 该 Lambda 接收两个 double 类型的参数:length(长)和 width(宽)。
  2. 函数体内的逻辑是计算长方形的面积(长乘宽),并将结果返回。
  3. 请省略 -> 尾置返回类型,让编译器自动推导。
  4. 调用这个 Lambda,传入长为 5.5,宽为 2.0,并将结果存储在一个变量中输出到屏幕上。

(请反复推敲代码逻辑,先自行构思后再查看解析)


详细解析与答案

解析思路:

  1. 我们需要一个变量接收 Lambda:auto calculateArea =
  2. 写起手式和参数列表(接收原材料):[](double length, double width)
  3. 题目要求省略 -> 返回类型,所以直接写大括号(执行清单):{ return length * width; }
  4. 不要忘记末尾的分号。
  5. 调用时,就像调用函数一样传入数据,并用一个 double 类型的变量接收成品:double area = calculateArea(5.5, 2.0);
  6. 最后打印输出。

标准答案代码:

#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
任务要求:

  1. 这个 Lambda 不需要任何外部传入参数(即 () 为空)。
  2. 使用合适的捕获方式,让 Lambda 能够直接访问并修改外部的 score 变量。
  3. 在 Lambda 函数体内,将 score 增加 15 分。
  4. 调用这个 Lambda。
  5. main 函数的最后,打印输出 score 的最终值,验证它是否成功变成了 75。

(请仔细思考应该用 [=] 还是 [&],构思后再查看解析)


详细解析与答案

解析思路:

  1. 我们要在 Lambda 内部修改外部的变量,并且希望这个修改能真实反映到原变量上。
  2. 显然,“复印件模式”(值捕获 [=][score])是不行的,它连修改都通不过编译。
  3. 我们必须使用“遥控器模式”(引用捕获 [&][&score])。
  4. 起手式写为 [&score](推荐,精确授权)或者 [&](全部授权)。
  5. 参数列表 () 保持为空。
  6. 函数体写 { 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++ 中,能够被当做函数调用的东西有很多:

  1. 普通函数(我们在第一讲里说的外包部门)。
  2. Lambda 表达式(随用随贴的便利贴)。
  3. 仿函数(重载了 () 运算符的类,这里不展开,你知道有这个东西即可)。

它们就像是不同形状的“插头”。有的 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 就像一个可以换枪管的武器。因为 NormalMultiplylambdaAdd 都是“接收两个 int 返回一个 int”,所以它们都能完美赋值给 taskWrapper。从此以后,老板只需要拿着 taskWrapper 去执行就行了,底层到底是普通函数还是 Lambda,老板根本不用操心!


第四讲 课后练习

题目:
请在一个 main 函数中,利用 std::function 实现以下逻辑:

  1. 包含必要的头文件。
  2. 声明一个名为 printer 的万能包装盒 (std::function)。
  • 规格要求: 接收一个 std::string 类型的参数,不返回任何东西(即返回 void)。
  1. 编写一个 Lambda 表达式,接收一个字符串并在屏幕上打印它(加上一句前缀,比如 "正在打印: ")。
  2. 将这个 Lambda 赋值给 printer 包装盒。
  3. 通过 printer 传入字符串 "Hello std::function!" 并执行调用。

(仔细思考 std::function 尖括号里的格式应该怎么写,构思后再查看解析)


详细解析与答案

解析思路:

  1. 用到了 std::function,所以顶部必须有 #include <functional>;用到了字符串,需要 #include <string>
  2. 定义万能插座的规格:不需要返回值,所以返回类型是 void;需要传入一个字符串,所以参数是 std::string。组合起来就是:std::function<void(std::string)> printer;
  3. 写 Lambda:[](std::string text) { std::cout << "正在打印: " << text << std::endl; }
  4. 将 Lambda 赋值给 printer
  5. 执行调用: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 算法”和第三讲的“捕获列表”来完成。

任务要求:

  1. 包含必要的头文件(<vector>, <algorithm> 等)。
  2. main 函数中定义一个及格线变量:double passMark = 75.0;
  3. 登记表数据已经给你准备好:std::vector<double> scores = {85.5, 92.0, 72.5, 60.0, 78.0, 59.5};
  4. 使用 STL 提供的统计指挥官:std::count_if
    (提示:std::count_if(起始点, 结束点, 规则) 会遍历数组,并返回符合规则的数量)
  5. 核心: 编写一个 Lambda 作为规则传入。这个 Lambda 需要捕获外部的及格线变量 passMark,并接收一个 double score 参数。如果 score < passMark,则返回 true
  6. 将统计出的人数打印出来。

(这是真正的实战大融合!仔细思考应该怎么捕获?怎么传参?怎么写返回值?构思后再查看解析!)


详细解析与答案

解析思路:

  1. 我们需要用到统计算法 std::count_if。它需要一个比较规则。
  2. 我们要写的 Lambda 是用来判断“某一个分数是否不及格”。
  3. 显然,我们需要从数组里接收每一次遍历到的分数,所以参数列表是:(double score)
  4. 我们要把分数和 passMark 比较。因为 passMark 是定义在 main 函数里的,我们要用 [] 把它捕获进来。只读就可以,所以用值捕获 [passMark][=]
  5. 函数体内执行比较并返回布尔值:{ return score < passMark; }
  6. 将这个 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×(n1)!)的递归 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 表达式。
任务要求:

  1. 传入一个整数 n n n,计算从 1 累加到 n n n 的总和(即 1 + 2 + . . . + n 1 + 2 + ... + n 1+2+...+n)。
  2. main 函数中定义这个 Lambda。
  3. 直接调用它计算从 1 加到 100 的结果,并打印。

(这个语法极其清爽,请大胆写出代码)


详细解析与答案

解析思路:

  1. 不需要包含 <functional>,也不需要提前声明包装盒。
  2. 直接用 auto sum = [](this auto&& self, int n) -> int { ... }; 即可。
  3. 递归终止条件:如果 n <= 1,返回 1。
  4. 递归逻辑:返回 n + self(n - 1)(注意,C++23 语法下不需要写 self(self))。
  5. 直接调用 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;
}

更多推荐