搜索

C++ 教程

C++基础

第1章 搭建开发环境与第一个程序 第2章 基本数据类型与变量 第3章 运算符与表达式 第4章 流程控制语句 第5章 数组与字符串 第6章 函数基础 第7章 指针基础 第8章 引用与const限定 第9章 动态内存管理 第10章 结构体与联合体 第11章 函数进阶

面向对象

第12章 面向对象编程入门 第13章 封装与访问控制 第14章 运算符重载 第15章 继承与派生 第16章 多态与虚函数

STL与进阶

第17章 模板编程基础 第18章 标准模板库(STL)容器 第19章 STL算法与迭代器 第20章 异常处理机制 第21章 文件输入输出 第22章 字符串类string 第23章 命名空间与作用域 第24章 类型转换操作符

现代C++

第25章 智能指针 第26章 移动语义与右值引用 第27章 Lambda表达式 第28章 多线程编程基础 第29章 模板元编程入门 第30章 设计模式基础 第31章 C++11/14新特性 第32章 C++17/20核心特性

实战进阶

第33章 性能优化技巧 第34章 调试与测试 第35章 项目结构与构建 第36章 网络编程入门 第37章 数据库连接操作 第38章 图形界面开发基础 第39章 综合项目实战 第40章 进阶学习路线

第27章 Lambda表达式

第27章 Lambda表达式

进阶 👤 已掌握函数对象和STL算法的开发者 🔧 快速定义匿名函数,简化回调逻辑
本章你将学到
  • Lambda表达式的完整语法与捕获列表
  • 如何用Lambda替代传统的函数对象
  • Lambda在STL算法和多线程中的实战应用
核心概念匿名函数捕获列表

想象一下,你需要一个临时的、只用一次的小工具,比如拧一颗特定尺寸的螺丝。你不会为此专门去买一套完整的工具套装,而是找一个顺手的小扳手。在C++中,Lambda表达式就是这样一个“顺手的小扳手”,它允许你在代码中就地定义一个匿名函数对象,无需预先声明一个独立的函数或函数对象类。

Lambda是什么?

从技术上讲,Lambda表达式是C++11引入的一种语法糖,用于生成一个未命名的函数对象(闭包)。编译器会根据你的Lambda代码,自动生成一个唯一的类类型并实例化它。这使得在需要函数指针、函数对象(仿函数)或`std::function`的地方,可以极其方便地嵌入一段逻辑。

解剖Lambda:从最小形式到完整形态

一个Lambda表达式的基本结构如下,我们通过一个表格来快速理解每个部分:

组成部分说明示例是否可省略
捕获列表 [ ]定义Lambda体可以访问哪些外部变量及访问方式。[x, &y]否(可为空[])
参数列表 ( )与普通函数参数列表相同。(int a, double b)是(无参时可省略)
可变规格 mutable允许按值捕获的变量在Lambda体内被修改。mutable是
异常说明 noexcept声明Lambda是否会抛出异常。noexcept是
返回类型 ->尾置返回类型声明。-> bool是(编译器可推导时)
函数体 { }包含具体的执行代码。{ return x > 0; }否

最简单的Lambda可以只有捕获列表和函数体:[] { std::cout << \"Hello Lambda\"; }。但实际开发中,捕获列表和参数列表是关键。

捕获列表:连接内外世界的桥梁

捕获列表决定了Lambda如何“看到”它所在作用域的变量。这是Lambda最强大也最容易出错的地方。我在项目中踩过这个坑:一个按值捕获的指针在Lambda被异步执行时,其指向的对象可能已被销毁,导致悬空指针访问。

捕获方式主要分为两类:按值捕获和按引用捕获。

int a = 10, b = 20;
// 按值捕获a,按引用捕获b
auto lambda1 = [a, &b]() { return a + b; };
// 默认按值捕获所有外部变量
auto lambda2 = [=]() { return a + b; };
// 默认按引用捕获所有外部变量
auto lambda3 = [&]() { return a + b; };
// 混合捕获:b按引用,其余按值
auto lambda4 = [=, &b]() { return a + b; };
// 混合捕获:a按值,其余按引用
auto lambda5 = [&, a]() { return a + b; };

常见误区

  • 悬空引用:Lambda被延迟调用(如放入线程或队列)时,按引用捕获的局部变量可能已离开作用域被销毁。
  • 误解mutable:mutable仅允许修改按值捕获的变量的副本,不影响外部原变量。它不改变捕获方式。
  • 捕获全局/静态变量:全局变量、静态局部变量、类的this指针无需捕获即可直接使用,捕获它们是多此一举。

Lambda如何工作:编译器的魔法

当你写下Lambda时,编译器在背后为你生成一个匿名的类。这个过程可以用一个简单的架构图来理解:

源代码 (Lambda)
→
编译器处理
→
生成的闭包类
↓
捕获列表
→
变为成员变量
参数与函数体
→
变为operator()

例如,int factor=2; auto l = [factor](int x){ return x * factor; }; 编译器大致会生成如下类:

class __AnonymousLambdaClass {
private:
    int factor; // 按值捕获的变量变为成员
public:
    __AnonymousLambdaClass(int f) : factor(f) {}
    int operator()(int x) const { // 注意默认是const的
        return x * factor;
    }
};

编者提示:理解Lambda生成的闭包类型是唯一的、匿名的,有助于你理解第28章《多线程编程基础》中如何将Lambda传递给std::thread构造函数,以及第25章《智能指针》中如何用Lambda自定义删除器。

实战场景:Lambda的用武之地

实际开发中,大部分团队会选择用Lambda来简化STL算法的调用和实现回调接口,代码更紧凑,意图更清晰。

STL算法伴侣

  • std::sort, std::find_if自定义比较或条件
  • std::for_each遍历容器并操作元素
  • 替代手写的函数对象类

异步与回调

  • std::thread的线程函数
  • std::async的异步任务
  • GUI或网络库中的事件处理器

延迟计算

  • 生成可调用对象,在需要时才执行逻辑
  • 与std::function结合,实现策略模式

来看一个具体的STL算法例子。假设我们有一个`vector`,需要找出所有薪水高于5000且工龄大于3年的员工。用Lambda写std::copy_if的条件一目了然:

struct Employee { string name; double salary; int years; };
vector staff = { ... };
vector candidates;
double salary_threshold = 5000.0;
int year_threshold = 3;

// 使用Lambda,条件逻辑紧邻算法,无需额外定义函数
std::copy_if(staff.begin(), staff.end(),
               std::back_inserter(candidates),
               [salary_threshold, year_threshold](const Employee& emp) {
                   return emp.salary > salary_threshold && emp.years > year_threshold;
               });

性能考量:Lambda vs 函数指针 vs std::function

Lambda生成的闭包对象,其operator()调用通常是内联的,性能与手写的函数对象类相当,优于通过函数指针的间接调用。而std::function是一个类型擦除的包装器,会带来轻微的开销(通常一次动态分配和虚函数调用),在性能敏感的循环中需谨慎使用。对于简单的回调,直接使用auto接收Lambda是最高效的。

进阶话题:泛型Lambda与初始化捕获

C++14引入了泛型Lambda,其参数可以使用auto,这实际上让Lambda的operator()变成了一个模板函数。这在编写通用代码时非常有用。

// C++14 泛型Lambda
auto adder = [](auto a, auto b) { return a + b; };
cout << adder(1, 2); // 3
cout << adder("hello, ", "world"); // "hello, world"

// C++14 初始化捕获(广义捕获),可以移动捕获只移动类型
auto ptr = std::make_unique<int>(42);
auto lambda = [p = std::move(ptr)]() { // 将ptr移动进捕获列表
    return *p;
};

如果你在编译时遇到错误“`error: use of deleted function ‘__lambda_...`”,这通常是因为你试图捕获或复制一个不可复制的对象(如`std::unique_ptr`),却没有使用移动捕获或引用捕获。

动手试一试

  1. 编写一个Lambda,它捕获一个整数`base`(按值),并接受一个整数`x`作为参数,返回`x + base`的值。使用`std::transform`算法,将一个`vector`中的每个元素都加上这个`base`值。
  2. 创建一个`vector`,包含一些单词。使用`std::sort`和一个Lambda,按照单词的长度进行升序排序。如果长度相同,则按字典序排序。
  3. (挑战)使用C++14的初始化捕获,将一个`std::unique_ptr`移动捕获到一个Lambda中,并在Lambda体内打印其值。思考为什么这里必须用移动捕获。

检验你的理解

  1. 以下代码中,变量`local`在`func()`返回后已被销毁,那么`task`被调用时会发生什么?
    void schedule(std::function task);
    void func() {
      int local = 100;
      schedule([&local](){ cout << local; });
    }
  2. 写出一个Lambda表达式,它不捕获任何变量,接受两个`int`参数,返回它们的乘积。
  3. 如果你想在Lambda内部修改一个按值捕获的变量`counter`,需要在Lambda的哪个部分添加什么关键字?

本章小结

  • Lambda本质:是编译器生成的匿名函数对象,语法为`[捕获](参数) mutable noexcept -> 返回类型 {函数体}`。
  • 捕获列表是关键:按值`[var]`创建副本,按引用`[&var]`创建别名。需警惕悬空引用,尤其是Lambda被延迟执行时。
  • 主要应用场景:与STL算法结合定义临时逻辑、作为线程或异步任务入口、实现简洁的回调函数。
  • 性能与类型:Lambda调用通常可内联,效率高。其类型唯一,可用`auto`接收或转换为`std::function`(有开销)。
  • 新特性:C++14的泛型Lambda(`auto`参数)和初始化捕获(`[var = expr]`)提供了更强的表达能力。

意见反馈

微信二维码 微信扫码关注 抖音二维码 抖音扫码关注