更新时间: 2026-08-27
39 篇讲算法库时,sort 的自定义比较器是个绕不开的点——当时说"比较器可以用 lambda",现在把它讲透。lambda(读作"兰姆达")是 C++11 引入的匿名函数对象:就地定义、就地使用,不需要起名字、不需要单独写一个函数或结构体。
本文要回答:lambda 的语法怎么读?[捕获列表] 到底在捕获什么?它和函数指针、手写仿函数有什么区别?
一、最简形态
#include <algorithm>
#include <vector>
std::vector<int> v{3, 1, 4, 1, 5};
// 按降序排序:lambda 作为比较器
std::sort(v.begin(), v.end(), [](int a, int b) {
return a > b;
});拆开看这个 lambda:[](int a, int b) { return a > b; }
| 片段 | 含义 |
|---|---|
[] | 捕获列表(这里为空:不捕获外部变量) |
(int a, int b) | 参数列表(和普通函数一样) |
{ return a > b; } | 函数体 |
也可以赋值给一个变量,当作普通函数对象用:
auto add = [](int a, int b) { return a + b; };
int sum = add(3, 4); // 7auto 承接 lambda 的类型——注意这里不能用 auto f = ... 之外的写法,因为每个 lambda 的类型都是独一无二的(编译器生成的匿名类),你写不出它的类型名。
二、捕获列表:lambda 的特别之处
普通函数只能用自己的参数,而 lambda 可以捕获定义它的作用域里的变量:
#include <iostream>
int main() {
int base = 10;
auto add_base = [base](int x) { // 按值捕获 base
return x + base;
};
std::cout << add_base(3); // 13
int counter = 0;
auto inc = [&counter]() { counter++; }; // 按引用捕获 counter
inc();
std::cout << counter; // 1
}捕获列表有几种写法:
| 写法 | 含义 |
|---|---|
[] | 不捕获任何外部变量 |
[base] | 按值捕获 base(拷贝) |
[&base] | 按引用捕获 base |
[=] | 按值捕获所有用到的外部变量 |
[&] | 按引用捕获所有用到的外部变量 |
[=, &out] | 默认按值,out 例外按引用 |
[this] | 捕获当前对象(成员函数里用) |
两个注意点:
- 按值捕获是拷贝:lambda 内拿到的是副本,改它不影响原变量(除非用
mutable,那属于进阶) - 按引用捕获要小心生命周期:lambda 活多久,引用就必须有效多久。把捕获了引用的 lambda 存到容器里、而引用的变量已经销毁,就是悬垂引用
@startuml
left to right direction
skinparam nodeFontSize 13
skinparam backgroundColor #FFFFFF
rectangle "作用域" as scope #E8F1FF {
node "int base = 10" as base #DCE9FF
node "int counter = 0" as counter #DCE9FF
node "[base] 拷贝 → lambda 内副本" as byv #FFF3D6
node "[&counter] 引用 → 直接改原变量" as byr #D9FFE2
}
base -[hidden]right- counter
byv -[hidden]down- byr
@enduml三、泛型 lambda 与尾置返回类型
泛型 lambda(C++14):参数可以用 auto,让编译器推导类型:
auto max_of = [](auto a, auto b) { return a > b ? a : b; };
std::cout << max_of(3, 4); // 整数
std::cout << max_of(3.14, 2.7); // 浮点尾置返回类型:函数体比较长、或者要强制指定返回类型时用 ->:
auto divide = [](int a, int b) -> double { // 明确返回 double
return static_cast<double>(a) / b;
};多数场景不写返回类型(编译器从 return 推导),但"函数体没有 return 却想返回 void 之外的"这类边界情况,就需要显式声明。
四、C 对照:函数指针 vs 函数对象 vs lambda
C 里"把行为当参数传"用的是函数指针:
#include <stdlib.h>
int cmp_desc(const void* a, const void* b) {
return *(int*)b - *(int*)a; // 又丑又容易错
}
// qsort 使用
qsort(arr, n, sizeof(int), cmp_desc);| 维度 | C 函数指针 | C++ 仿函数(手写类) | C++ lambda |
|---|---|---|---|
| 定义 | 单独写一个函数 | 写一个类,重载 operator() | 就地写,无需命名 |
| 状态 | 无(纯函数) | 类的成员变量 | 捕获列表 |
| 内联优化 | 难(间接调用) | 容易(编译期已知类型) | 容易 |
| 使用场景 | 回调、qsort | C++98 时代的替代 | C++11+ 首选 |
lambda 本质就是编译器帮你生成的仿函数:捕获列表对应类的成员,函数体对应 operator()。所以 lambda 也有类型、也能存、也能复制——它不是魔法,只是语法糖。而正因为类型编译期已知,编译器能把 lambda 调用直接内联,性能上和手写循环没区别——这就是"零成本抽象"在回调场景的体现。
五、lambda 在算法库里的高频用法
lambda 最常见的舞台就是 <algorithm>:
#include <algorithm>
#include <vector>
#include <string>
std::vector<std::string> names{"bob", "Alice", "charlie"};
// 按长度排序
std::sort(names.begin(), names.end(),
[](const std::string& a, const std::string& b) {
return a.size() < b.size();
});
// 查找第一个以 'a' 开头的名字
auto it = std::find_if(names.begin(), names.end(),
[](const std::string& s) { return !s.empty() && s[0] == 'a'; });
// 统计偶数个数
int evens = std::count_if(vec.begin(), vec.end(),
[](int x) { return x % 2 == 0; });sort/find_if/count_if 这类"带谓词(predicate)"的算法,配 lambda 是最顺手的组合——判断逻辑就地写,读代码的人不用跳去别处找函数定义。这在 39 篇算法库里有不少例子,现在语法补齐了,可以回头对照着看。
六、与本站主线衔接
- 39 篇(算法库):
sort比较器、find_if谓词的 lambda 写法是主旋律 - 40 篇(priority_queue):自定义比较器用结构体(模板参数要类型),lambda 需用
decltype包一层——C++17 的惯用写法是auto cmp = [](...){...}; std::priority_queue<T, vector<T>, decltype(cmp)> pq(cmp); - 27 篇(构造/析构):lambda 捕获生命周期与对象生命周期的关系
demos/里不少实验用了 lambda 作为回调,可对照阅读
七、一句话总结
lambda 是 C++11 引入的就地匿名函数对象,语法 [捕获](参数) { 函数体 },捕获列表让它能带状态,类型由编译器生成所以能完美内联——它是现代 C++ 里"把行为当值传递"的标准写法,算法库的回调几乎都被它承包了。