C++ Lambda 表达式中的 [] —— 捕获列表详解

一、先看全貌:Lambda 的完整语法

[捕获列表](参数列表) -> 返回类型 { 函数体 }
  ↑         ↑           ↑          ↑
  ①         ②           ③          ④
部分 作用 是否必须
[] 捕获列表 决定 lambda 能"记住"外部哪些变量 ✅ 必须
() 参数列表 调用 lambda 时传入的参数 可省略
-> 返回类型 指定返回值类型 可省略(自动推导)
{} 函数体 具体的代码逻辑 ✅ 必须

[] 就是 C++11 引入 Lambda 表达式时的专属语法,叫做"捕获列表"(Capture List)。
它是 lambda 的"身份标志"——编译器看到 [] 就知道"这是一个 lambda"。


二、为什么需要捕获列表?

Lambda 本质上是一个匿名函数,它定义在某个作用域内部。问题来了:

int x = 10;
int y = 20;

// 我想在 lambda 里用 x 和 y,怎么办?
auto fn = [???](int z) {
    return x + y + z;  // x 和 y 不是参数,是外部变量!
};

普通函数做不到这一点:

// ❌ 普通函数无法直接访问定义它时周围的局部变量
int x = 10;
void fn(int z) {
    std::cout << x + z;  // 如果 x 是局部变量,这里编译不过
}

[] 就是告诉编译器:我要把外部的哪些变量"抓进来"带在身上。


三、捕获列表的所有写法 📋

3.1 [] —— 什么都不捕获

int x = 10;

auto fn = []() {
    // std::cout << x;  // ❌ 编译错误!没有捕获 x
    std::cout << "我不需要外部变量" << std::endl;
};

fn(); // ✅

相当于一个完全独立的普通函数,和外界没有任何关系。


3.2 [x] —— 按值捕获指定变量

int x = 10;
int y = 20;

auto fn = [x]() {
    std::cout << x << std::endl;  // ✅ 可以用 x
    // std::cout << y;             // ❌ 没有捕获 y
};

x = 999;   // 修改原始的 x
fn();       // 输出 10,不是 999!
            // 因为捕获的是当时 x 的"副本"

比喻:拍了一张 x 的照片📸,之后原来的 x 怎么变,照片不会变。

注意:按值捕获的变量默认是 const 的,不能修改:

int x = 10;

auto fn = [x]() {
    // x++;  // ❌ 编译错误!按值捕获的变量默认是 const
    std::cout << x;
};

想修改副本?加 mutable

int x = 10;

auto fn = [x]() mutable {   // 加了 mutable
    x++;                      // ✅ 可以修改副本了
    std::cout << x;           // 11
};

fn();
std::cout << x;  // 10 ← 原始的 x 不受影响,改的只是副本

3.3 [&x] —— 按引用捕获指定变量

int x = 10;

auto fn = [&x]() {
    x++;                      // ✅ 直接修改原始的 x
    std::cout << x;
};

fn();                         // 输出 11
std::cout << x;               // 11 ← 原始的 x 也变了!

比喻:不是拍照,而是拿了一根绳子🪢绑着 x。你拉绳子,x 就跟着动。


3.4 [=] —— 按值捕获所有外部变量

int x = 10;
int y = 20;
int z = 30;

auto fn = [=]() {
    std::cout << x + y + z;   // ✅ 全都能用,全是副本
};

fn(); // 60

相当于:把周围所有变量都拍了一张照片。


3.5 [&] —— 按引用捕获所有外部变量

int x = 10;
int y = 20;

auto fn = [&]() {
    x++;
    y++;                       // ✅ 全都能改,全是引用
};

fn();
std::cout << x << " " << y;   // 11 21

⚠️ JavaScript 的闭包行为就等价于这个 [&] 这就是前面博客里说的。


3.6 混合捕获 —— 灵活组合

int a = 1, b = 2, c = 3;

// a 按值捕获,b 按引用捕获,c 不捕获
auto fn1 = [a, &b]() {
    std::cout << a << b;
    // std::cout << c;  // ❌ 没捕获 c
};

// 默认按值捕获所有,但 b 按引用
auto fn2 = [=, &b]() {
    std::cout << a << b << c;  // ✅ a, c 是副本,b 是引用
};

// 默认按引用捕获所有,但 a 按值
auto fn3 = [&, a]() {
    std::cout << a << b << c;  // ✅ a 是副本,b, c 是引用
};

四、完整对照表 📊

写法 含义 能否修改原始变量
[] 什么都不捕获
[x] 按值捕获 x(拷贝)
[x]() mutable 按值捕获 x(可改副本) ❌(改的是副本)
[&x] 按引用捕获 x
[=] 按值捕获所有
[&] 按引用捕获所有
[=, &x] 默认按值,x 按引用 只有 x ✅
[&, x] 默认按引用,x 按值 除了 x 都 ✅

五、回到你问的代码 🔍

std::function<int(int)> createAdder(int x) {
    return [x](int y) {       // ← 这里
        return x + y;
    };
}

逐层拆解:

[x]         →  按值捕获外部变量 x(拷贝一份 x 带走)
(int y)     →  调用这个 lambda 时传入一个 int 参数 y
{ return x + y; }  →  函数体:返回 x + y

执行流程:

auto add10 = createAdder(10);
//   1. 调用 createAdder,参数 x = 10
//   2. 创建 lambda,按值捕获 x,lambda 内部保存了 x 的副本 = 10
//   3. createAdder 返回这个 lambda
//   4. 此时 createAdder 已经结束了,参数 x 被销毁
//   5. 但没关系!lambda 里保存的是副本,不受影响

add10(5);   // lambda 执行:副本 x(10) + y(5) = 15 ✅
add10(20);  // lambda 执行:副本 x(10) + y(20) = 30 ✅

这里为什么用 [x](值捕获)而不是 [&x](引用捕获)?

// ❌ 如果用引用捕获:
std::function<int(int)> createAdder(int x) {
    return [&x](int y) {     // 引用捕获 x
        return x + y;
    };
}   // ← 函数结束,x 被销毁了!

auto add10 = createAdder(10);
add10(5);   // 💥 未定义行为!x 已经不存在了,引用指向了一块废弃的内存

⚠️ 如果 lambda 要"活过"外部函数的生命周期(被 return 出去),必须用值捕获 [x],不能用引用捕获 [&x] 这是 C++ 程序员必须时刻注意的。

而 JavaScript 不需要担心这个问题——垃圾回收器会保证被闭包引用的变量不被销毁。


六、编译器在背后做了什么? 🛠️

当你写下:

int x = 10;
auto fn = [x](int y) { return x + y; };

编译器实际上帮你生成了一个匿名类

// 编译器在背后自动生成的(大致等价):
class __lambda_at_line_2 {
private:
    int x;                          // 值捕获 → 成员变量(副本)

public:
    __lambda_at_line_2(int _x) : x(_x) {}   // 构造时保存 x

    int operator()(int y) const {    // 重载 () 运算符
        return x + y;
    }
};

// 你写的 auto fn = [x](int y) {...}; 等价于:
__lambda_at_line_2 fn(x);    // 构造对象,把 x 拷贝进去
fn(5);                        // 调用 fn.operator()(5) → 10 + 5 = 15

如果是引用捕获 [&x]

class __lambda_ref {
private:
    int& x;                         // 引用捕获 → 引用成员

public:
    __lambda_ref(int& _x) : x(_x) {}

    int operator()(int y) const {
        return x + y;
    }
};

所以 [] 捕获列表本质上就是告诉编译器:给自动生成的匿名类添加什么样的成员变量。


七、一句话总结 💡

[] 是 C++ lambda 表达式的"捕获列表",它决定了 lambda 能"记住"外部的哪些变量、以什么方式记住(拷贝还是引用)。这就是 C++ 版的"闭包"机制。

后记

2026年4月16日14点53分于上海,在opus 4.6辅助下完成。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐