C++_chapter15_C++重要知识点_lambda,initializer_list
本文内容:lambda,initializer_list
文章目录
- 第15章 C++重要知识点
- 15.8 lambda表达式,for_each、find_if简介
- 15.8.1 lambda 用法简介
- 15.8.1.1 省略返回值
- 15.8.1.2 省略方式
- 15.8.2 捕获列表
- a) [] 为空情况
- b) [&] 捕获外部作用域中所有变量,并作为引用在函数体内使用
- c) [=]
- d) [this] 捕获类中this指针
- e ) [变量名] : 捕获外部指定的变量名
- f) [=,&变量名]
- g) [&,变量名] 按照引用捕获所有的变量,按值捕获后边的变量名
- 15.8.3 lambda表达式延迟调用易出错的细节分析
- 15.8.4 lambda 表达式中 mutable (易变的)
- 15.8.5 lambda 表达式的类型及存储
- 15.8.6 lambda 与 for_each 和 find_if
- 5.8.6.1 for_each 和 lambda表达式
- 5.8.6.2 find_if
- 15.8.7 lambda表达式的应用实践
- 实例1 :map键值对中加入 function函数对象
- 实例2:智能指针与lambda表达式
- 实例3:priority_queue 与 lambda表达式的使用
- 15.8.8 lambda表达式捕获模式的陷阱分析和展示
- 5.8.8.1 捕获列表中的地址符&
- 5.8.8.2 形参列表中使用 auto
- 5.8.8.3 成员变量的捕获问题
- 15.9可变参数函数,initializer_list,省略号形参
- 15.9.1 可变参函数
- 15.9.2 initializer_list(初始化列表)
- 5.9.2.1 使用大括号 { } 传参
- 5.9.2.2 initializer_list拷贝和赋值
- 5.9.2.4 initializer_list使用场景
- 15.9.3 省略号形参 (...) 三个点
第15章 C++重要知识点
15.8 lambda表达式,for_each、find_if简介
15.8.1 lambda 用法简介
lambda 是C++ 11 引入,是一个匿名函数,可以捕获一定范围内的变量。
特点:
a) 是个匿名函数,也可以理解为“可调用的代码单元”,或者理解为未命名的内联函数;
b) 它也有一个返回类型,一个参数列表,一个函数体;
c) 与普通函数不同的是lambda 表达式可以在函数内部定义。
格式:
[捕获列表] (参数列表)-> 返回类型{函数体};
因为很多时候,lambda表达式返回值特点明显,所以允许lambda表达式返回类型省略,编译器自动推导;
15.8.1.1 省略返回值
auto f2 = [](int a) {return a + 1;};cout << f2(1) << endl;
编译器并不是总能推断出返回值类型,如果编译器不能自动推导,需要写出返回值。
15.8.1.2 省略方式
大家注意:编译器并不是总能推断出返回值类型,如果编译器不能自动推导,需要写出返回值。
auto f3 = []() {return 1; };auto f32 = [] {return 1; };
c) 捕获列表[] 和函数体不能省,必须时刻包含。
d) lambda 调用方法和普通函数相同,都是使用 () 这种函数调用运算符。
e) lambda 表达式可以不返回任何类型,不返回任何类型就是void
auto f4 = [] {};
15.8.2 捕获列表
通过捕获列表来捕获一定范围内的变量.
a) [] 为空情况
[]为空表示不捕获任何变量; 如果是静态变量可直接使用。
在lambda表达式中,可以直接使用外部的静态变量。
static int i = 9;auto f1 = [] {return i; //直接在lambda中使用静态变量。};
b) [&] 捕获外部作用域中所有变量,并作为引用在函数体内使用
int i = 9;auto f5 = [&]{i = 5; // 因为&的存在,那么久允许给I赋值,从而也就改变了i的值;return i;};cout << f5() << endl;cout << i << endl;
c) [=]
捕获外部作用域中所有变量,并作为副本(按值)在函数中使用,也就是可以用它的值,但不允许给它赋值。
int i2 = 9;auto f6 = [=] {return i2; };
d) [this] 捕获类中this指针
一般用于类中,捕获当前类中this指针,让Lambda表达式和当前类成员函数同样的访问权限。如果 [] 中已经使用过了 & 或者 = ,那么默认就已经使用了this, 说白了,捕获this的目的就是为了在lambda中使用当前类的指针。
e ) [变量名] : 捕获外部指定的变量名
如果是多个变量名,彼此之间用 , 分割,[变量名] 表示按值捕获,[=]则是捕获所有的变量,比=更精准的捕获。
按引用捕获变量名[&变量名]。
f) [=,&变量名]
=表示按值捕获所有外部变量,但是按引用捕获&中所有的变量,这里等号必须写在开头位置。也就是说,这个捕获列表,第一个位置表示的是默认捕获方式,后续其他的都是显示捕获方式。
g) [&,变量名] 按照引用捕获所有的变量,按值捕获后边的变量名
15.8.3 lambda表达式延迟调用易出错的细节分析
int x = 5;// auto f7 = [=]auto f7 = [&]{return x;}; // x = 10;cout << f7() << endl; // 5 , 因为将5 输入到了lambda表达式中;
预期的结果是10,实际上是5.
15.8.4 lambda 表达式中 mutable (易变的)
[=]以值方式捕获外部变量时候,加上mutable就可以修改该值了。注意,加mutable时候不能省略()。可以修改该值,但是该值是原来的副本,所以对原来的值没有影响。
int x2 = 5;auto f8 = [=]() mutable // 注意要加mutable ,则()就可以省略了{x2 = 6;return x;};
15.8.5 lambda 表达式的类型及存储
C++11 中 Lambda 表达式的类型被称为 闭包类型 closure type;
闭包:函数内的函数(可调用对象),正常不能在函数中定义函数,本质上就是Lambda表达式创建的运行时期的对象。
lambda表达式是一种比较特殊的,匿名的,类类型[闭包类]的对象(也就是定义了一个类的类型,又生成了一个匿名的类对象)
所以,我们可以用std::function 和 std::bind 来保存和调用lambda表达式,每个Lambda都会编译器给咱们生成lambda表达式这种语法,是我们可以就地定义匿名函数;
std::function<int(int)> fc1 = [](int tv) {return tv; };cout << fc1(15) << endl;std::function<int(int)> fc2 = std::bind([](int tv) {return tv;}, 15);cout << fc2(15);int a[5];a[0] = 1; // 便捷写法 (语法糖)*(a + 1) = 3;
15.8.6 lambda 与 for_each 和 find_if
5.8.6.1 for_each 和 lambda表达式
for_each是个函数模板;在for_each的第三个参数中使用一个labmda表达式,可以对原始的数组进行修改。
void test03()
{vector<int> myvector = { 1,2,3,4,5,6 };// for_each(myvector.begin(), myvector.end(), myfunc);for_each(myvector.begin(), myvector.end(), [](int &v) {cout << v << " "; });cout << endl;
}
5.8.6.2 find_if
用来查找一个东西,取决于它的第三个参数,它的第三个参数也是个函数对象。
vector<int> myvector2 = { 10,202,30 };auto result = find_if(myvector2.begin(), myvector2.end(), [](int val) ->bool{cout << endl;cout << val << " ";return true;// 只要返回false, 那么find_if 就不停地便利myvector, 一直返回true或者便利完为止。});if (result == myvector2.end()){cout << "没找到" << endl;}else{cout << "找到了" << endl;}
15.8.7 lambda表达式的应用实践
Lambda表达式就是一个函数对象,可以方便的表示一元函数对象 和 二元函数对象。
Lambda表达式只能使用在语句中,如果想跨语句使用之前定义好的lambda表达式,怎么办呢? 用什么类型来表示lambda表达式呢?
当然是function类型来表示函数对象类型了,
实例1 :map键值对中加入 function函数对象
int main()
{map<int, function<int(int, int)>> cacul;cacul[1] = [](int a, int b)->int {return a + b; };cacul[2] = [](int a, int b)->int {return a - b; };cacul[3] = [](int a, int b)->int {return a * b; };cacul[4] = [](int a, int b)->int {return a / b; };cout << "10 + 12" << cacul[1](10, 12) << endl;system("pause");return 1;
}
实例2:智能指针与lambda表达式
当使用FILE 句柄时,自动调用第二个参数lambda表达式。
unique_ptr<FILE, function<void(FILE *)>> ptr1(fopen("data.txt", "w"), [](FILE *pf) {fclose(pf); });
实例3:priority_queue 与 lambda表达式的使用
不使用lambda表达式需要在Data类中提供 < > 运算符重载函数。
priorty中使用lambda表达式之后
using FUNC = function<bool(Data&, Data&)>;priority_queue<Data, vector<Data>, FUNC> maxHeap([](Data &d1, Data &d2)->bool{return d1.mb > d2.mb;});maxHeap.push(Data(10, 20));maxHeap.push(Data(15, 15));maxHeap.push(Data(20, 10));
15.8.8 lambda表达式捕获模式的陷阱分析和展示
5.8.8.1 捕获列表中的地址符&
lambda表达式用于捕获外部变量时候,如果lambda [&]时候,一定要注意外部变量的生命周期。srand() 表示生成一个随机数种子,当调用 rand() 时会产生一个不一样的值。
std::vector<std::function<bool(int)>> gv;
void myfunc()
{srand((unsigned)time(NULL));int tmpvalue = rand() % 6; // 产生0-5 之间随机值。gv.push_back([&](int tv) {if (tv % tmpvalue == 0) // 问题:当被调用时候return true;return false;});
}
捕获函数中的变量,当函数结束的时候,释放栈空间,对应的变量也就结束了。
cout<xgv[0](10)<<end1;//跟踪调试,这个调用导致问题,因为此时1ambda里tmpvalue已被销myfunc( );//毁,所以会产生未定义行为
针对上述代码设置断点并跟踪调试,会发现当调用gv0来执行lambda表达式中的代码时,其中的tmpvae已经是一个无效值,所以程序执行后会产生未定义行为(结果错误甚至程序崩溃)。
5.8.8.2 形参列表中使用 auto
C++14 中允许在lambda表达式的形参数列表中。
将上面的lambda中的[&] 修改为 [=] ,程序就不会崩溃。
void test3(){std::vector<std::function<bool(int)>> gv;srand((unsigned)time(NULL));int tmpvalue = rand() % 6; // 产生0-5 之间随机值。gv.push_back([=](auto tv) { if (tv % tmpvalue == 0) // 问题:当被调用时候,return true;return false;});}
上面代码中,lambda表达式中按值捕获的方式解决了因为变量tmpvalue超出作用域范围(又叫引用悬空)而产生的问题,因为按值捕获的方式在lambda表达式里持有的是tmpvalue的值(副本),而且是在lambda表达式定义的那行就已经持有该值了。也就是说myfunc 一被调用,lambda表达式中就持有了tmpvalue的值。
想象一下,tmpvalue是一个int类型的值还好办,如果是一个指针,那么就算是在lambda表达式中持有的是指针的副本(指针副本和原指针指向的是同一块内存),但如果后续把这个指针指向的内存 delete掉,那么再次调用该lambda表达式同样面临lambda 表达式中该指针副本指向的内存已经无效的问题,因为这段内存已经被系统回收了。所以这务必要引起程序员的注意,不要犯类似的错误。
5.8.8.3 成员变量的捕获问题
在类中lambda表达式使用[=] 或者 [this] 捕获类的成员变量。生成类的成员属性的副本;在类中,lambda 表达式执行成功与否,取决于pat对象是否存在,只有pat对象存在,这个lambda表达式执行才正确。
15.9可变参数函数,initializer_list,省略号形参
15.9.1 可变参函数
能够接受非固定的个数参数就是可变参类型;initializer_list 标准库类型,该类型能够使用的前提条件是所有的实参类型相同。
15.9.2 initializer_list(初始化列表)
如果一个函数,它的实参数量不可预知,但是所有参数类型相同,我们就可以使用这个initializer_list 类型的形参来接收。
initializer_list 是C++11提供的新类型,也是一个类模板 vector;我们把initializer_list 理解成某种类型数组,这个类模板里制定的类型模板参数就是这个数组里保存的数据类型。
initializer_list<int> myarray; // 数组,元素类型是int,空列表
initializer_list<int> myarray2 = { 1,2,3,4,5 };
注意:initializer_list 队形中的元素,永远是常量值,不能改变。
C++11 将使用大括号{}的初始化{初始化列表}作为一种比较通用的初始化方式,注意积累。
5.9.2.1 使用大括号 { } 传参
若要向initializer_list 形参传递一个序列,则必须把这个序列放在{}中包括起来作为一个整体;
printvalue({ "aa","bb" });
5.9.2.2 initializer_list拷贝和赋值
拷贝,赋值一个initializer_list 对象,不会拷贝列表中的元素,原来对象拷贝或者赋值出来的对象共享列表中的元素。
myarray3 中的对象只是 元素的引用,并不真正拥有这些数据。
initializer_list<string> myarray3 = { "aa","bb","cc" };initializer_list<string> myarray4(myarray3);initializer_list<string> myarray5;myarray5 = myarray4;
5.9.2.4 initializer_list使用场景
std::initializer_list 使用场景:
// 方式1:void test(){// 1 使用 std::initializer_list 方式初始化类容器std::vector<int> v = { 1, 2, 3, 4, 5 };std::list<std::string> l1 = { "apple", "banana", "cherry" };}// 方式2:构造函数和赋值class MyClass{public:MyClass(std::initializer_list<int> list){for (auto& value : list){cout << value << endl;}}};void test2(){MyClass obj = { 1, 2, 3, 4 };}// 方式3:作为函数参数,可以接受任意数量的同类型参数void print(std::initializer_list<int> list){for (auto elem : list) {std::cout << elem << ' ';}std::cout << '\n';}void test3(){print({ 1,2,3,4,5,6 }); // 输出: 1 2 3 4 5 6}// 方式4:聚合类初始化// 无构造函数,无私有或保护的非静态成员;无基类和虚函数;无私有或保护的继承// 什么是聚合类?聚合类的一个主要特点是 花括号初始化语法进行初始化// 聚合初始化允许你直接初始化对象的数据成员,而不需要调用构造函数。struct Point{int x, y;};void test4(){Point p = { 1,2 };cout << p.x << endl;cout << p.y << endl;}
15.9.3 省略号形参 (…) 三个点
省略号形参一般无法正确处理类类型对象,能正确处理的int,char * ;这种省略号形参形式的可变参函数,虽然参数数量不固定,但是函数的所有参数是存储在先行连续的栈空间中。而且带 … 的可变参 函数必须至少有一个普通函数,我们就可以通过这个普通参数来寻址后续的所有的可变参类型以及值。
cout << average(3, 1, 2, 3, 4);
int average(int num, ...)
{va_list valist; // 创建一个va_list 类型的变量;int sum = 0;va_start(valist, num); // 是valist 指向一个起始参数for (int i = 0; i < num; ++i){// 遍历参数 sum += va_arg(valist, int); // 参数2,说明返回类型是int类型。}va_end(valist);return sum;
}
#include <stdarg.h>
用到的宏
va_list valist; // 创建一个va_list 类型的变量;
va_start(valist, num); // 是valist 指向一个起始参数
sum += va_arg(valist, int);
va_end(valist);
实现类似printf
funcTest("5", "a", "b", "c", "d", "e");
//void funcTest(const char *msg, ...) // 2
void funcTest(const char *msg ...)
{va_list valist; // 创建一个 va_list 变量int csgs = atoi(msg); // 这个就是拿到可变参数量,这个数量我们后续要用于获取可变参数。// atoi 将 char * 转为 int类型va_start(valist, msg); // 使 valist指向起始参数int paracount = 0;while (paracount < csgs){char *p; // 这块我们假定给的可变参都是字符串;p = va_arg(valist, char *);printf("第%d个参数是:%s\n", paracount, p);paracount++;}va_end(valist);
}
注意事项:
1 至少有一个有效形参,形参不能是完全 ...;
2 ... 只能出现在形参列表最后一个位置;void funcTest(const char *msg, ...)
3 ... 之前的逗号,可以省略;
4 如果有多个普通参数,va_start(valist,,msg),第二个参数必须... 之前的那个参数// void funcTest(const char *msg ,const char *msg2 ...) va_start(valist,msg2)
5 一般这些可变参类型是数值型或者字符串类型还能正常处理,其他类型一般都不能正常处理,所有 ... 使用场合不多。
6 不建议在C++中使用 ... 但是遇到了要知道。
