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