CC++ & Algorithm

Lambda表达式基础与语法

极难5
语言版本:通用
概述:Lambda表达式是C++11引入的“匿名函数”小魔法,可以让你在需要临时函数的地方直接写一小段代码,就像在路边写个“顺手”的便条,不用专门定义函数。

什么是 Lambda 表达式?

想象你放学回家,妈妈让你把一袋苹果按大小从小到大排列。如果你需要写一份“排序说明书”,你会专门写一篇长长的论文介绍“如何比较两个苹果”,还是直接写个便条:“比较两个苹果:先看第一个是不是比第二个小,是的话就放前面”?

编程中也常常遇到类似情况:你只需要一个临时用一下的比较方法,比如排序时指定按什么规则排。以前你必须定义一个单独的函数(或者一个仿函数类),用完就丢,有点麻烦。C++11 引入了一个很棒的特性——Lambda 表达式,让你可以像写便条一样,直接在需要的地方写出一个小函数,用完就消失,非常方便。

Lambda 表达式本质上是一个匿名的函数对象(编译器会悄悄帮你生成一个匿名的仿函数类)。它可以被当作一个变量来保存,也可以直接传递给算法,就像传递一个数字或字符串一样简单。

最简单的 Lambda 长这样:

auto double_it = [](int x) { return x * 2; };   // 定义一个“乘以2”的小函数
int result = double_it(5);  // result = 10

这个 [](int x) { return x * 2; } 就是 Lambda 表达式,它像一个“随身携带的计算器”。


Lambda 表达式语法详解

Lambda 表达式的基本结构是:

[capture] (parameters) -> return_type { body }
  • [capture]:捕获列表,用来“借”周围作用域中的变量,让 Lambda 内部可以使用它们。这个符号是必须的,哪怕什么都不写([])。
  • (parameters):参数列表,和普通函数一样,可以省略(如果无参数)。
  • -> return_type:返回类型(可选),大多数时候编译器能自动推导,但如果想强制返回类型,可以写在这里。
  • { body }:函数体,包含你要执行的代码。

1. 最简单的 Lambda

auto say_hello = [] {         // 无参数,捕获列表为空,返回类型自动推导
    cout << "你好!" << endl;
};
say_hello();   // 调用它

2. 带参数的 Lambda

auto add = [](int a, int b) { return a + b; };
cout << add(3, 4) << endl;   // 7

3. 显式指定返回类型

如果 Lambda 体内有多个 return 语句类型不同,或者你希望强制转换类型,可以加上 -> type

auto divide = [](double a, double b) -> double { 
    if (b == 0) return 0.0; 
    return a / b; 
};

4. 捕获列表:让 Lambda 使用外部的变量

这是 Lambda 最神奇的地方——它可以“借用”它所在函数或代码块里的变量。捕获列表就是用来声明你需要借哪些变量,以及怎么借(按值还是按引用)。

  • [x]:按值捕获变量 x(复制一份给 Lambda,Lambda 内改不了外部的 x)。
  • [&x]:按引用捕获 x(Lambda 内可以直接修改外部的 x)。
  • [=]:按值捕获所有用到的外部变量。
  • [&]:按引用捕获所有用到的外部变量。
  • 可以混合,比如 [=, &y]:y 按引用,其他按值。

生活例子:你帮妈妈比较苹果大小,但你手里有一个“标准苹果”作为参考。你就可以把这个标准苹果“捕获”到比较规则里。

int base = 10;   // 基准值
auto compare = [base](int x) { return x > base; };   // 按值捕获 base
cout << compare(15) << endl;   // 1 (true)
cout << compare(5) << endl;    // 0 (false)

注意:如果按值捕获,Lambda 内部得到的 base 是捕获那一刻的副本,后面外部 base 变了也不会影响 Lambda 内部。

如果希望 Lambda 内部能修改捕获的变量,必须按引用捕获:

int count = 0;
auto increment = [&count]() { count++; };   // 引用捕获
increment();
cout << count << endl;   // 1

新手易犯错误:忘记写捕获列表,却想在 Lambda 内部使用外部变量,编译器会报错。


在 STL 算法中使用 Lambda

很多 STL 算法都接受可调用对象作为参数,比如 sortfor_eachfind_iftransformcount_if。Lambda 最常和这些算法配合使用。

例子 1:排序时指定降序

vector<int> v = {3, 1, 4, 1, 5};
sort(v.begin(), v.end(), [](int a, int b) { return a > b; });  // 降序

这里我们没有写 greater<int>(),而是用 Lambda 直接表达了“a > b”的比较逻辑,就像在排序的说明书上写了一句注释。

例子 2:for_each 打印每个元素

vector<int> v = {10, 20, 30};
for_each(v.begin(), v.end(), [](int x) { 
    cout << "值: " << x << endl; 
});

例子 3:按字符串长度排序(生活例子)

假设你有一堆同学的名字,你想按名字长度从短到长排列,比较规则用 Lambda 写出来:

vector<string> names = {"Alice", "Bob", "Charlie", "David"};
sort(names.begin(), names.end(), [](const string& a, const string& b) {
    return a.size() < b.size();   // 长度小的放前面
});
// 结果: Bob, Alice, David, Charlie

例子 4:find_if 查找第一个偶数

vector<int> v = {1, 3, 5, 6, 7};
auto it = find_if(v.begin(), v.end(), [](int x) { return x % 2 == 0; });
if (it != v.end()) cout << "找到偶数: " << *it << endl;  // 6

例子 5:count_if 统计成绩及格人数

vector<int> scores = {85, 42, 73, 60, 91, 55};
int passed = count_if(scores.begin(), scores.end(), [](int s) { return s >= 60; });
cout << "及格人数: " << passed << endl;   // 4

例子 6:transform 把数组每个元素加 10

vector<int> v = {1, 2, 3};
vector<int> result(v.size());
transform(v.begin(), v.end(), result.begin(), [](int x) { return x + 10; });
// result = {11, 12, 13}

新手容易犯的常见错误

❌ 错误 1:忘记写捕获列表

int threshold = 5;
vector<int> v = {10, 2, 30};
auto it = find_if(v.begin(), v.end(), [](int x) { return x > threshold; }); // 错误!没有捕获 threshold

编译器会报错:threshold 未捕获。应该写成 [threshold][&]

❌ 错误 2:按值捕获后想修改外部变量,但没成功

int count = 0;
auto increment = [count]() { count++; };   // 按值捕获,count 是副本
increment();
cout << count << endl;   // 依然是 0,因为修改的是副本

如果想修改外部的 count,必须按引用捕获:[&count]

❌ 错误 3:Lambda 体内有多个 return 语句,返回类型不同

auto check = [](int x) {
    if (x > 0) return x;        // int
    else return 0.5;            // double,类型冲突
};

编译器无法自动推导返回类型。解决方法:显式指定返回类型 -> double,或者让两个 return 类型一致。

❌ 错误 4:Lambda 体内写了复杂语句(超过两行)却不注意可读性

Lambda 适合临时短小的逻辑。如果逻辑很长(比如超过 5 行),建议单独写成一个普通函数或仿函数类,代码更清晰。


完整可运行的 C++ 示例(含捕获列表)

下面的代码综合展示了 Lambda 的多种用法,包括捕获列表的使用。

#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
using namespace std;

int main() {
    // 1. 基础:定义乘法器
    auto multiply = [](int x, int y) { return x * y; };
    cout << "3 * 4 = " << multiply(3, 4) << endl;   // 12

    // 2. 捕获外部变量:统计大于基准值的元素个数
    int base = 10;
    vector<int> numbers = {5, 12, 18, 3, 20};
    // 按值捕获 base
    int big_count = count_if(numbers.begin(), numbers.end(), [base](int n) { return n > base; });
    cout << "大于 " << base << " 的个数: " << big_count << endl;  // 3

    // 3. 引用捕获:在 Lambda 内累加计数
    int sum = 0;
    for_each(numbers.begin(), numbers.end(), [&sum](int n) { sum += n; });
    cout << "总和: " << sum << endl;   // 58

    // 4. 混合捕获:按值捕获变量 min,按引用捕获 max
    int min_val = 10, max_val = 0;
    auto find_min_max = [min_val, &max_val](int x) {
        if (x < min_val) min_val = x;   // 错误:min_val 是按值捕获,不能修改副本
        // 应改为按引用捕获两者,或者用另一种方式。这里演示错误,实际不会编译
    };
    // 正确做法:全部按引用
    // auto find_min_max = [&min_val, &max_val](int x) { ... };

    // 5. 立即执行的 Lambda(常用于初始化)
    int random_seed = []() {
        // 这里可以放一些复杂的初始化代码
        return 42;
    }();   // 调用这个 Lambda,结果赋给 random_seed
    cout << "随机种子: " << random_seed << endl;

    // 6. Lambda 作为函数参数(自定义排序:按字符串长度排序)
    vector<string> fruits = {"banana", "apple", "cherry", "date"};
    sort(fruits.begin(), fruits.end(), [](const string& a, const string& b) {
        return a.size() < b.size();   // 按长度升序
    });
    cout << "按长度排序后的水果: ";
    for (const string& f : fruits) cout << f << " ";   // date apple banana cherry
    cout << endl;

    return 0;
}

输出说明

  • 3 * 4 = 12
  • 大于10的个数: 3
  • 总和: 58
  • 随机种子: 42
  • 按长度排序后的水果: date apple banana cherry

Python 中的 Lambda(对比学习)

Python 的 lambda 更简洁,但只能写一个表达式(不能包含语句),适合简单逻辑。

from typing import List

def main():
    # 1. 基础
    multiply = lambda x, y: x * y
    print("3 * 4 =", multiply(3, 4))   # 12

    # 2. 按值捕获——Python 的 lambda 会自动捕获外部变量(闭包)
    base = 10
    numbers = [5, 12, 18, 3, 20]
    big_count = sum(1 for n in numbers if n > base)
    print("大于", base, "的个数:", big_count)   # 3

    # 3. 累加(使用 for 循环或 reduce)
    total = 0
    for n in numbers:
        total += n
    print("总和:", total)   # 58

    # 4. 立即执行的 lambda
    random_seed = (lambda: 42)()
    print("随机种子:", random_seed)

    # 5. 排序:按字符串长度
    fruits = ["banana", "apple", "cherry", "date"]
    fruits.sort(key=lambda s: len(s))
    print("按长度排序后的水果:", fruits)   # ['date', 'apple', 'banana', 'cherry']

if __name__ == "__main__":
    main()

总结与相关指引

要点回顾

  • Lambda 表达式是创建匿名函数对象的语法糖,核心结构 [] (params) { body }
  • 捕获列表 [] 不可或缺,用于借入外部变量。
  • 常与 STL 算法(sortfor_eachfind_if 等)配合,使代码紧凑、可读性强。
  • 注意按值捕获和按引用捕获的区别,以及返回类型推导的规则。
  • 如果 Lambda 体超过 5 行,建议提取为单独的函数。

接下来可以学习

  • 捕获列表的深入用法(mutable、初始化捕获、泛型 Lambda)。
  • Lambda 与仿函数的对比。
  • 在自定义容器或算法中如何利用 Lambda 提高灵活性。

Lambda 是现代 C++ 必备的工具,掌握它后你会发现写排序、过滤、变换等操作变得非常自然,就像在一张便条上写下规则一样简单。快去试试吧!

例题精讲

1单选题

以下关于C++ Lambda表达式的说法中,哪一项是正确的?

A默认情况下,按值捕获的变量在Lambda函数体内可以被修改,修改会影响外部变量。
B按引用捕获的变量在Lambda函数体内不能被修改。
C使用mutable关键字后,按值捕获的变量在Lambda函数体内可以被修改,但不影响外部变量。
D捕获列表可以为空,此时Lambda表达式不能访问任何外部变量,包括全局变量。
2判断题

对于无捕获的Lambda表达式(即捕获列表为空),它可以被隐式转换为函数指针。

3填空题
补全以下代码,使得Lambda能够修改外部变量x的值,并输出修改后的结果。

#include <iostream>
int main() {
    int x = 10;
    auto lambda = [__](int y) { x = y; };
    lambda(20);
    std::cout << x << std::endl;
    return 0;
}
4单选题

对于以下Lambda表达式: auto f = [](auto a, auto b) { return a + b; }; 下列哪一项描述是正确的?

A该Lambda是泛型Lambda,支持不同类型的参数,但只能用于相同类型的参数相加。
B该Lambda是泛型Lambda,可以接受不同类型参数,返回类型由编译器推导为decltype(a+b)。
C该Lambda只能用于整数类型,因为+运算符仅对算术类型有效。
D该Lambda在C++11中已经支持,因为auto作为参数类型是C++11特性。
5填空题
以下代码尝试将Lambda表达式赋值给一个函数指针,请补全Lambda定义,使其能够成功编译。

#include <iostream>
int (*funcPtr)(int, int) = [](int a, int b) -> int { return a * b; };
// 或者
int main() {
    int (*fp)(int, int) = [](int a, int b) __ { return a + b; };
    std::cout << fp(3,4) << std::endl;
    return 0;
}

请填空。