文章目录
- 引言
- [一、Lambda 的基本语法](#一、Lambda 的基本语法)
-
- [1.1 语法拆解](#1.1 语法拆解)
- [1.2 Lambda 的类型是什么](#1.2 Lambda 的类型是什么)
- [二、捕获列表:Lambda 的核心能力](#二、捕获列表:Lambda 的核心能力)
-
- [2.1 `\[\]` ------ 不捕获任何外部变量](#2.1
[]—— 不捕获任何外部变量) - [2.2 `=` ------ 按值捕获所有外部变量](#2.2
[=]—— 按值捕获所有外部变量) - [2.3 `\&` ------ 按引用捕获所有外部变量](#2.3
[&]—— 按引用捕获所有外部变量) - [2.4 显式捕获------推荐的方式](#2.4 显式捕获——推荐的方式)
- [2.1 `\[\]` ------ 不捕获任何外部变量](#2.1
- [三、Lambda 与 C 回调函数的对比](#三、Lambda 与 C 回调函数的对比)
-
- [3.1 函数指针做不到的事](#3.1 函数指针做不到的事)
- [3.2 函数指针能做的,Lambda 都能做](#3.2 函数指针能做的,Lambda 都能做)
- 四、捕获的四个陷阱
-
- [4.1 按引用捕获导致的悬挂引用](#4.1 按引用捕获导致的悬挂引用)
- [4.2 `=` 不会真的"拷贝所有东西"](#4.2
[=]不会真的"拷贝所有东西") - [4.3 按值捕获的变量在定义点是 const](#4.3 按值捕获的变量在定义点是 const)
- [4.4 隐式捕获成员的静默开销](#4.4 隐式捕获成员的静默开销)
- [五、Lambda 的进阶特性(C++14/17/20)](#五、Lambda 的进阶特性(C++14/17/20))
-
- [5.1 泛型 Lambda(C++14)](#5.1 泛型 Lambda(C++14))
- [5.2 `constexpr` Lambda(C++17)](#5.2
constexprLambda(C++17)) - [5.3 模板 Lambda(C++20)](#5.3 模板 Lambda(C++20))
- [六、编译器视角:Lambda 到底是什么](#六、编译器视角:Lambda 到底是什么)
- [七、Lambda 在 STL 中的典型用法](#七、Lambda 在 STL 中的典型用法)
-
- [7.1 自定义排序](#7.1 自定义排序)
- [7.2 条件查找和过滤](#7.2 条件查找和过滤)
- [7.3 用 Lambda 做 RAII 式的清理](#7.3 用 Lambda 做 RAII 式的清理)
- [八、Lambda vs 函数对象 vs 函数指针------什么时候用什么](#八、Lambda vs 函数对象 vs 函数指针——什么时候用什么)
- 总结
本系列为《C++深度修炼:基础、STL源码与多线程实战》第14篇
前置条件:理解函数指针(C 语言),了解
auto(第13篇),了解引用(第9篇)
引言
C 语言里,你想传递"一段逻辑"给别的函数,基本只有一个办法------函数指针:
c
#include <stdlib.h>
int cmp_asc(const void *a, const void *b) {
return *(int*)a - *(int*)b;
}
int main() {
int arr[] = {5, 2, 8, 1, 9};
qsort(arr, 5, sizeof(int), cmp_asc);
// cmp_asc 的定义和调用点可能相距 50 行------阅读代码时要反复跳转
}
问题在哪里?数据和逻辑是分离的。 cmp_asc 函数看不到调用处的上下文,不能直接访问 main 中的局部变量。如果你想让比较逻辑依赖某个外部阈值,只能走全局变量或者 qsort_r 这种平台特定的扩展。
C++ 的 lambda 表达式 一次性解决了这个问题:你可以在需要的地方直接写一个函数 ,而且这个函数能捕获调用处的局部变量。
cpp
#include <algorithm>
#include <vector>
int main() {
std::vector<int> v = {5, 2, 8, 1, 9};
std::sort(v.begin(), v.end(),
[](int a, int b) { return a < b; }); // 就地定义比较逻辑
}
本文将 lambda 从头到尾讲清楚------从语法到捕获列表的机制,再到 C++14/17/20 的增强。
一、Lambda 的基本语法
1.1 语法拆解
cpp
// [捕获列表] (参数列表) -> 返回类型 { 函数体 }
auto add = [](int a, int b) -> int { return a + b; };
大部分情况下可以省略返回类型(编译器自动推导):
cpp
auto add = [](int a, int b) { return a + b; }; // 自动推导返回 int
// 完整语法:
// [capture] (params) mutable exception -> return_type { body }
// 只有 [capture] 和 { body } 是必须的,其他都可以省略
最简形式:
cpp
auto hello = [] { std::cout << "hello\n"; }; // 无参数、无返回值
hello(); // 调用方式和普通函数一样
1.2 Lambda 的类型是什么
每个 lambda 表达式都有一个独一无二的、不可书写的类型------由编译器为它单独生成:
cpp
auto f1 = [](int x) { return x * 2; };
auto f2 = [](int x) { return x * 2; };
// f1 和 f2 的类型完全不同------即使函数体一模一样
// sizeof(lambda) 通常很紧凑
std::cout << sizeof(f1) << '\n'; // 通常是 1(无捕获时)
因为类型没有名字,所以必须用 auto 来接收 lambda。如果你想把它存到容器里或者传给需要具体类型的地方,可以用 std::function(第53篇会深入其实现):
cpp
#include <functional>
std::function<int(int)> f = [](int x) { return x * 2; };
编译器实际上把 lambda 转换为一个匿名的函数对象(functor) ------一个重载了 operator() 的类。这是理解 lambda 工作原理的关键,下文会展开。
二、捕获列表:Lambda 的核心能力
2.1 [] ------ 不捕获任何外部变量
cpp
int x = 10;
// auto f = [] { return x; }; // ❌ 编译错误:x 不在 lambda 的作用域内
auto f = [] { return 42; }; // ✅ 只能访问自己的参数和全局变量
2.2 [=] ------ 按值捕获所有外部变量
cpp
int threshold = 10;
int offset = 5;
auto pred = [=](int x) {
return x > threshold + offset; // threshold 和 offset 被拷贝进了 lambda
};
threshold = 100; // 修改外部变量不影响 lambda 内部的拷贝
std::cout << std::boolalpha << pred(20) << '\n'; // true(20 > 10 + 5)
2.3 [&] ------ 按引用捕获所有外部变量
cpp
int counter = 0;
auto inc = [&] { ++counter; }; // counter 是引用------直接操作外部变量
inc();
inc();
std::cout << counter << '\n'; // 2
2.4 显式捕获------推荐的方式
按值/按引用全捕获虽然方便,但会掩盖"lambda 到底依赖什么"的信息。更好的做法是显式列出捕获的变量:
cpp
int x = 10;
double y = 3.14;
std::string name = "alice";
// 按值捕获 x 和 y,按引用捕获 name
auto f = [x, y, &name] {
// x, y 是副本,name 是引用
std::cout << x << ' ' << y << ' ' << name << '\n';
};
为什么显式捕获更好:
- 读者一眼就知道 lambda 依赖什么外部状态------不用脑补
- 避免意外的拷贝 ------
[=]对大型对象会产生性能问题 - 避免悬挂引用 ------
[&]捕获的引用在 lambda 比变量活得久时是致命的(见第四节)
C++14 起,还支持带初始化器的捕获:
cpp
// 移动捕获------把 unique_ptr 移进 lambda
auto up = std::make_unique<int>(42);
auto f = [p = std::move(up)] { return *p; };
// up 现在是 nullptr,p 在 lambda 内部是 unique_ptr
三、Lambda 与 C 回调函数的对比
3.1 函数指针做不到的事
C 语言的函数指针无法携带上下文数据。当你调用 qsort 时,比较函数只能拿到两个元素------看不到任何外部信息。绕过这个限制只有两种办法:
c
// 方法 1:全局变量(丑陋、线程不安全)
static int g_threshold;
int cmp_with_threshold(const void *a, const void *b) {
return (*(int*)a - g_threshold) - (*(int*)b - g_threshold);
}
// 方法 2:平台特定的 qsort_r(不跨平台、参数顺序不一致)
// glibc: qsort_r(base, nmemb, size, thunk, cmp)
// BSD: qsort_r(base, nmemb, size, cmp, thunk) // 参数顺序都不一样!
Lambda 的答案------上下文直接跟在代码旁边:
cpp
int threshold = 10;
std::vector<int> v = {5, 15, 8, 12, 3};
std::sort(v.begin(), v.end(),
[threshold](int a, int b) {
// 可以用 threshold!不需要全局变量
return std::abs(a - threshold) < std::abs(b - threshold);
});
// 按与 threshold 的距离排序
3.2 函数指针能做的,Lambda 都能做
无捕获的 lambda 可以隐式转换为函数指针:
cpp
// ✅ 无捕获 → 函数指针
int (*fp)(int, int) = [](int a, int b) { return a + b; };
// ❌ 有捕获 → 不能转换为函数指针
int x = 10;
// int (*fp2)(int) = [x](int a) { return a + x; }; // 编译错误
如果你需要把有捕获的 lambda 传给 C API 的回调参数(就是 void* userdata 那个),可以这样做:
cpp
// C API
typedef void (*callback_t)(void *userdata, int value);
void register_callback(callback_t cb, void *userdata);
// C++ 包装
auto lambda = [&](int value) { std::cout << value << '\n'; };
// 传 lambda 的地址 + 捕获数据的地址------但需要用无捕获 lambda 做桥接
using cb_adapt = void(*)(void*, int);
register_callback(
[](void *data, int value) {
auto &fn = *static_cast<decltype(lambda)*>(data);
fn(value);
},
&lambda
);
四、捕获的四个陷阱
4.1 按引用捕获导致的悬挂引用
cpp
#include <iostream>
#include <functional>
std::function<int()> create_dangling() {
int local = 42;
return [&] { return local; }; // ❌ local 的引用在函数返回后就失效了!
}
int main() {
auto f = create_dangling();
std::cout << f() << '\n'; // 未定义行为------读取已销毁的栈变量
}
cpp
// ✅ 按值捕获------安全
std::function<int()> create_safe() {
int local = 42;
return [=] { return local; }; // local 被拷贝进 lambda
}
4.2 [=] 不会真的"拷贝所有东西"
cpp
class Widget {
public:
void schedule() {
int local = 1;
// [=] 看起来应该拷贝 this 和 local......
auto f = [=] { return value_ + local; };
// 实际上 [=] 只拷贝了 local,value_ 是通过 this 指针访问的!
// 等价于 [this, local] 而不是 [*this, local]
task_ = f;
} // Widget 对象析构后,task_ 还持有 this 指针的拷贝------悬挂!
private:
int value_ = 10;
std::function<int()> task_;
};
C++17 起,可以用 [*this] 显式捕获对象副本:
cpp
// C++17:捕获 *this 的副本------安全
auto f = [*this, local] { return value_ + local; };
4.3 按值捕获的变量在定义点是 const
cpp
int x = 0;
auto f = [x] { /* x = 5; */ }; // ❌ 编译错误:x 是只读的
// 按值捕获的变量默认带 const------lambda 的 operator() 是 const 成员函数
如果你需要在 lambda 内部修改按值捕获的副本,用 mutable:
cpp
int x = 0;
auto counter = [x]() mutable {
++x; // ✅ 现在可以修改 x 的副本了
return x;
};
std::cout << counter() << '\n'; // 1
std::cout << counter() << '\n'; // 2
std::cout << x << '\n'; // 0 ------ 外部变量不受影响
4.4 隐式捕获成员的静默开销
cpp
struct LargeObject {
char data[1000000];
int id;
};
LargeObject obj;
// ❌ [=] 看起来像拷贝------但你用到了 obj,它整个被拷贝了!
auto f = [=] { return obj.id; };
std::cout << "sizeof(f) = " << sizeof(f) << '\n'; // 约 1MB!
// 更好的做法:
auto f2 = [id = obj.id] { return id; }; // 只拷贝需要的 int
五、Lambda 的进阶特性(C++14/17/20)
5.1 泛型 Lambda(C++14)
Lambda 的参数可以用 auto------这会把它变成隐式模板:
cpp
// C++14:泛型 lambda
auto twice = [](auto x) { return x * 2; };
std::cout << twice(10) << '\n'; // 20
std::cout << twice(3.14) << '\n'; // 6.28
std::cout << twice(std::string("hello")) << '\n'; // 编译错误------string 不能 *2
编译器为 auto 参数生成的等价代码:
cpp
// 等价于:
struct AnonymousLambda {
template <typename T>
auto operator()(T x) const { return x * 2; }
};
泛型 lambda 配合 STL 算法非常强大:
cpp
std::vector<int> vi = {1, 2, 3, 4, 5};
std::vector<double> vd = {1.1, 2.2, 3.3};
auto print_all = [](const auto &container) {
for (const auto &x : container) {
std::cout << x << ' ';
}
std::cout << '\n';
};
print_all(vi); // 自动推导为 vector<int>
print_all(vd); // 自动推导为 vector<double>
5.2 constexpr Lambda(C++17)
C++17 起,lambda 隐式为 constexpr(如果函数体满足 constexpr 的要求):
cpp
// C++17:lambda 可以用于编译期计算
constexpr auto square = [](int n) { return n * n; };
static_assert(square(5) == 25); // ✅ 编译期求值
// 用 lambda 在编译期初始化数组
constexpr int squares[] = {
[](int i) constexpr { return i * i; }(0),
[](int i) constexpr { return i * i; }(1),
[](int i) constexpr { return i * i; }(2),
};
5.3 模板 Lambda(C++20)
C++20 允许 lambda 参数使用显式模板语法(不用 auto 绕弯):
cpp
// C++20:显式模板 lambda
auto make_vector = []<typename T>(std::initializer_list<T> il) {
return std::vector<T>(il);
};
auto v = make_vector({1, 2, 3, 4, 5}); // vector<int>
// 和 auto 参数版本的区别:可以拿到"T"这个名字------可以做类型相关的操作
六、编译器视角:Lambda 到底是什么
当你写下一个 lambda 表达式时,编译器悄悄生成了一个类:
cpp
// 你写的:
int threshold = 10;
auto pred = [threshold](int x) { return x > threshold; };
// 编译器生成的(语义等价):
class __lambda_14_3 {
int threshold_; // 按值捕获的变量变成了成员变量
public:
explicit __lambda_14_3(int t) : threshold_(t) {}
auto operator()(int x) const { return x > threshold_; }
};
__lambda_14_3 pred(threshold);
理解这个转换后,很多行为就清楚了:
- 捕获 = 将外部变量存入成员变量------在 lambda 构造时完成
- 调用 lambda = 调用
operator()------访问的是成员变量,不是原始外部变量 mutable= 去掉operator()的const限定------允许修改按值捕获的成员sizeoflambda =sizeof所有捕获变量之和(可能加上对齐填充)
验证:
cpp
int a = 0, b = 0, c = 0;
auto f1 = []{}; // 无捕获 → sizeof = 1
auto f2 = [a]{}; // 捕获 1 个 int → sizeof = 4
auto f3 = [a, b]{}; // 捕获 2 个 int → sizeof = 8
auto f4 = [&a]{}; // 捕获 1 个引用 → sizeof = 8(64 位平台上指针大小)
auto f5 = [=]{}; // 捕获 3 个 int → sizeof = 12
// 打印 sizeof 验证即可
七、Lambda 在 STL 中的典型用法
7.1 自定义排序
cpp
#include <algorithm>
#include <vector>
#include <string>
#include <iostream>
struct Person {
std::string name;
int age;
};
int main() {
std::vector<Person> people = {
{"alice", 30}, {"bob", 25}, {"charlie", 35}
};
// 按年龄排序
std::sort(people.begin(), people.end(),
[](const Person &a, const Person &b) { return a.age < b.age; });
// 按名字长度排序
std::sort(people.begin(), people.end(),
[](const Person &a, const Person &b) {
return a.name.size() < b.name.size();
});
for (const auto &p : people) {
std::cout << p.name << " (" << p.age << ")\n";
}
}
7.2 条件查找和过滤
cpp
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 5, 10, 15, 20, 25};
// 查找第一个大于阈值的元素
int threshold = 12;
auto it = std::find_if(v.begin(), v.end(),
[threshold](int x) { return x > threshold; });
if (it != v.end()) {
std::cout << "第一个大于 " << threshold << " 的元素: " << *it << '\n'; // 15
}
// 计数
auto n = std::count_if(v.begin(), v.end(),
[](int x) { return x % 2 == 0; });
std::cout << "偶数个数: " << n << '\n'; // 3(10, 20, 空头25)
}
7.3 用 Lambda 做 RAII 式的清理
cpp
#include <cstdio>
#include <iostream>
#include <memory>
int main() {
FILE *fp = std::fopen("/tmp/test.txt", "w");
if (!fp) return -1;
// 保证函数退出时 fclose 一定被调用
auto cleanup = [](FILE *f) { if (f) std::fclose(f); };
std::unique_ptr<FILE, decltype(cleanup)> guard(fp, cleanup);
std::fputs("hello\n", fp);
// guard 析构时自动调用 fclose
}
八、Lambda vs 函数对象 vs 函数指针------什么时候用什么
| 维度 | 函数指针 | 函数对象(Functor) | Lambda |
|---|---|---|---|
| 携带上下文 | ❌ 不能 | ✅ 成员变量 | ✅ 捕获列表 |
| 可内联 | ⚠️ 困难 | ✅ 容易 | ✅ 最容易 |
| 书写位置 | 必须定义在外部 | 必须定义在外部 | 可以就地定义 |
| 类型名 | 有 | 有 | 无(匿名) |
| 与 C API 交互 | ✅ 天然 | ❌ 需要函数指针桥接 | 无捕获的才可转 |
| 模板参数 | ❌ | ✅ | ✅(C++14 起支持 auto 参数) |
| 适合场景 | 简单的无状态回调 | 需要复用或有状态的复杂逻辑 | 一次性、就地定义的回调 |
经验法则:
- 传给 STL 算法的比较/判断逻辑 → Lambda(就地定义,看一眼就懂)
- 需要复用的有状态逻辑 → 函数对象(有名字,可测试,可文档化)
- C API 回调 → 函数指针(或带
void* userdata参数的 C API + lambda 桥接) - 5 行以上的逻辑 → 提取为命名函数或函数对象------不要用 lambda 吞掉大段代码
总结
Lambda 表达式让 C++ 从"函数只能定义在远处"升级到"逻辑可以跟在调用点身边":
- 语法核心:
[捕获列表](参数){函数体}------捕获列表是 lambda 区别于函数指针的唯一特征 [=]按值捕获,[&]按引用捕获 ------默认用显式捕获([x, &y]),避免意外拷贝和悬挂引用- 编译器把 lambda 变成一个匿名的函数对象 ------捕获的变量成为成员变量,
operator()就是函数体 - 有捕获的 lambda 不能转成函数指针------这是和 C API 交互时的关键限制
- C++14 泛型 lambda(
auto参数)让 lambda 能处理任意类型------实际上是隐式模板 - 悬挂引用是 lambda 的第一大陷阱 ------尤其是
[&]+ 返回 lambda、[=]+this指针
lambda 是现代 C++ 算法式编程的基石。下一篇------C++工程基础:从 gcc 到 CMake 的过渡------我们将把视野从语言特性扩展到工程构建,理解多文件编译、链接和头文件组织的关键规则。
📝 动手练习:
- 写一个 lambda,按字符串长度对一个
vector<string>排序。对比用函数指针的写法(需要额外定义函数)- 写一个
create_counter(int start)函数,返回一个 lambda,每次调用时返回start + 调用次数(需要用mutable)- 用
std::find_if+ lambda 在一个vector<int>中查找第一个能被 3 整除且大于 10 的数- 构造一个返回 lambda 的函数,lambda 按引用捕获函数的局部变量------在
main中调用这个 lambda,观察未定义行为(打印随机值或崩溃)- 写一个泛型 lambda(C++14),打印任意类型容器中每个元素的平方(假设元素支持
*运算符)