在C++11及之后的标准中,lambda表达式提供了一种在调用位置定义匿名函数对象的便捷方式。它常用于标准算法、异步任务以及回调函数,能够显著减少冗余的命名函数定义。一个lambda本质上是一个重载了调用运算符的闭包类实例,编译器在背后为你生成了对应的类型。

一、lambda表达式的基础语法
lambda表达式的最简形式只包含一对空括号和一个函数体,例如 [](){}。其中方括号部分是捕获列表,圆括号是参数列表,花括号内是函数体。如果没有参数,圆括号可以省略,但捕获列表的方括号不能省。
下面是一个最简单的lambda,它不接受参数也不返回有意义的值:
#include <iostream>
int main() {
auto say_hello = []() {
std::cout << "hello lambda" << std::endl;
};
say_hello();
return 0;
}
当我们需要参数和返回值时,可以像普通函数一样声明。编译器通常能根据return语句自动推导返回类型,但在包含多个返回分支且类型不同时,建议使用尾置返回类型明确指定。
例如一个求两数之和的lambda:
#include <iostream>
int main() {
auto add = [](int a, int b) -> int {
return a + b;
};
std::cout << add(3, 4) << std::endl;
return 0;
}
二、捕获列表的几种形式
捕获列表决定了lambda体内可以访问哪些外部变量,以及是以值还是引用的方式访问。最基础的写法是显式列出变量名,例如 [x, &y] 表示值捕获x、引用捕获y。值捕获会在lambda对象构造时拷贝一份,引用捕获则持有原变量的别名。
除了逐个列举,还可以使用默认捕获符号。 [=] 表示以值方式捕获所有被使用的外部变量, [&] 表示以引用方式捕获所有被使用的外部变量。混合写法如 [=, &counter] 允许默认值捕获但针对某个变量使用引用。
以下示例展示了值捕获与引用捕获的差异:
#include <iostream>
int main() {
int a = 10;
int b = 20;
auto by_val = [a]() {
// a 是拷贝,修改不影响外部
return a + 1;
};
auto by_ref = [&b]() {
b += 1; // 修改会影响外部 b
return b;
};
std::cout << by_val() << std::endl;
std::cout << by_ref() << std::endl;
std::cout << "b now: " << b << std::endl;
return 0;
}
需要特别注意的是,值捕获的变量在lambda体内默认是只读的。如果试图修改值捕获的变量,必须添加 mutable 关键字,这会改变闭包类运算符的const属性。
示例如下:
#include <iostream>
int main() {
int count = 0;
auto inc = [count]() mutable {
count += 1;
return count;
};
std::cout << inc() << std::endl; // 输出 1
std::cout << "outer count: " << count << std::endl; // 仍输出 0
return 0;
}
三、捕获this与成员函数中的lambda
在类的成员函数内定义lambda时,如果需要访问当前对象的成员变量或函数,必须捕获 this 指针。写成 [this] 或 [=] 都可以将this纳入捕获,但本质都是引用方式持有当前对象。
这意味着如果lambda生命周期超过了对象本身,例如被异步线程保存,就可能发生悬空this访问。因此在异步场景下,更安全的做法是值捕获需要的成员变量副本,而不是捕获this。
#include <iostream>
#include <thread>
class Worker {
public:
void run() {
int local_data = 42;
// 值捕获局部变量,避免捕获 this 导致悬空
std::thread t([local_data]() {
std::cout << "data: " << local_data << std::endl;
});
t.join();
}
};
int main() {
Worker w;
w.run();
return 0;
}
上面的代码通过值捕获局部变量,使线程函数不依赖Worker对象状态,避免了对象析构后访问非法内存的问题。这也是实际工程中推荐的做法。
四、在算法与STL中的典型用法
lambda最常见的用途是作为标准算法的谓词或比较器。例如使用 std::sort 按自定义规则排序,或者利用 std::for_each 遍历处理元素。它让算法调用处的逻辑一目了然,不需要跳转到其他函数定义。
下面例子对一个结构体数组按年龄排序:
#include <iostream>
#include <vector>
#include <algorithm>
struct Person {
std::string name;
int age;
};
int main() {
std::vector<Person> people = {
{"Alice", 30},
{"Bob", 25},
{"Carl", 35}
};
std::sort(people.begin(), people.end(),
[](const Person& a, const Person& b) {
return a.age < b.age;
});
for (const auto& p : people) {
std::cout << p.name << ": " << p.age << std::endl;
}
return 0;
}
除了排序,lambda也常与 std::find_if 配合查找元素。相比写独立谓词函数,lambda让条件就写在调用处,可读性和维护性都更好。
例如查找第一个年龄大于30的人:
#include <iostream>
#include <vector>
#include <algorithm>
struct Person {
std::string name;
int age;
};
int main() {
std::vector<Person> people = {
{"Alice", 30},
{"Bob", 25},
{"Carl", 35}
};
auto it = std::find_if(people.begin(), people.end(),
[](const Person& p) {
return p.age > 30;
});
if (it != people.end()) {
std::cout << "found: " << it->name << std::endl;
}
return 0;
}
五、初始化捕获与通用细节
C++14引入了初始化捕获,允许在捕获列表中定义新变量并移动或拷贝外部资源,语法如 [id = expr]。这对捕获只能移动的对象(如 std::unique_ptr )非常有用,也支持以不同名字在lambda内引用捕获值。
示例展示移动捕获:
#include <iostream>
#include <memory>
int main() {
auto ptr = std::make_unique<int>(99);
auto lambda = [p = std::move(ptr)]() {
if (p) {
std::cout << *p << std::endl;
}
};
lambda();
return 0;
}
另外,lambda可以赋值给 std::function 或 auto变量,但不同类型lambda之间不能直接赋值,因为它们的闭包类型互不相同。若需统一接口,应使用 std::function 包装,但这会带来轻微的性能开销。
总体而言,理解捕获列表是掌握lambda的关键。值捕获安全但可能拷贝昂贵,引用捕获高效但要小心生命周期,初始化捕获则补齐了移动语义的缺口。合理选用这些特性,才能写出既简洁又稳健的C++代码。
C++_lambda匿名函数捕获列表修改时间:2026-08-01 17:39:34