C++中的lambda表达式是定义匿名函数的便捷方式,而变量捕获是lambda表达式的核心特性之一,它让lambda可以访问外部作用域的变量,这种结合外部变量的lambda本质上就是C++中的闭包。理解变量捕获的规则和闭包的实现机制,能让我们更灵活地使用lambda处理各类逻辑。

lambda变量捕获的基本方式
值捕获
值捕获会在lambda定义时,拷贝外部变量的值到lambda内部,后续外部变量的修改不会影响lambda内捕获的值。值捕获的变量默认是只读的,如果需要修改,要在lambda参数列表后加mutable关键字。
#include <iostream>
int main() {
int a = 10;
// 值捕获a,默认不可修改捕获的值
auto lambda1 = [a]() {
// a = 20; // 报错,值捕获的变量默认只读
std::cout << "值捕获的a: " << a << std::endl;
};
a = 30; // 修改外部a的值
lambda1(); // 输出10,不受外部修改影响
// 加mutable后可以修改捕获的值,但修改的是拷贝,不影响外部
auto lambda2 = [a]() mutable {
a = 40;
std::cout << "mutable修改后的值捕获a: " << a << std::endl;
};
lambda2(); // 输出40
std::cout << "外部a的值: " << a << std::endl; // 输出30,外部不受影响
return 0;
}
引用捕获
引用捕获会让lambda内部持有外部变量的引用,lambda内对捕获变量的修改会直接影响外部变量,且外部变量的修改也会同步反映到lambda内部。需要注意引用捕获的生命周期问题,避免捕获已经销毁的局部变量。
#include <iostream>
int main() {
int b = 20;
// 引用捕获b
auto lambda3 = [&b]() {
b = 50; // 修改捕获的引用,会影响外部b
std::cout << "引用捕获修改后的b: " << b << std::endl;
};
lambda3(); // 输出50
std::cout << "外部b的值: " << b << std::endl; // 输出50,外部被修改
return 0;
}
混合捕获与隐式捕获
我们可以同时混合使用值捕获和引用捕获,也可以让编译器自动推断捕获方式,也就是隐式捕获。隐式值捕获用[=],隐式引用捕获用[&]。
#include <iostream>
int main() {
int x = 1, y = 2;
// 隐式值捕获所有外部变量
auto lambda4 = [=]() {
std::cout << "隐式值捕获x: " << x << ", y: " << y << std::endl;
};
// 隐式引用捕获所有外部变量
auto lambda5 = [&]() {
x = 10;
y = 20;
std::cout << "隐式引用捕获修改后x: " << x << ", y: " << y << std::endl;
};
lambda4(); // 输出1,2
lambda5(); // 输出10,20
std::cout << "外部x: " << x << ", y: " << y << std::endl; // 输出10,20
return 0;
}
C++11及之后的新捕获方式
初始化捕获(C++14)
C++14引入了初始化捕获,允许我们在捕获时定义新的变量,用任意表达式初始化,解决了之前只能捕获外部已有变量的限制,也可以用来捕获只能移动的对象。
#include <iostream>
#include <memory>
int main() {
int m = 5;
// 初始化捕获,定义新变量n,值为m+3
auto lambda6 = [n = m + 3]() {
std::cout << "初始化捕获的n: " << n << std::endl;
};
lambda6(); // 输出8
// 捕获只能移动的对象
std::unique_ptr<int> ptr = std::make_unique<int>(100);
auto lambda7 = [p = std::move(ptr)]() {
std::cout << "捕获的unique_ptr指向的值: " << *p << std::endl;
};
lambda7(); // 输出100
return 0;
}
广义lambda捕获(C++14)
广义lambda捕获还支持捕获this指针,在类的成员函数中使用lambda时,捕获this可以访问类的成员变量和成员函数,本质也是引用捕获,需要注意this的生命周期。
#include <iostream>
class Test {
public:
int val = 10;
void func() {
// 捕获this,访问成员变量val
auto lambda = [this]() {
std::cout << "类成员val: " << val << std::endl;
val = 20; // 修改成员变量
};
lambda();
std::cout << "修改后的val: " << val << std::endl;
}
};
int main() {
Test t;
t.func(); // 先输出10,再输出20
return 0;
}
C++闭包的实现机制
C++的闭包本质上是编译器为lambda表达式生成的一个匿名类的实例对象。这个匿名类会重载operator()函数,让对象可以像函数一样被调用。捕获的变量会成为这个匿名类的成员变量,值捕获的变量是拷贝构造的成员变量,引用捕获的变量是引用类型的成员变量。
比如下面的lambda:
int a = 10;
auto lambda = [a]() { return a + 1; };
编译器会大致生成如下的匿名类:
class AnonymousClass {
private:
int a; // 值捕获的变量作为成员变量
public:
AnonymousClass(int a_) : a(a_) {} // 构造函数初始化捕获的变量
int operator()() const { // 重载调用运算符,默认是const的,所以值捕获变量不可修改
return a + 1;
}
};
// lambda的定义等价于
AnonymousClass lambda(a);
当lambda被调用时,就是调用这个匿名类对象的operator()方法。如果lambda有mutable关键字,那么operator()就不会被声明为const,就可以修改值捕获的成员变量了。
捕获方式的选用建议
- 如果不需要修改外部变量,且变量生命周期足够长,优先使用值捕获,避免引用捕获带来的生命周期问题。
- 如果需要修改外部变量,或者捕获的变量较大,拷贝成本高,可以使用引用捕获,但要确保lambda调用时外部变量还存在。
- 在类的成员函数中使用lambda访问成员变量时,要谨慎捕获this,避免lambda的生命周期超过类的实例,导致悬空引用。
- 对于只能移动的对象,使用C++14的初始化捕获来转移所有权,避免拷贝错误。
常见问题解答
为什么值捕获的变量默认不能修改?
因为lambda生成的匿名类的operator()默认是const成员函数,const成员函数不能修改非mutable的成员变量,所以值捕获的变量默认只读,加mutable后operator()就不是const的了,就可以修改捕获的拷贝值。
引用捕获会不会有风险?
会有风险,如果lambda的生命周期超过了被捕获的引用对应的变量的生命周期,那么lambda调用时访问的就是已经销毁的内存,会导致未定义行为。比如不要在函数内返回捕获了局部变量引用的lambda。
// 错误示例:返回捕获局部变量引用的lambda
auto get_lambda() {
int local = 10;
return [&local]() { return local; }; // local在函数结束后销毁,引用悬空
}
隐式捕获有什么注意事项?
隐式捕获[=]和[&]会捕获所有用到的外部变量,可能会不小心捕获不需要的变量,尤其是[&]隐式引用捕获,容易引入生命周期问题,建议尽量显式指定需要捕获的变量,提高代码的可读性和安全性。