【C++11】Lambda 表达式详细讲解(语法、捕获列表、mutable 关键字、返回值规则和常见避坑点) 文章目录前言一、Lambda 是什么二、基本语法1. 完整格式2. 简单例子三、捕获列表重点难点1. 值捕获拷贝变量2. 引用捕获变量的别名3 隐式捕获让编译器自己找变量4. 混合捕获灵活搭配5. 捕获 this 指针四、mutable 关键字五、返回值规则1. 多个 return 且类型不一致2 空函数体或者复杂逻辑六、Lambda 的本质是什么七、典型使用场景1. 配合 STL 算法八、常见坑点与避坑1. 悬空引用重点引用捕获的变量被销毁了2. 值捕获是「当时拷贝」3. 全局变量 / 静态变量不用捕获4. lambda 之间不能赋值总结前言在 C11 之前写回调、配合 STL 算法的时候哪怕只有一两行逻辑也得单独写个函数或者仿函数。名字想半天定义还得跑到函数外面代码拆得七零八落。C11 引入的lambda 表达式也叫匿名函数就是来解决这个问题的就地定义、就地使用短小的逻辑直接写在调用的地方代码紧凑又好读。一、Lambda 是什么lambda 本质就是一个没有名字的临时函数。用完就丢不用额外起名字也不用跑到外面定义。举个最直观的对比遍历数组打印每个元素。不用 lambda 的写法#includeiostream#includevector#includealgorithmusingnamespacestd;voidPrint(intx){coutx ;}intmain(){vectorintv{1,2,3,4,5};// for_each遍历迭代器区间内每一个元素把每个元素传给可调用对象执行一遍for_each(v.begin(),v.end(),Print);return0;}用 lambda 的写法#includeiostream#includevector#includealgorithmusingnamespacestd;intmain(){vectorintv{1,2,3,4,5};// for_each遍历迭代器区间内每一个元素把每个元素传给可调用对象执行一遍for_each(v.begin(),v.end(),[](intx){coutx ;});return0;}逻辑直接写在调用的地方不用单独定义函数不用想函数名阅读的时候一眼就能看到做了什么。二、基本语法1. 完整格式[捕获列表](参数列表)mutable-返回值类型{函数体}各个部分说明[捕获列表]控制 lambda 能不能用外面的变量、怎么用。必须写就算空的也要写[]。(参数列表)和普通函数的形参一样。没有参数可以省略不写。mutable可选。值捕获的变量默认是 const 的加了才能修改副本。- 返回值类型可选。大部分情况编译器能自动推导不用写。{函数体}逻辑代码。2. 简单例子1. 无参无返回值autohello[](){couthello lambdaendl;};hello();// 调用2. 带参数autoadd[](inta,intb){returnab;};coutadd(1,2)endl;3. 显式指定返回值autofunc[](intx)-double{if(x0)returnx*1.5;elsereturnx*2.0;};三、捕获列表重点难点lambda 里想用外部作用域的变量必须在捕获列表里声明不然访问不到。这是 lambda 最核心也最容易搞混的部分。1. 值捕获拷贝变量写法[变量名]或者[]隐式捕获所有用到的变量。特点lambda 里得到的是副本不是同一个变量默认是 const 的不能修改想改要加mutable后面讲捕获的时候就拷贝好了后面外面变量再改变lambda 里也不受影响#includeiostreamusingnamespacestd;intmain(){inta10;// 值捕获 aautofunc[a](){coutaendl;// 读没问题//a 20; // 编译报错值捕获默认 const};func();// 输出 10a20;func();// 还是输出 10因为捕获的时候就拷贝了return0;}2. 引用捕获变量的别名写法[变量名]或者[]隐式捕获所有用到的变量。特点lambda 里用的就是外面那个变量本身改了 lambda 里的值外面也跟着变不存在 const 限制该怎么改怎么改#includeiostreamusingnamespacestd;intmain(){inta10;// 引用捕获 aautofunc[a](){a20;// 改变a的值};func();coutaendl;// 输出 20外面也变了return0;}3 隐式捕获让编译器自己找变量一个个写捕获变量太麻烦可以让编译器根据 lambda 里用到的变量自动捕获[]所有用到的外部变量全部值捕获[]所有用到的外部变量全部引用捕获#includeiostreamusingnamespacestd;intmain(){inta1,b2,c3;// 隐式值捕获autoadd1[](){returnabc;};// 隐式引用捕获autoadd2[](){a;b;c;returnabc;};intresult1add1();intresult2add2();coutresult1endl;coutresult2endl;return0;}4. 混合捕获灵活搭配允许大部分值捕获个别变量单独引用捕获或者大部分引用捕获个别变量单独值捕获#includeiostreamusingnamespacestd;intmain(){inta1,b2,c3;// 整体值捕获a 单独引用捕获autofunc1[,a](){a10;// a 是引用捕获里面改变外面也改变// b 20; // b 是值捕获const 改不了// c 30; // c 是值捕获};// 整体引用捕获b 单独值捕获autofunc2[,b](){a10;// a 是引用捕获里面改变外面也改变//b 20; // b 是值捕获const 改不了c30;// c 是引用捕获};return0;}注意隐式符号只能写一个要么要么剩下的单独指定相反的捕获方式。5. 捕获 this 指针在类的成员函数里写 lambda想访问类的成员变量 / 成员函数捕获this指针就行。#includeiostreamusingnamespacestd;classTest{public:voidPrint(){// 捕获 this就能用 _numautofunc[this](){cout_numendl;};func();}private:int_num100;};intmain(){Test test;test.Print();return0;}四、mutable 关键字值捕获的变量默认是 const 的想修改这个副本需要加mutable。#includeiostreamusingnamespacestd;intmain(){intcnt0;// 加 mutable 才能修改值捕获的 cntautoinc[cnt]()mutable{cnt;coutcntendl;};inc();// 1inc();// 2coutcntendl;// 外面还是 0改的是副本return0;}注意mutable 改的只是 lambda 内部的副本外面的变量不受影响。为什么第二次inc();的结果是2原因lambda 对象 inc 创建后值捕获的 cnt 被拷贝到 lambda 内部成为 lambda 的成员变量内部的cnt副本会保留状态之后每次调用 inc()都是复用同一个 inc 对象操作它里面保存的那份副本不会再次从外部拷贝。五、返回值规则lambda 的返回值大部分时候不用写编译器自动推导。但有两种情况建议显式写。1. 多个 return 且类型不一致如果函数体里有多个 return返回的类型不一样编译器可能推不出来或者推出来的不是你想要的。显式指定更稳妥。autofunc[](intx)-double{if(x0)returnx*1.5;// doubleelsereturnx;// int};2 空函数体或者复杂逻辑有时候逻辑复杂编译器不好推导显式写- 返回类型也更清晰。六、Lambda 的本质是什么lambda 不复杂编译器看到 lambda就会自动生成一个匿名的仿函数类捕获的变量变成类的成员变量重载operator()函数体就是 operator () 的实现所以每个 lambda 的类型都是唯一的哪怕两个 lambda 长得一模一样类型也不同。所以不能互相赋值。值捕获默认 const因为生成的 operator () 是 const 成员函数。加了 mutable 就变成非 const 的。lambda 的大小就是捕获变量的大小之和。#includeiostreamusingnamespacestd;intmain(){inta0;doubleb0;autofunc[a,b](){};coutsizeof(func)endl;// 16就是 int double 的大小return0;}想要把这个 lambda 存储起来怎么办需要用包装器std::function#includeiostream#includefunctionalusingnamespacestd;functionint(int,int)add[](inta,intb){returnab;};intmain(){intretadd(1,2);coutretendl;return0;}七、典型使用场景1. 配合 STL 算法这是 lambda 最常用的地方自定义排序、查找条件、遍历操作都特别方便。自定义排序#includeiostream#includevector#includealgorithmusingnamespacestd;intmain(){vectorintv{3,1,4,2,5};// 降序排序sort(v.begin(),v.end(),[](inta,intb){returnab;});for(autoe:v){coute ;}coutendl;return0;}条件查找#includeiostream#includevector#includealgorithmusingnamespacestd;intmain(){vectorintv{3,1,4,2,5};// 找第一个大于 3 的数// find_if遍历迭代器区间元素依次把元素传给可调用对象遇到第一个返回 true 的元素就返回否则返回 end()autoitfind_if(v.begin(),v.end(),[](intx){returnx3;});cout*itendl;// 4return0;}遍历操作#includeiostream#includevector#includealgorithmusingnamespacestd;intmain(){vectorintv{3,1,4,2,5};// for_each遍历迭代器区间内每一个元素把每个元素传给你给的可调用对象执行一遍for_each(v.begin(),v.end(),[](intx){x*2;// 每个元素乘 2});for(autoe:v){coute ;}coutendl;return0;}八、常见坑点与避坑1. 悬空引用重点引用捕获的变量被销毁了引用捕获的是局部变量函数结束变量销毁了再调用 lambda 就会访问非法内存。反面例子#includeiostreamusingnamespacestd;autoGetLambda(){intx10;return[x](){// 捕获局部变量的引用coutxendl;};}intmain(){autofuncGetLambda();func();// 运行崩溃或者未定义行为因为 x 已经销毁了return0;}能值捕获就别引用捕获必须引用捕获的一定要保证变量生命周期比 lambda 长。2. 值捕获是「当时拷贝」捕获那一刻就拷贝好了后面外面变量再变lambda 里的不会跟着变。#includeiostreamusingnamespacestd;intmain(){inta10;autofunc[a](){coutaendl;};a20;func();// 输出 10不是 20return0;}3. 全局变量 / 静态变量不用捕获全局变量、静态变量不在栈上lambda 直接就能用不用捕获。4. lambda 之间不能赋值每个 lambda 类型都是编译器生成的唯一类型两个 lambda 不能互相赋值。想统一存储用包装器std::function。总结lambda 是匿名函数就地定义就地用适合短小逻辑。捕获列表控制外部变量的访问方式值捕获是副本引用捕获是别名。mutable用来修改值捕获的副本不影响外部变量。返回值一般自动推导多 return 且类型不同时就建议显式指定。本质是编译器生成的匿名仿函数类每个 lambda 类型唯一。坑点悬空引用、值捕获时机、类型不可赋值。lambda 是 C11 非常实用的特性写算法、写回调、封装逻辑都离不开。把上面这些点搞懂日常使用就基本没问题了。