目录
1. 整体学习思维导图
2. {}列表初始化
2.1 单个对象情况
2.2 多对象情况
3. 右值引用和移动语义
3.1 左值和右值
3.2 左值引用和右值引用
3.3 引用延迟生命周期
3.4 左值和右值的参数匹配
4. 左值引用和右值引用
4.1 左值引用
4.2 右值引用
5. 移动构造和移动赋值
6. 类型划分
7. 引用折叠
8. 完美转发forward
9. 可变参数模版
9.1 函数重载和模版
9.2 可变参数模版语法和原理
9.3 包扩展
9.4 emplace系列和push/insert系列的区别
10. 类的新功能
10.1 默认的移动构造和移动赋值
10.2 成员变量给缺省值
10.3 default和delete
10.4 final和override
10.5 C++11后STL的变化
11. lambda
11.1 lambda的基本了解和语法
11.2 捕获列表
11.3 lambda的原理
12. 包装器
13. bind
1. 整体学习思维导图
2. {}列表初始化
2.1 单个对象情况
-
在C++11发布之前,我们的{}用于初始化的地方很少,一般在数组和结构体使用。
// 结构体
struct S
{int x;int y;
};// C++98 {} -> 初始化数组和结构体
int array1[] = { 1,2,3,4,5 };
int array2[5] = { 0 };
S object = { 1, 2 };
-
C++11以后想统⼀初始化⽅式,试图实现⼀切对象皆可用{}初始化,{}初始化也叫做列表初始化。
-
内置类型⽀持,自定义类型也⽀持,⾃定义类型本质是类型转换,中间会产生临时对象,最后优化了以后变成直接构造。
-
{}初始化的过程中,可以省略掉=
-
C++11列表初始化的本意是想实现⼀个大统⼀的初始化⽅式,其次他在有些场景下带来的不少便利,如容器push/insert多参数构造的对象时,{}初始化可以避免匿名对象和有名对象的创建。
/* 以下两句代码的初始化都是正确的,只是语法层面上存在差异,底层是一样的 */
int a1 = { 1 };
int a2 = 1;
/* 原本{ 2024, 12, 9 }会被构造成一个临时对象,然后再拷贝构造给d1,编译器优化后直接构造 */
// 一切对象皆可初始化
Date d1 = { 2024, 12, 9 };// 获取构造的临时对象,const的左值引用是为了满足权限问题
const Date& d2 = { 2024, 12, 9 };// 初始化可以省略 =
Date d1{ 2024, 12, 9 };// 由于全缺省参数,我们可以单参数构造
Date d1 = {2024};// 可以通过隐式类型转换减少有名/匿名对象的使用
// 这边{ 2024, 12, 9 }构造成一个Date对象,进行插入到vector
vector<Date> v;
v.push_back({ 2024, 12, 9 });
2.2 多对象情况
以上Date类型的{}列表初始化都是针对于一个对象,如果是vector<Date>呢,那么{}又如何实现列表初始化。
-
C++11库中提出了⼀个std::initializer_list的类, auto il = { 10, 20, 30 }; auto -> the type of il is an initializer_list ,这个类的本质是底层开⼀个数组,将数据拷贝过来,std::initializer_list内部有两个指针分别指向数组的开始和结束用于管理这块区域。
内部实现:
template<class T>
class vector
{
public: typedef T* iterator; vector(initializer_list<T> l) { for (auto e : l) push_back(e) }
private: iterator _start = nullptr; iterator _finish = nullptr; iterator _endofstorage = nullptr;
};
/* mylist中只存储了两个指针,x64环境一个指针8字节 */
std::initializer_list<int> mylist;
mylist = { 10, 20, 30, 40 };
cout << sizeof(mylist) << endl;
/* 直接构造 */
vector<int> v1({ 1,2,3,4,5 });
/* 构造临时对象+拷贝构造 -> 优化为直接构造*/
vector<int> v2 = { 1,2,3,4,5 };
/* 构造+赋值重载 */
vector<int> v3;
v3 = {1, 2, 3, 4, 5};
/* pair构造+{}列表初始化 */
map<string, string> dict{ {"string", "字符串"}, {"left", "左边"} };
/* 返回参数构造 */
vector<int> smallestK(vector<int>& arr, int k) {// 获取随机种子srand(time(NULL));mysqort(arr, 0, arr.size() - 1, k);return {arr.begin(), arr.begin() + k};}
3. 右值引用和移动语义
在之前的学习中我们了解过引用的语法,如:
int b = 0;
int& a = b;
a就是b的引用,相当于取别名,在C++11之后Type& x称作为左值引用,Type&& y称作为右值引用,但是本质没变,无论是左值引用还是右值引用都是取别名。
3.1 左值和右值
-
左值可以称作为lvalue(loacte value),寓意可以存储在内存中,有着明确地址的对象
-
右值被称作为rvalue(read value),寓意可以提供数据值,但是不可以寻找到地址的,例如:临时变量,字面量常量,存储在寄存器中的变量
-
左值和右值的核心区别在于能否取地址
// 常见的左值
// 左值:可以取地址
// 以下的p、b、c、*p、s、s[0]就是常见的左值
int* p = new int(0);
int b = 1;
const int c = b;
*p = 10;
string s("111111");
/* s[0]返回值是char& */
s[0] = 'x';// 右值:不能取地址
double x = 1.1, y = 2.2;
// 以下⼏个10、x + y、fmin(x, y)、string("11111")都是常⻅的右值
10;
x + y;
/* 该函数返回的是一个double类型,意味着中间会产生临时变量 */
fmin(x, y);
string("11111");
3.2 左值引用和右值引用
-
左值引用就是Type& r1 = x; 给左值取别名
-
右值引用就是Type&& r2 = y; 给右值取别名
-
左值引用如果想要引用右值需要+const修饰(右值通常是不可以修改的,如果想要使用左值引用需要考虑权限缩放问题)
const int& r1 = 1 + 1;
-
右值引用不可以引用左值,但是可以引用move(左值)->涉及引用折叠,可以先理解成强转(Type&&)左值
-
一个右值引用如果绑定了右值,那么这个右值引用的变量就是左值
int a = 0;
int& b = a;
int&& c = 1 + 1;
cout << &b << endl;
cout << &c << endl;
// c是右值引用的变量表达式,一旦绑定了右值(1+1),那么这个变量表达式(c)就是左值,可以取地址
// 右值引用变量c和左值引用变量b都是左值!
-
语法层面上,左值引用和右值引用都不开空间
// 左值引⽤给左值取别名
int& r1 = b;
int*& r2 = p;
int& r3 = *p;
string& r4 = s;
char& r5 = s[0]; // 右值引⽤给右值取别名
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
string&& rr4 = string("11111"); // 左值引⽤引用右值加上const修饰(权限缩放问题)
const int& rr1 = 10;
const double& rr2 = x + y;
const double& rr3 = fmin(x, y);
const string& rr4 = string("11111"); // 右值引⽤引用左值使用move(引用折叠,强转)
int&& r1 = move(b);
int*&& r2 = move(p);
int&& r3 = move(*p);
string&& r4 = move(s);
char&& r5 = move(s[0]); // 这⾥要注意的是,右值引用变量rr1的属性是左值,所以不能再被右值引⽤绑定,除⾮move⼀下
int&& rr1 = 10;
// int&& rr2 = rr1; err
int&& rr2 = move(rr1);
3.3 引用延迟生命周期
-
无论是左值引用还是右值引用都可以延长临时变量的生命周期,唯一区别在于左值引用无法修改,右值引用可以修改,注意这种延长生命周期是不可以将变量从一个栈帧延长到另一个栈帧不销毁!
// 临时变量的生命周期只在当前行
// 延长生命周期
const int& x = 1 + 1; /* 左值引用 */
int&& y = 1 + 1; /* 右值引用 */
// cout << x++ << endl; // err
cout << ++y << endl;
3.4 左值和右值的参数匹配
-
在C++98 我们可以使用const Type& 用于实现左值和右值的参数匹配
-
C++11后,分别重载左值引⽤、const左值引用、右值引⽤作为形参的f函数,那么实参是左值会匹配f(左值引用),实参是const左值会匹配f(const左值引用),实参是右值会匹配f(右值引用),函数重载的参数会匹配最适合他的参数,但是如果没有右值引用的函数重载,那么右值会走const Type&。
void f(int& x)
{std::cout << "左值引用重载 f(" << x << ")\n";
} void f(const int& x)
{std::cout << "到 const 的左值引用重载 f(" << x << ")\n";
} void f(int&& x)
{std::cout << "右值引用重载 f(" << x << ")\n";
}
-
f(r1)->(引用左值) 的类型为int&
-
f(r2)->(引用左值) 的类型为const int&
-
f(r3)->(引用右值) 的类型为const int& -> C++11之前右值的匹配情况
-
f(r4)->(引用右值) 的类型为int&&,但是r4作为右值引用的变量是左值
-
f(1+1)->(引用右值) 的类型为int&&
4. 左值引用和右值引用
4.1 左值引用
-
减少形参的拷贝,改变实参,类似于指针的作用,但是比指针安全!
void fun(int x1, int x2) // 形参会产生拷贝
{}void fun(int& x1, int& x2) // 取别名
{}
-
返回实参,减少(临时对象+拷贝)/以供修改,如map的operator[]实现
V& operator[](const K& key)
{pair<Iterator, bool> ret = _tb.Insert({ key, V() });return ret.first->second;
}
4.2 右值引用
我们看以下代码模块:
首先是这个代码模块不可以使用左值引用返回!以addStrings举例:
所以这种情况不可以返回一个左值引用!
/* 这块代码是一个字符串的相加 */
class Solution {
public:// 传值返回需要拷⻉string addStrings(string num1, string num2) {string str;int end1 = num1.size() - 1, end2 = num2.size() - 1;// 进位int next = 0;while (end1 >= 0 || end2 >= 0){int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;int ret = val1 + val2 + next;next = ret / 10;ret = ret % 10;str += ('0' + ret);}if (next == 1)str += '1';reverse(str.begin(), str.end());return str;}
};
那么能否返回一个右值引用,我们知道在函数栈帧中的str如果要返回,会先拷贝给一个临时对象,然后临时对象在拷贝给接收参数,我们先前知道了右值引用可以延长临时对象的生命周期,那么使用string&& 做返回值是否可行呢?
我们可以看见,使用右值引用可以给临时对象延长生命周期,但是str属于该函数栈帧,一旦函数运行结束栈帧销毁空间释放,如果使用右值引用可以强行留下该str空间不释放的话可能会带来内存泄漏的问题!所以此处不可以使用右值引用!
5. 移动构造和移动赋值
string(string&& s)
{cout << "string(string&& s) -- 移动构造" << endl;// 转移掠夺你的资源swap(s);
}string& operator=(string&& s)
{cout << "string& operator=(string&& s) -- 移动赋值" << endl;swap(s);return *this;
}void swap(string& ss)
{::swap(_str, ss._str);::swap(_size, ss._size);::swap(_capacity, ss._capacity);
}
-
移动构造是一种构造函数,移动构造的第一个参数(this)必须要求是该类类型的引用,但是不同的是要求这个参数是右值引用。
-
移动赋值也是如此
-
这两种函数的本质是掠夺资源,因为要用到swap,参数是左值引用,这也是为什么右值引用变量本身是左值的其中一个原因。
有了移动构造和移动赋值之后,你函数栈帧的str该销毁就销毁,但是你的资源和我的ret交换,减少了拷贝的开销,提升了效率!
ouyang::string s1("11111111111");
// 拷贝构造
/*ouyang::string s2 = s1;*/
// 移动构造
ouyang::string s3 = move(s1);
// 移动赋值
ouyang::string s4;
s4 = ouyang::string("2222222222");
为了清楚看到函数的调用,我们需要在Linux环境下去掉编译器优化的场景和非优化的场景进行对比演示!
g++ test.cpp -fno-elide-constructors // 关闭编译器优化
-
对比演示,只有拷贝构造,赋值重载的情况
去掉优化:
开启编译器优化:
-
对比演示,存在移动构造,和移动赋值
去掉优化:
开启编译器优化:
意义:即使我们使用的编译器没有优化,移动构造和移动赋值可以大大降低资源的开销,相较于拷贝和赋值提升了不少性能!左值引用和右值引用的最终目的是为了减少拷贝,提高效率。左值引用还可以修改参数和做返回值方便使用!对于深拷贝自定义对象,移动构造和移动赋值的实际运用意义很大,但是对于浅拷贝对象对于效率的影响不大!
6. 类型划分
C++11对左值和右值进行了以下划分(简单了解):
左值:有名字,没有被move
将亡值:有名字,已经被move的左值
右值:没有名字
7. 引用折叠
-
C++中不可以直接定义这样的变量,int& && r = 1; 会产生报错,但是如果是typedef和模版传参就会导致出现引用的引用,为了解决引用的引用不明确的情况,出现了引用折叠!
-
typedef
-
typedef int& lret;
typedef int&& llret;
int n = 0; // 一个左值(区别左右值就看能不能取地址)
lret& r1 = n; // int& & 折叠->int&
llret& r2 = n; // int&& & 折叠->int&
lret&& r3 = n; // int& && 折叠->int&
//llret&& r4 = n; // int&&&& 折叠->int&& //err
// 总结:只要存在左值引用折叠全是左值引用,需要两个右值引用折叠才是右值引用
-
模版传参
// 该模版由于引用折叠原因,只能实例化出int&
template<class T>
void fun1(T& x)
{}int main()
{int n = 0; // 一个左值(区别左右值就看能不能取地址)fun1<int>(n); //模版实例化为void fun1(int& x)//fun1<int>(1); // errfun1<int&>(n); //模版实例化为void fun1(int& x)//fun1<int&>(1);// errfun1<int&&>(n); //模版实例化为void fun1(int& x)return 0;
}// 由于引用折叠原因,该模版可以称作为万能引用
// 所传参数如果是int&就实例化为int&
// 所传参数如果是int&&就实例化为int&&
template<class T>
void fun2(T&& x)
{}fun2<int&>(n); //模版实例化为void fun2(int& x)
fun2<int&&>(1); //模版实例化为void fun2(int&& x)
引用折叠的意义:我们模版参数传入左值,就是左值引用模版,传入右值就是右值引用的模版。
template<class T>
void function(T&& t)
{int a = 0;T x = a;++x;cout << a << endl;cout << &a << endl;cout << &x << endl;
}int main()
{int n = 0;// n是左值,T的类型就是int&,模版引用折叠,实例化出void function(int& t)function(n);cout << endl;// 1是右值,T的类型就是int,模版引用折叠,实例化出void function(int&& t)function(1);cout << endl;// move(n)是右值,T的类型就是int, 模版引用折叠,实例化出void function(int&& t)function(std::move(n));cout << endl;const int a = 0;// a是左值,T的类型就是const int&, 模版引用折叠,实例化出void function(const int& t)function(a); // 不可以对const修饰的x++cout << endl;// move(a)是右值,T的类型就是const int, 模版引用折叠,实例化出void function(const int&& t)function(std::move(a)); // 不可以对const修饰的x++cout << endl;return 0;
}
8. 完美转发forward
在了解完美转发之前我们需要了解:无论是左值引用还是右值引用的变量本身都是一个左值,看以下代码为例:
// 函数重载
void fun(int& x)
{cout << "左值引用" << endl;
}
void fun(const int& x)
{cout << "const左值引用" << endl;
}
void fun(int&& x)
{cout << "右值引用" << endl;
}
void fun(const int&& x)
{cout << "const右值引用" << endl;
}// 万能引用
template<class T>
void function(T&& t)
{fun(t);
}int main()
{int a = 0;// a是左值,应该调用void fun(int& x)function(a);// move(a)是右值,应该调用void fun(int&& x)function(std::move(a));// 1是右值,应该调用void fun(int&& x)function(1);cout << endl;const int b = 0;// b是const左值,应该调用void fun(const int& x)function(b);// move(b)是const右值,应该调用void fun(const int&& x)function(std::move(b));return 0;
}
但是实际情况确是如上图,这是怎么回事呢?我们前面提到了右值引用变量本身是左值,这就会带来我们在函数体中再次调用时会带来退化情况,为了满足我们的要求不退化,就需要使用到完美转发forward!
// 万能引用
template<class T>
void function(T&& t)
{fun(forward<T>(t));
}
下面以List做场景看待,存在移动构造和移动赋值,但是没有完美转发,对于左值和右值是否能进行不同的处理?
左值原本处理过程: 构造+拷贝构造
右值原本处理过程: 构造+移动构造
list_node() = default;
template<class X>
list_node(X&& data):_data(forward<X>(data)), _next(nullptr), _prev(nullptr)
{}template<class X>
void push_back(X&& x)
{insert(end(), forward<X>(x));
}template<class X>
iterator insert(iterator pos, X&& x)
{Node* cur = pos._node;Node* prev = cur->_prev;Node* newnode = new Node(forward<X>(x));// prev newnode curnewnode->_next = cur;cur->_prev = newnode;newnode->_prev = prev;prev->_next = newnode;++_size;return newnode;
}
经过以上万能引用+完美转发的改造,可以让左/右值明确执行属于自己的过程!
9. 可变参数模版
9.1 函数重载和模版
在了解可变参数模版之前,我们先来温习一下函数重载和模版。
-
函数重载:可以让同名函数传入不同类型的值,函数重载使得我们的函数名可以同一,它解决了函数名的问题!
void Swap(int s1, int s2)
{ // ...
}void Swap(double s1, double s2)
{ // ...
}
-
模版:模版可以使我们在相同函数的基础上让编译器自行推导我们传入参数类型,它解决了参数类型不同的问题!
template<class T1, class T2>
void Swap(T1 t1, T2 t2)
{//...
}
-
温习以上内容之后我们会发现还存在一个问题需要解决那就是再模版的基础上解决参数个数的问题,我们传入需要推导的参数可能是1个,2个... ,如果函数向模版形式去完成就需要实现多个模版,所以可变参数模版也可以说是模版的模版!
9.2 可变参数模版语法和原理
-
C++11支持可变参数的函数模版/可变参数的类模版。
-
可变数目的参数称作为参数包
-
参数包的种类:
-
模版参数包:包含零到多个模版参数
-
函数参数包:包含零到多个函数参数
-
// Args是参数包的名称,也可以取别的
template<class ...Args> void Func(Args... args){} // Args... 表示参数类型为传值
template<class ...Args> void Func(Args&... args){} // Args&...表示参数类型为传左值引用
template<class ...Args> void Func(Args&&... args){} // Args&...表示参数类型为万能引用(引用折叠)
-
我们用省略号来指出⼀个模板参数或函数参数的表示⼀个包,在模板参数列表中,class...或typename...指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟...指出接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,跟前面普通模板⼀样,每个参数实例化时遵循引用折叠规则。
我们平时使用的printf就使用了这样的思想,printf会把传入的参数在底层存放到一个数组中,打印时取用!
printf("%d\n", 1);
printf("%d %d\n", 1, 2);
我们写一个可变参数函数模版来测试一下:
// 可变参数函数模版
template<class ...Args>
void Print(Args&&... args)
{// (sizeof...) 该运算符可以用于计算参数包的参数个数cout << sizeof...(args) << endl;
}int main()
{double x = 1.1;Print();Print(1);Print(1, string("XXXXXX"));Print(1, string("XXXXXX"), 1.1);Print(1, string("XXXXXX"), x);return 0;
}
前面我们说过可变参数模版可以称作为模版的模版,它的本质也是如此,我们接下来看一个可变参数函数模版如何推导函数模版,然后再从函数模版推导到具体的函数的。
9.3 包扩展
了解了可变参数模版后,我们怎么使用参数包的参数呢?而包扩展就是解析参数包中的参数将他们扩展出来以供使用!
// 包扩展
void ShowList() // 参数包零参数的,递归结束
{cout << endl;
}// 解析参数包
template<class T, class ...Args>
void ShowList(T&& x, Args&&... args)
{cout << x << " ";// 递归调用ShowList(args...);
}template<class ...Args>
void Print(Args&&... args)
{// args参数包中含有N个参数// 我们调用函数ShowList,参数包的第一个参数传入x// 参数包剩下的N-1个参数传入第二个参数包ShowList(args...);
}int main()
{double x = 1.1;Print();Print(1);Print(1, string("XXXXXX"));Print(1, string("XXXXXX"), 1.1);Print(1, string("XXXXXX"), x);return 0;
}
注意:以上的递归推导过程都是处于编译时,我们不可以使用运行时的条件判断终止递归!
// 解析参数包
template<class T, class ...Args>
void ShowList(T&& x, Args&&... args)
{// error 编译时if (sizeof...(args) == 0)return;cout << x << " ";ShowList(args...);
}
整体的递归推导过程:
第二种用法:
// 包扩展的第二种用法
template<class T, class ...Args>
const T& Getargs(T&& x, Args... args)
{cout << x << " ";return x;
}template<class ...Args>
void Arguments(Args... args)
{}template<class ...Args>
void Print(Args&&... args)
{// 注意GetArg必须返回或者到的对象,这样才能组成参数包给ArgumentsArguments(Getargs(args)...);cout << endl;
}//void Print(int x, string y, double z)
//{
// Arguments(GetArg(x), GetArg(y), GetArg(z));
//}int main()
{double x = 1.1;Print();Print(1);Print(1, string("XXXXXX"));Print(1, string("XXXXXX"), 1.1);Print(1, string("XXXXXX"), x);return 0;
}
9.4 emplace系列和push/insert系列的区别
-
对于左值
ouyang::list<string> lt1;
string s1("111111111");
lt1.emplace_back(s1);
lt1.push_back(s1);
// 都是一样的,拷贝构造
-
对于右值
ouyang::list<string> lt1;
string s1("111111111");
lt1.emplace_back(move(s1));
lt1.push_back(move(s1));
// 都是一样的,移动构造
-
直接传参
ouyang::list<string> lt1;
lt1.emplace_back("111111111");
lt1.push_back("111111111");
// emplace:走参数包,直接构造
// push_back:隐式类型转换,产生临时对象,走移动构造
-
多参数pair
ouyang::list<pair<string, int>> lt1;
// 不可以,参数包无法解析
// lt1.emplace_back({"苹果", 1}); error
lt1.emplace_back("苹果", 1);
lt1.push_back({"苹果", 1});
// emplace:走参数包,直接构造
// push_back:隐式类型转换,产生临时对象,走移动构造
总结:emplace系列兼容push/insert系列的功能,部分场景下emplace系列可以直接构造,push/insert系列则是构造+移动构造,所以emplace系列更加便捷!
10. 类的新功能
10.1 默认的移动构造和移动赋值
我们前面了解了类的六大默认构造函数(构造/析构/拷贝构造/拷贝赋值重载/取地址重载/const 取地址重载),了解移动构造和移动赋值之后就变成八大默认构造函数了,但是默认生成移动构造和移动赋值有一定的前提条件!
-
如果你没有实现移动构造,并且没有实现析构/拷贝构造/拷贝赋值重载中的其中任意一个才会默认生成移动构造。默认生成的移动构造对于内置类型会采取逐字节拷贝,对于自定义类型会去查看是否实现移动构造,如果实现走移动构造,如果没有实现走拷贝构造。
-
移动赋值和移动构造的条件大致一样的!
10.2 成员变量给缺省值
成员变量处给缺省值会走初始化列表,如果初始化列表有着自己的值优先初始化列表,没有值使用成员变量处的缺省值!
class Test
{
public:Test(){}private:int _a = 1;int _b = 0;
};int main()
{Test t1;return 0;
}
10.3 default和delete
-
default可以强制默认函数生成(我们有时会存在默认函数没有但是我们需要生成的情况)
-
delete可以限制某些默认函数的生成
Person(Person&& p) = default; // 强制移动构造生成
Person(const Person& p) = delete; // 限制拷贝构造生成
10.4 final和override
-
final使该类无法继承
-
override可以在编译时检测是否成功重写虚函数
10.5 C++11后STL的变化
-
增加了容器unordered_map/unordered_set
-
emplace系列
-
范围for遍历
11. lambda
11.1 lambda的基本了解和语法
lambda是一个匿名函数对象,不同于普通函数他可以被定义在函数内部。lambda表达式语法使用层而言没有类型,所以我们⼀般是用auto或者模板参数定义的对象去接收 lambda 对象。
// 基本语法格式
[capture-list] (parameters)-> return type { function boby } [capture-list] : 捕捉列表
(parameters) : 参数列表
-> return type : 返回值类型
{ function boby } : 函数体
其中编译器会根据[]来判断下面代码是否为lambda函数,[]捕捉列表中的变量可以被lambda函数使用,参数列表可以省略,返回值可以由编译器推导也可以省略,函数体不可省略。
-
写一个lambda函数输出Hello World
int main()
{// lambda函数auto fun = [] { cout << "Hello World!" << endl; };fun();return 0;
}
-
模拟应用场景
class Goods
{
public:Goods(string name = "", double price = 0.0, int num = 0, double assess = 0.0):_name(name),_price(price),_num(num),_assess(assess){}string _name; // 商品名double _price; // 价格 unsigned int _num; // 销量double _assess; // 评价 0.0 - 5.0
};// 价格排序
struct Compare1
{
public:bool operator()(const Goods& d1, const Goods& d2){return d1._price > d2._price;}
};// 销量排序
struct Compare2
{
public:bool operator()(const Goods& d1, const Goods& d2){return d1._num > d2._num;}
};// 评价排序
struct Compare3
{
public:bool operator()(const Goods& d1, const Goods& d2){return d1._assess > d2._assess;}
};int main()
{vector<Goods> list = { {"手机", 1999.0, 10, 4.8}, {"电脑", 6999.0, 5, 4.2}, {"耳机", 299.0, 100, 4.9} };// 根据价格排序// 仿函数sort(list.begin(), list.end(), Compare1());// lambda函数sort(list.begin(), list.end(), [](const Goods& d1, const Goods& d2) { return d1._price > d2._price; });// 根据销量排序// 仿函数sort(list.begin(), list.end(), Compare2());// lambda函数sort(list.begin(), list.end(), [](const Goods& d1, const Goods& d2) { return d1._num > d2._num; });// 根据评价排序// 仿函数sort(list.begin(), list.end(), Compare3());// lambda函数sort(list.begin(), list.end(), [](const Goods& d1, const Goods& d2) { return d1._assess > d2._assess; });return 0;
}
我们可以从以上可见,仿函数和lambda函数似乎有相同之处,但是相较于仿函数来说,lambda函数更加可视化!
11.2 捕获列表
-
lambda函数只可以使用参数和函数体中定义的变量,如果要使用作用域外的变量需要进行捕捉!
-
第⼀种捕捉方式是在捕捉列表中显示的传值捕捉和传引用捕捉,捕捉的多个变量用逗号分割。[x,y,&z]表示x和y值捕捉,z引用捕捉,值捕捉默认是const修饰,不可以更改!需要强行更改(消除const)需要在lambda函数后加上关键字mutable
-
lambda 表达式如果在函数局部域中,他可以捕捉 lambda 位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉, lambda 表达式中可以直接使⽤。这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空。
-
如果使用关键字mutable,传值捕捉的对象就可以修改了,但是修改还是形参对象,不会影响实参。
// 全局变量
int x = 1;int main()
{int b = 1;int c = 2;// 传值捕捉auto fun1 = [b, c](){int a = 0;// b++, c++; // error};// mutableauto fun11 = [b, c]() mutable{int a = 0;b++, c++;};// 传引用捕捉auto fun2 = [&b, &c](){int a = 0;b++, c++;};// 传值,传引用auto fun3 = [b, &c](){int a = 0;//b++; // errorc++;};// 隐式值捕捉// 用了哪些变量就捕捉哪些变量auto fun4 = [=](){int a = 0;return a + b;};// 隐式引用捕捉// 用了哪些变量就捕捉哪些变量auto fun5 = [&](){int a = 0;b++;c++;return a + b;};// 混合捕捉auto fun6 = [&, c](){int a = 0;b++;// c++; errorreturn a + b;};return 0;
}
11.3 lambda的原理
-
lambda 的原理和范围for很像,编译后从汇编指令层的⻆度看,压根就没有 lambda 和范围for这样的东西。范围for底层是迭代器,⽽lambda底层是仿函数对象,也就说我们写了⼀个lambda 以后,编译器会⽣成⼀个对应的仿函数的类。
-
仿函数的类名是编译按⼀定规则⽣成的,保证不同的 lambda ⽣成的类名不同,lambda参数/返回类型/函数体就是仿函数operator()的参数/返回类型/函数体, lambda 的捕捉列表本质是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda 类构造函数的实参,当然隐式捕捉,编译器要看使⽤哪些就传那些对象。
12. 包装器
function<返回类型(所传参数)> 变量名 = 所包对象的名称;
// 使用包装器包装函数,仿函数,lambda
#include <functional>
int Add(const int& x, const int& y)
{return x + y;
}struct f
{int operator()(const int& x, const int& y){return x + y;}
};int main()
{function<int(int, int)> f1 = Add;function<int(int, int)> f2 = f();function<int(int, int)> f3 = [](int x, int y) { return x + y; };cout << f1(1, 1) << endl;cout << f2(1, 1) << endl;cout << f3(1, 1) << endl;return 0;
}// 包装器包装类函数
class Plus
{
public :Plus(int n = 10): _n(n){}static int plusi(int a, int b){return a + b;} double plusd(double a, double b){return (a + b) * _n;}
private:int _n;
};int main()
{// 包装器包装对象函数,对象中static函数没有this指针function<int(int, int)> f1 = &Plus::plusi;cout << f1(1, 4) << endl;// 对象中普通函数存在this指针function<double(Plus*, double, double)> f2 = &Plus::plusd;Plus pl;cout << f2(&pl, 2.2, 1.1) << endl;function<double(Plus, double, double)> f3 = &Plus::plusd;cout << f3(pl, 2.2, 1.1) << endl;function<double(Plus&&, double, double)> f4 = &Plus::plusd;cout << f4(move(pl), 2.2, 1.1) << endl;// 匿名对象cout << f4(Plus(), 2.2, 1.1) << endl;return 0;
}
题目使用展现:
150. 逆波兰表达式求值 - 力扣(LeetCode)
class Solution {
public:int evalRPN(vector<string>& tokens) {map<string, function<int(int, int)>> fun = {{"+", [](int a, int b) { return a + b; } },{"-", [](int a, int b) { return a - b; } },{"*", [](int a, int b) { return a * b; } },{"/", [](int a, int b) { return a / b; } }};stack<int> st;for (auto& e : tokens){if (fun.count(e)){int right = st.top();st.pop();int left = st.top();st.pop();int ret = fun[e](left, right);st.push(ret);}else{st.push(stoi(e)); // stoi将string转换成int}}return st.top();}
};
13. bind
-
bind 是⼀个函数模板,它也是⼀个可调用对象的包装器,可以把他看做⼀个函数适配器,对接收的fn可调⽤对象进⾏处理后返回⼀个可调⽤对象。 bind 可以⽤来调整参数个数和参数顺序。bind 也在<functional>这个头⽂件中。
-
调⽤bind的⼀般形式: auto newCallable = bind(callable, arg_list); 其中newCallable本⾝是⼀个可调用对象,arg_list是⼀个逗号分隔的参数列表,对应给定的callable的参数。当我们调⽤newCallable时,newCallable会调用callable,并传给它arg_list中的参数。
-
arg_list中的参数可能包含形如_n的名字,其中n是⼀个整数,这些参数是占位符,表示newCallable的参数,它们占据了传递给newCallable的参数的位置。数值n表示生成的可调用对象中参数的位置:_1为newCallable的第⼀个参数,_2为第⼆个参数,以此类推。_1/_2/_3....这些占位符放到placeholders的⼀个命名空间中。
// 测试函数
int SumT(int a, int b)
{int _n = 10;return a * _n + b;
}int SubT(int a, int b, int c)
{int _n = 10;return a * _n - b - c;
}// bind
int main()
{// 参数顺序不同// _1表示第一个参数,_2表示第二个参数,以此类推auto f1 = bind(SumT, _1, _2);cout << f1(10, 20) << endl;auto f2 = bind(SumT, _2, _1);cout << f2(10, 20) << endl;// 参数个数不同,强绑一个auto f3 = bind(SubT, 100, _1, _2); // 强绑第一个cout << f3(200, 300) << endl;auto f4 = bind(SubT, _1, 100, _2); // 强绑第二个cout << f4(200, 300) << endl;auto f5 = bind(SubT, _1, _2, 100); // 强绑第三个cout << f5(200, 300) << endl;return 0;
}
对于类对象的成员函数绑死
// 我们之前的包装器包装类函数使用bind就方便许多了
// 成员函数对象进行绑死,就不需要每次都传递了
int main()
{// 没有bindfunction<double(Plus&&, double, double)> f1 = &Plus::plusd;cout << f1(Plus(), 1, 2) << endl;// 使用bindfunction<double(double, double)> f2 = bind(&Plus::plusd, Plus(), _1, _2);cout << f2(1, 2) << endl;return 0;
}
实现一个年利率的计算使用bind进行绑定利率:
// lambda函数
auto fun = [](double rate, double money, int year)->double{double ret = money;for (int i = 1; i <= year; ++i){// 利滚利,本金加利息ret += (ret * rate);}return ret - money;};// 1.5%
function<double(double, double)> fun_1_5 = bind(fun, 0.015, _1, _2);
// 2.5%
function<double(double, double)> fun_2_5 = bind(fun, 0.025, _1, _2);
cout << fun_1_5(1000, 3) << endl;
cout << fun_2_5(1000, 3) << endl;