在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称。不过由于C++03(TC1)主要是对C++98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C++98/03标准。从C++0x到C++11,C++标准10年磨一剑,第二个真正意义上的标准珊珊来迟。相比于C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更
强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多,所以我们要作为一个重点去学习。
列表初始化
首先c++11扩大了{}初始化的范围首先就是其它地方也可以使用{}去初始化
例如下面三种:
还有数组和struct结构体也可以使用这种方法初始化。
总结:
类也可以使用{}去初始化:
当然这里的本质其实是一个类型转换
例如下面的这个:
上图中的第一行和第二行代码使用的是构造+拷贝构造(然后因为此时是连续的构造所以这就就优化成了直接构造)。
而下面的左值引用则是创建了一个临时对象,因为临时对象具有常属性所以必须使用const修饰。
除此之外在c++11中使用{}号还能更好的初始化new出来的空间。例如下面:
这里的Data是一个简单的日期类。
下面我们来看一下vector,list的初始化使用下面的这种方式:
那么这里的初始化和上面date的初始化一不一样呢?原理肯定是不一样的:
Data处能够使用{}底层的原理是编译器将连续的构造优化了,此时d2中{}内的元素和对应构造函数参数个数是匹配的。d2的{}中元素的个数,新增会报错,减少也报错。但是vector中则不一样,vector初始化{}中的元素是可以继续增减的。
那么c++为了能够完成这样的初始化是在vector中增加了一个构造函数的这个构造就是
不仅仅vector存在list也存在这个构造(每一个stl容器中都有)。
即默认情况下{}内的元素会被c++默认为一个initializer的对象,但是需要注意的是Date d2那里的{}是没有被认为一个initializer对象的。
此时d2的那里就是一个特殊的处理了。
当Date d2{}中的元素和构造函数的参数个数不匹配的时候,此时{}就会被认为是initializer对象。
这个initializer是c++中的一个类型
除了特殊情况(Date中的{})之外{}中的元素都会被认为是一个initializer_list对象.这个initializer也是支持迭代器的。
这个initializer_list和list没有关系。
下面的这个代码我们就可以这么去理解:
首先就是编译器在常量区开辟了一段空间,然后将10.20等等元素放到常量区中然后initializer_list这个结构体中存在两个指针一个指向这片空间的开始一个指向这片空间的结束,至于initializer_list的迭代器自然使用原生指针去实现的。指出迭代器那么initializer_list就能够和容器一样去遍历:
那么范围for自然也就是支持的了(因为范围for底层就是被替换成为迭代器)。
那么现在我们思考一下我们就能明白为什么vector能够使用{}去初始化了,这里就是先去遍历initializer_list对象中的数据,然后插入到vector中就可以了(或者直接使用迭代器区间初始化)。如果我想在自己的vector中支持一个使用{}去初始化也是很简单的只需要增加下面的这个构造函数。
这样我们自己实现的那个vector也能够支持使用{}去初始化了。
那么map自然也是可以使用的了
首先dict外面的那个括号可以认为是调用initializer_list的构造来初始化dict,那么里面的那两个{}呢?而这里面的两个括号都是使用字符串去构造一个pair,构造成pair之后在整体隐式类型转换成为了initializer_list,然后调用initializer_list的构造来初始化dict。
除了初始化的地方存在initializer_list版本,赋值(=)也可能是存在initializer_list版本的。
例如上面就是因为赋值也是存在initializer_list版本的。
总结c++11中的一切都可以使用初始化列表初始化分成三个部分,一步分就是原先c语言中就存在的使用{}去初始化。
第二个c++在c语言的基础上扩大了{}的初始化(数组,int,并且可以不使用=),然后就是c++支持自定义类型支持多参数的隐式类型转换。
只有参数和拷贝构造参数一样才会被识别成为隐式类型转换。
此时dd3不行就是因为dd3后面的括号被识别成为了initializer_list对象,但是Date中不存在initializer_list的构造。所以上面就会报错。
所以只有参数数量和构造函数的参数类型是一样的,才会被认为是多参数的隐式类型转换(可以认为是单参数的隐式类型转换的扩展)
最后一个自然就是使用initializer_list对象去构造完成的初始化了(赋值只要支持initializer_list也是可行的)。
然后下面就是使用auto实现让编译器推导自己是什么类型。auto应用于范围for还能让范围for不再是一个写死的范围for,当然auto除了在范围for的时候使用的多以外,在迭代器的那里使用的也很多。
下一个是能够推导类型的关键字,
首先我们可以认识一下推导类型的第一个函数
这个typeid的作用就是能够将类型的字符串拿出来。
但是仅限于此。
是不能使用这个再去定义变量的。
当然如果只是在一个函数中想要重新定义一个同类型的变量直接使用一个auto当然是可以的,
注释掉的代码就是错误的
但是如果是下面的情况呢?
假设这里我们不知道i*d是什么类型的。那么在vector中我们能直接使用auto吗?此时我们就需要ret的类型,去实例化vector
这个时候就可以使用decltype了
运行结果:
总结:
然后下一个就是在c++中空指针一般是使用nullptr去表示的。
然后是范围for(底层迭代器)。
然后是智能指针,后面会详细说明。
下面就是在c++11中stl的变化:
主要就是增加了新的容器(下图红色:)
unordered_map和set之前已经详细说过了,forward_list也就是单链表,而list是一个双向链表。
如果array有价值那么array的价值就是对于越界的判定变得严格了,但是为啥不使用vector呢?
下面是stl中容器变化的总结:
右值引用和移动语义
下面是右值引用和移动语义的学习(最重要)。
首先什么是左值和右值呢?
总结:左值最重要的特点就是能够被取出地址,左值一般是叫做表达式,那么左值引用就是给左值取别名。
左值也可以出现在右边:
什么是右值呢?
右值是不能取地址的。常见的右值:
函数返回是一个传值返回,下面的两个之所以是右值其实指的是i+j,以及fmin(i,j)返回的那个临时对象是一个右值。都不能取地址。
总结:区分左值和右值就看能否取地址(核心)。
下面我们需要学一个新的东西叫做右值引用:
右值引用是给右值引用取别名。
那么能否交叉呢?
首先左值引用能否给右值取别名呢?
当然可以例如下面:
但是不能将const去掉(如果去掉会导致权限放大而出错)。
那么右值引用能否给左值取别名呢?
这个move暂时不用管。
那么我们思考一下右值引用的意义是什么呢?
首先我们需要知道引用出现的意义就是减少拷贝,而大部分情况下使用左值引用已经能够解决问题了。
例如使用左值引用传参就减少了拷贝的发生。
引用传参在所有的情景都能够使用了,但是引用返回所有的场景都能够使用左值引用吗。
所以左值引用,唯一没有解决的问题也就是这一个场景了。
下面我使用一个string的模拟实现代码来看:
我实现一个这样的函数这个函数就是一个不能使用左值引用返回的函数因为ret在出了这个作用域之后就被销毁了。那么c++11是怎么解决这个问题的呢(不使用值返回),使用右值引用能够解决吗?也不可以,因为右值引用也没有将这个ret的生命周期延长啊。
所以这里使用引用返回是无法解决这里的问题的。但是右值引用可以强行解决这里的问题,那么右值引用如何强行解决这里的问题呢?
怎么解决呢?
要解决这里的问题就需要使用移动构造和移动赋值。
首先这里需要重载一个右值应用版本的构造。
在c++11里面对右值是存在区分的:
然后下面就是移动构造:
这里的打印是不需要的。
有了这个东西之后就能够解决上面说的那个问题了。下面我们来梳理一遍:
如果没有移动构造:
在整个过程中拷贝了两次空间析构了两次空间,首先就是使用ret取拷贝了一个临时对象这个临时对象指向一片新空间,然后在使用这个临时对象拷贝一个给s。析构了ret原先指向的空间和临时对象拷贝的那个空间。
这中间是很浪费的。
所以这里就有了移动构造,因为上面的ret可以被认为是一个将亡值,然后将ret内的资源转移给临时对象(这里如果是正常的对象是无法完成的)这里的ret严格来说并不是右值,只是编译器将其识别成为了一个特殊的右值(将亡值),然后这里就是直接转移这个将亡值的资源给临时对象,此时就不需要进行一次深拷贝了。
然后在s的那里还要完成一个移动赋值,虽然普通的赋值函数使用const string& 也能够接收右值(临时对象将亡值是一个右值,但是这里如果出现了一个更加符合的那么编译器就会走更加符合的那个)
此时就完成了对将亡值资源的转移并且,我还将自己不需要的资源转移给了这个将亡值交给他帮我销毁资源。
下面是图解:
所以右值引用就是间接起作用的,当然也没有开辟新的空间(将亡值)。
如果是下面这样写呢?
没有移动拷贝和构造:
因为这里是连续的拷贝构造所以可以优化
此时依旧是具有重新开空间的消耗的。如果是存在了移动拷贝和构造:
会直接将资源转移
需要注意一般情况下不要对一个左值进行一个move
例如上图中的s1因为资源已经被转移了,所以就变成空值了。
移动构造和移动赋值对于日期类那样的类是毫无意义的,总结移动构造和移动语义对于使用深拷贝的类是有意义的,对于浅拷贝的类是没有意义的。浅拷贝的类没有涉及到新空间的开辟,所以交换资源也就没有了意义。
下面是移动构造和移动赋值使用的一些场景首先就是我们一直在使用的swap函数:
在没有移动构造和移动赋值之前,swap函数对于深拷贝的类都死重新开辟空间,然后拷贝然后在交换。
而在使用了移动构造和移动赋值之后对于深拷贝的类交换就可以直接使用移动构造,和移动赋值,不需要额外开辟空间了。效率就高了很多,所以在c++11之后,对于深拷贝的类直接使用swap效率也不会很低。
除此之外在有了移动构造和移动赋值之后,c++也不会担心传值返回的效率低了。
例如下面的函数返回值,如果没有移动构造和移动赋值那效率是非常低的。
但是有了之后那效率就高起来了。
那么在c++11之前,c++98是如何处理这个问题的呢?
c++98提高效率,本质上是利用了输出型参数。间接解决了问题。c++11直接传值返回效率也很高,因为有了移动构造和移动赋值。
除此之外
例如vector,list,map,unordered_map等等。
还有一个就是move这个函数的使用,例如move(左值),那么这个函数的返回值的属性才会被修改成为右值,但是左值的属性是不会改变的。
上面代码中的最后s3 = s1就是一个拷贝构造,因为s1依旧是一个左值。即move不会影响s1的属性。
下面我们来看一下下面两个容器的插入接口:
可以看到set和list都新增加了一个右值引用的接口,那么这个右值引用接口的意义是什么呢?
我们可以使用我们自己的这个string来看一下:
首先我们尝试一个将一个左值插入到list中
出现了一个深拷贝这个深拷贝是从哪里来的呢?
如果这里插入的是一个右值呢?
下面的tostring的返回值就是一个右值(将亡值)。此时就是构造加上两个移动构造我这里运行只显示了一个是因为我使用的编译器是vs2022,这个编译器将一个移动构造给优化了,显示的一个移动构造就是将上图中string所指向的空间,交换到了将要储存到list中的那个变量中。还有一个移动构造是在,to_string函数的最后,return ret,ret被认为是一个将亡值(右值)也应该调用移动构造但是因为vs2022的优化,这里就被优化掉了。
虽然这里走了两次移动构造,但是移动构造比起深拷贝的代价是低很多很多的,因为移动构造就涉及到一个指针内容的交换而已。
如果这里没有移动构造那么这里就是两次深拷贝。两次深拷贝变成了两次移动构造,这个效率的提升是很大的。
除此之外下面的这个场景也是一个移动构造。
先使用“11111”构造一个临时对象,然后往下传递,临时对象是一个右值,最后使用移动构造。
所以总结可以看到set和list都新增加了一个右值引用的接口,那么这个右值引用接口的意义是什么呢?
这个意义针对于深拷贝的类,如果这个类是一个深拷贝的类并且这个类实现了移动构造,那么此时插入就可以大大的提高效率,减少拷贝。()主要就是针对于插入完成之后就会被销毁的储存对象例如上面to_string的返回值,以及使用1111构造的临时对象,左值还是需要深拷贝。
下面我就在自己之前所写的list中增加一下右值引用版本的push_back。
首先既然要完成push_back的右值引用版本首先自然就是完成函数了:
这里存在一个结论:右值不能修改,但是右值被右值引用知乎,需要被修改,否则无法完成移动构造和移动赋值,所以右值引用的属性其实是左值。
因为上面的push_back需要使用到buyNode函数所以这里也需要去完成一个buyNode函数的右值引用版本
这里去调用Node的构造函数的时候,依旧要使用move原因如上,所以下面还需要完成一个右值引用版本的构造函数。
x依旧要使用一下move最后才会去调用T这个类型中的移动拷贝。
下面来解释一下这个结论
总结:如果右值引用无法被修改那么移动构造也就无法完成了,移动构造要完成自然要将右值引用的属性修改为左值才能被修改,所以这也就是为什么上面必须要使用move转换一个x的属性。
这里会出现一个深拷贝的原因是我的这个链表是带一个哨兵卫的,在创建哨兵卫的时候,进行了一次深拷贝。
那么除了使用move解决这个问题之外,还有一个方法能够解决这个问题。
进入下一个学习
万能引用
这个t既可以接收右值也可以接收左值。
你传递的是右值,那么t就是一个右值引用,你传递的是左值这里就是一个左值引用。你传递const 左值,那就是const左值引用。
下面使用下面的代码验证一下:
运行的结果:
为什么都是左值引用呢?
原因如下:这里虽然你传左值这里的t是左值引用,那么最后Fun最后去调用左值引用,自然是正常的,但是如果这里的t你传递的是一个右值引用,那么t虽然此时是一个右值引用,但是最终这个t的属性也就是一个左值,所以最后走的都是左值引用。
那么这里使用move能否解决问题呢?当然不能因为此时就将本来是左值引用属性的t修改成了右值。
使用move之后:
那么如果这里我期望保持t的属性呢?(上面list能够使用move能够解决问题在于我知道此时他需要转换为右值)。
而这里库中提供了一个东西叫做完美转发:
此时就在这里保持了t的属性这个东西也就是完美转发。
所以对于刚刚上面的场景使用完美转发也是可以解决的。
总结:移动构造和移动复制对于深拷贝的那一些类的影响是最大的,其次就是swap函数。
所以以后如果想要自己在去实现容器的话,如果容器是存在深拷贝的话,需要去完成移动拷贝和移动赋值。
然后就是插入接口也增加了右值引用的接口。
然后在c++11之后类的新功能也进行了增加。
首先是默认成员函数进行了增加。变成了8个默认成员函数
增加了移动构造和移动赋值。
默认成员函数也就是我们自己不写编译器默认会自己生成一份。
对于移动构造和移动赋值的默认生成规则比较复杂。
并且编译器自动生成的移动构造和移动赋值会做什么事呢?
为什么条件会这么严格呢?因为一般而言如果你是一个需要完成深拷贝的类,那么一定需要完成析构函数,拷贝构造,以及拷贝赋值,否则深拷贝的类就会出现错误。
所以理论上而言这三个函数你要实现那就一起实现,那么就三个都不需要实现。
所以对于移动构造和移动赋值的默认生成就会存在上面的条件。
除此之外c++11中还存在一个关键字default
这个关键字的作用就是强制编译器生成默认函数。
和default关键字对应还存在一个delete关键字:
例如某些对象我不想让他的左值被拷贝
除此之外还有
finnal关键字的作用为修饰一个类让其不能被继承, 还有一个作用是修饰虚函数让这个虚函数不能被重写。
override关键字的作用为修饰派生类的虚函数,检查这个虚函数是否完成重写,没有完成重写就会报错。
下一个是
c++中的可变参数模板
我们之前也学习到过可变参数,例如printf,但是那是函数的可变参数。
c函数的可变参数底层是通过一个指针数组实现的,这里传了多个参数之后,就会将这些参数传递到这个数组中,然后但看到可变参数之后,其实就是去解析这个数组。
而在c++11之后,对这个函数的可变参数进行了扩张,扩张到了模板中也可以使用可变参数。即模板的可变参数包。
例如下面这样:
可以根据传入的数值来倒堆。
下面我们可以使用sizeof去计算这个参数包中参数的个数:
语法规定在参数包的前面需要加上...(这个是语法规定)
。如果我们想要将是参数包中的参数去出来要怎么做呢?
不能使用下面的这种方法:
那要如何取呢?模板参数包中是多个类型,而函数参数包中是多个变量。所以模板参数包不支持上面的这种方法取出储存的东西。
需要使用下面的这种方法
这里的目的是解析出模板参数包中的内容进行一一打印。
这里是一个编译时的递归推演,假设这里的参数包中含有的三个内容(1,2,3)此时的参数包中含有三个参数。
在底层推演的时候,参数包中的第一个值就传递给了_showlist(val,arsg),那么val这个值也就是1了,然后这里继续将这个参数包的第二个值使用递归的方法传递过去,此时的val就是2,直到最后参数包中没有参数了,(参数包中没有参数也是存在的)那么就会调用最后的无参数的_showlist进行递归的返回。
除了上面的方法之外还有一个方法能够获得参数包中的内容。
这里利用了一个逗号表达式,逗号表达式需要取右边的这一整个的值,所以这里需要将这个逗号表达式展开,也就是需要将这个参数包给解开,并且因为不能确定这个数组不能开多大,所以需要将整个参数包解开。这个数组有多大,将由这个逗号表达式有几个来决定(参数包有几个这里几个逗号表达式)。然后通过这个函数取打印参数包中的值。
使用这种方法,也行这种方法就是利用数组取让编译器强行推导。
这里就是这个数组大小由参数包中的元素决定,而这个参数包中的元素存在多少个那么这个函数就会被调用打印多少次。
下面我们回归到实际应用。
在容器中存在两种尾插方法以链表为例子:
emplace_back版本的插入
然后这里有人认为emplace_back版本比push_back版本高效。
怎么高效呢?高效的点在哪里呢?
在emplace版本中参数包中的每一参数都可以是引用/万能引用。
这里先提出结论在普通情况下这两个尾插是没有区别的。
首先我们先来看一下使用push_back尾插一个左值一个右值:
可以看到一个是深拷贝一个是移动拷贝。
如果这里使用的是emplace版本的呢?
因为emplace这里是一个参数包,所以可以传递多个参数,但是这里只有一个参数所以这里的参数包是没有作用的。
然后运行后的结果:
依旧是一个深拷贝一个移动拷贝。
所以只有一个参数的时候是没有区别的。
那么什么地方会有区别呢?
下面的这个场景存在一些区别:
然后是运行结果:
第一个调用的是构造加移动构造而第二个则是直接的构造。
为什么呢?
第一个是先构造了一个临时对象然后使用移动拷贝将其插入到list中
第二个为什么只使用了一个构造呢?
而第二个相当于直接使用这个"xxxx",在数组上完成了对象的构造。
首先在参数包中可以完成推演能够知道这个参数类型是一个string。然后回到push_back那里首先这里发生了一个隐式类型转换,因为在string的list中push_back的value是被写死了的。就是string,所以这里首先使用一个const char* 构造了一个string,然后在传参,然后因为构造1出的string是一个临时对象(右值),所以这里就会进行移动拷贝。
对于参数包而言,这里这个参数包推演到了这个参数的类型之后,所以这里是不存在类型转换的。然后这里会将这个参数包往下传,传到你要使用参数包中的内容取构造节点中的值的时候,然后使用参数包直接取构建这个值。
所以这里只有一次构造。
除此之外多参数的时候也很不一样(pair).如果这里是push_back我们可以这么写:
这里就是一个构造加移动构造:
如果这里使用的是emplace_back可以这么写:
在push_back那里value_type是写死的你只能传递一个pair,所以这里首先是使用了构造构造出来一个pair<string,int>,然后因为是右值发生了移动拷贝,但是emplace_back则不一样,这里因为推演出来的参数和实参是一样的就不会发生类型转换,然后就会将这个参数包继续往下传递,知道需要使用这个参数包构建节点的时候,才会使用参数包中的值直接构造。
这里第二个直接构造的原因还是一样
总结emplace版本:
但是对于这样深拷贝的类,这两者在效率上没有很大的提升,因为移动构造的效率已经很高了。
总结:
只不过empalce版本可以玩的花样多了很多
既可以直接传,也可以传递一个pair。
底层的实现原理:
这里的_data的类型就是T
这里相当于:
总结一下:对于深拷贝的类emplace_back版本的插入和push_back版本的插入效率并没有很大的改变(移动构造的效率已经够高了),反而是一个浅拷贝的类(浅拷贝没有移动构造)如果这个类具有多个内置类型反而会有很好的效果(emplace_back)。
下面就是c++11中的lambada表达式。
首先什么是lambada表达式:
首先我们去看一个经典的情景:
这里存在一个商品的类和商品的对象,如果我们想要给这个数组中的商品按照一定的规则进行排序。然后这个规则会可能会根据以后增加的商品属性进行更改。
此时如果你是重载一个小于和大于是不太好的,因为更具规则不同会产生不同的比较结果。
那么此时上一个仿函数是最好的。
然后如果下面你想按照其它的属性进行比较,都是可行的。增加新的仿函数即可。
所以这里仿函数是能够更好的适应这里的需求的。
但是仿函数也是具有缺陷的,如果有人这么写呢?
我不知道这是拿什么比较的,即仿函数还需要取决于你的命名是否很好。
那么lambda表达式是如何解决这里的问题的呢?
首先我们来学习lambda的使用。
lambda的使用
首先是lambda的总体介绍和构成:
mutable在某些情况下可以省略不写,参数列表也是可以省略的。最后返回类型也是可以不写的(可以自动的推导返回值)。函数体和捕捉列表是必须要写的。
下面我们来看一下最简单的lambda表达式
从本质上来说lambda就是一个函数对象。即上面的一个整体是一个对象。
那么我们如何调用他呢?
一般使用auto玩。
也可以将return-tepe去除。
因为这里存在return 0 可以自己去推导返回值类型。
这就是一个lambda的简单使用。
然后下面我们就使用这个lambda去解决上面商品的那个场景。
此时即使你增加其它商品的属性都能够完成比较。
这里我们还可以将返回值都去掉(自己推导)。
lambda的使用场景也就是需要需要一个可调用对象的时候(也可以使用仿函数解决),但是lambda更加的小巧一点。
lambda本质上我们可以理解成一个匿名函数对象。
为什么呢?首先单单看lambda的话这就是一个函数,和普通的函数比较就多了一个[](捕捉列表下面会将解怎么使用)。因为没有名字,所以在使用的时候,我们通常是赋值个左边的一个对象。
然后sort那里即能够传递仿函数又能够传递lambda是因为这里他们两个都是一个对象,这里你还可以传递函数指针。原因还是在于模板
所以我们平时写的这个lambda是一个对象,他的类型是可以得到的,并且lambda的底层其实还是一个仿函数。
我们来使用typeid来看一下这个lambda的类型是什么。
此时这里我们两个lambda实现的内容都是一样的但是这两个lambda的类型也是不一样的。
那么这到底是个什么东西呢?
这其实就是两个类<>中的内容就是这两个类的名字。
也就是说在底层的时候在语法编译的时候并没有lambda,而是将lambda生成了几个不同的类,类的名字就是<>中的内容。那么f1,f2就是这个类型的对象,而这里我们能够使用f1(),f2()去使用,即在使用lambda生成的类中都是重载了()的。而这个()的函数需要的变量就是lambda()中的内容。函数体就是lambda中函数体里面的内容。
这里的名字很奇怪是因为这里的名字是lambda+uuid
那么什么是uuid呢?
首先 我们这里要保证每一个lambda都是一个独立的类。
为了防止出现一样的类名所以这里是增加了uuid的
这个uuid就能够保证,这里也不深度的去说明这个uuid了
注意的是这里保证的是当前机器唯一。
所以lambda就是帮你完成了一个仿函数。所以在c++中lambda底层就是一个仿函数。仿函数在c++中很重要很多东西底层都是使用仿函数实现的。
在上面我们写的两个lambda对象f1和f2虽然写的时候是一摸一样的但是不能将f1赋值给f2.
原因就是每一个lambda就是一个单独的仿函数。
即lambda对于我们是匿名,对于编译器而言只是一个名字优点奇特的类而已。
下面是lambda的其它用法:(讲解捕捉列表的使用),lambda的其它部分(参数,返回值,函数体和我们普通的函数一样是没有区别的,直接使用即可,了解了lambda的本质之后我们就可以认为这就是给operator()这个函数使用的一部分。)。我们来看下面的情景:
我现在想要使用lambda,完成两个数字的交换:
上面是第一种方法。
下面一种方法:
假设我不想传参数:也想完成交换呢?
当然也可以那就是使用捕捉列表。
下面是堆捕捉列表的一些基本的说明。
什么是父作用域:就是lambda这个函数所在的域。例如下面lambda这个函数所在的域是main函数,所以下面的lambda的父作用域就是main函数。
捕捉列表也就是我能够捕捉我所在的这个域中的变量过来直接使用,捕捉多个使用逗号分割。
但是这么写会出现错误,因为传值捕捉过来的变量是不可被修改的。即捕捉过来的这个变量成了lambda这个类的成员变量。但是这个成员变量是coonst修饰的(即捕捉过来的变量能够使用,但是不能够修改),如果想要能够被修改那就需要使用mutable关键字。
能够直接使用的原因也就能够理解了,类中的函数自然能够使用类的成员变量。
此时这里的调用就不需要调用了,因为operator()在底层使用的使用直接就可以使用捕捉过来的变量(成员变量,因为const修饰所以加上mutable)。
但是运行后的值:
为什么没有修改呢?因为这里捕捉使用的是传值捕捉,即将外面的x和y的值,获得然后传递给lambda类的构造函数然后使用这个值重新开辟空间,所以此时我们在这里修改,改的不是外面的x和y的值,而是这个lambda这个类中新构建的x和y的值。(总结里面的x和y以及外面的x和y不是同一个x和y)。打印地址可以验证
这里就需要使用传引用捕捉。
即将捕捉列表中的x和y替换成了&x和&y,但是这里就出现了这lambda的劣势,就是&这个符号在单独使用的时候还有取地址的作用。即此时的引用和取地址符号混在了。
此时的本质就是在lambda的类中存在两个成员变量,这两个成员变量是外面x和y的别名。因为外面的x和y以及在lambda类中的成员变量本质来说已经处于了两个作用域,所以可以这么写。
所以如果你在这里想拿,x和y的地址,只能将x和y的地址先保存在两个新变量中,然后再去捕捉新变量,才能获得x和y的地址。不能直接捕捉x和y的地址(这里可以看作是一个特殊)。
下面如果我们在这里使用引用捕捉,这里还需不需要加上mutable呢?
答案是不需要,因为当我们传引用捕捉过来的时候,本来的目的就是为了修改外面的某些变量,所以这里不需要mutable(可以看作是一个特殊处理)。
下面一个问题如果这里是一个引用捕捉一个传值捕捉呢?
如果你想要修改还是需要加上mutable,因为上面的规则只是针对于引用捕捉。
这里虽然会感觉无法做到逻辑自洽,但是为了一些其它的用法,是会做一些特殊的处理的。
例如我们曾经学过的迭代器中的重载->.
如果想要逻辑自洽那么我们使用起来应该这么使用
但是如果这么写运算符重载的意义就失去了很多,基于这样的原因,所以编译器牺牲了一下,做了特殊的处理。
对于捕捉还有一个点如果父作用域中存在很多很多的变量,如果一个一个捕捉是很麻烦的。
这里就可以使用下面的关键字
如果这个lambda是在一个成员函数中写的,那么this指针也会被一起捕捉。
同理引用也是:
各种捕捉方式还可以实现混用,例如下面这样:
下面这样使用是不行的:
你已经决定了全部值要使用值捕捉,然后又要求,z要单独使用传值捕捉。这样就重复了,编译器不支持这样玩。
例如你在全局写一个lambda(一般不会这么玩)。
需要注意下面这样写会报错:
因为捕捉只能在父作用域中捕捉,而a1和a2是不在父作用域中的。这里使用a1和a2的方法就是将this捕捉,然后通过this来访问a1和a2。
这里不需要写this->a1和this->a2,可以认为是编译器的特殊处理。
因为lambda的底层就是一个仿函数:
所以都是调用构造然后调用operator()。
下面是c++11中的包装器
在c++11中存在了lambda之后,觉得还是不够好用,所以这里又搞了一个东西叫做包装器。
包装器的本质就是一个适配器。
为什么要存在包装器呢?因为有时候我们需要对可调用对象进行调节。
包装器的使用
首先包装器包装的是可调用对象。
可调用对象有下面的几种类型:
虽然lambda的底层是一个仿函数,但是这里还是需要算上的。
首先包装器能够包装他们三个中的任意一个:
把他们包装成一个统一的。
然后就存在了包装器。
如何写包装器呢?
现在假设这里存在一个lambda一个swap函数一个仿函数,然后这里存在一个map,要求我们实现一个指令去调用不同的函数。
如果你全部都是函数指针那么那么可以写成函数指针那样的类型,但是这里并不全都是函数指针/仿函数。
所以这里就需要使用到包装器了(头文件functional)。
首先包装器可以包装函数指针:
完美的完成了任务。
也可以包装仿函数对象:
也可以包装lambda
也就是包装器自己不是一个可调用对象,你需要给他传递东西(函数指针,仿函数对象,lambda)。
这样我们就能够将可调用对象放到容器中了。
那么就可以完成下面的这种奇怪的用法了。
这里使用了初始化列表完成了对map的初始化。
这就类似于Linux中的bash输入一个指令对于一个进程。
这里就是完成了写一个类型(包装器)能够接收三个不同的对象(函数指针,仿函数,lambda)。
使用function去包装的前提是参数个数类型和返回值是需要相同的,如果参数个数和类型是不相同的呢?那就需要使用其它的方法了。后面存在一个容器,能够储存任意类型。
下面我们使用一道题目来再次使用一下这个function包装函数。
题目是一个逆波兰表达式(如果是操作符就运算,操作数就入栈) 。
因为这道题目这里只存在加减乘除,但是如果下面还想要加入新的运算功能。这里我们使用一个map来改装一下。
有了这个map之后,下面我们就变得轻松不少了。
例如就不需要使用switch case了
这样就能够直接使用str去调用对应的函数了,以后增加功能也只需要在map的那里增加就可以了。代码:
// 使用包装器以后的玩法
class Solution {
public:int evalRPN(vector<string>& tokens) {stack<int> st;map<string, function<int(int, int)>> opFuncMap ={{ "+", [](int i, int j) {return i + j; } },{ "-", [](int i, int j) {return i - j; } },{ "*", [](int i, int j) {return i * j; } },{ "/", [](int i, int j) {return i / j; } }};for (auto& str : tokens){if (opFuncMap.find(str) != opFuncMap.end()){int right = st.top();st.pop();int left = st.top();st.pop();st.push(opFuncMap[str](left, right));}else{// 1、atoi itoa// 2、sprintf scanf// 3、stoi to_string C++11st.push(stoi(str));}}return st.top();}
};
有了map之后我们就不需要使用swait case了。这里使用仿函数也是可以的但是就有些重了。
包装器还有一个作用就是将类型进行了统一,例如某个模板,如果你传递了函数指针,仿函数,lambda三个对象过去,那么就会形成三分不同的函数/类。
但是如果你使用了function进行了包装那么就只需要生成一份函数/类了。
对于包装器而言包装函数指针还有一些特别的玩法。
包装普通的函数指针当然没有问题,但是包装成员函数是存在特别的。
包装成员函数首先因为成员函数在类域中需要先指定类域。
现在我们包装的是一个静态的成员函数但是如果我们包装的不是一个静态的成员函数呢?
然后就会发现这样写无法编译通过的。
需要增加一个&符号
为什么要增加一个这样的符号呢?
因为普通函数函数名就是地址,成员函数需要增加一个&符号,静态的成员函数可以不加,但是建议还是加上。
但是现在还是编译不过为什么呢?
因为在成员函数中还存在一个this指针。
所以可以这么写:
但是这里不能使用匿名对象,因为匿名对象是一个右值,右值是没有办法取地址的。
你还可以这么玩:
为什么这里就可以呢?
这里你可以认为是一个特殊处理。
原因在于使用上面的方法你每一次都要写一个有名对象就很烦。
那么能否不传递这个第一个参数呢(成员函数)(因为第一个是固定的)
由此就衍生出了第二个包装器,绑定(blind)。
第一个用法能够调整顺序。bind第一个作用就是能够替换参数顺序
bind第一个作用就是能够替换参数顺序
代码:
此时就是第一个参数传给了第二个第二个参数传递给了第一个,这样就及那个顺序悄悄换了,这就是参数顺序调整了。
有一些用但是不多。
bind还有第二个玩法:调整参数个数。
可以看到下面的那个代码已经固定了一个参数(第一个参数),所以下面得到的答案是15。本质就是有些参数在我们bind的时候直接被写死了。
这里的意思就是第一个参数已经被我使用20绑死了,如果这里是三个参数的化后面就是后面两个参数就可以使用_1和_2来表示,一样可以做到调换参数的顺序。
此时function的类型就变了,之前的形参是两个现在是一个。调整参数个数是很重要的。
那么根据绑定的这个特点我们就可以完成下面这样的操作,对于成员函数。
后面还存在一个_2.这样把每次调用时都需要传的参数绑死,能够在调用的时候简化调用。
这个函数在系统库函数中是能够使用的,例如某些在系统库中的函数某三个参数是一定需要传递的用于在库函数中某些必须使用的东西,但是和用户使用没有关系的值,然后还存在三个参数交给用户去传递。此时这里就可以将这个接口绑定一下,使用和调用的时候,能够稍微简单一点。
例如在windows上创建进程。
这里的function是一个类模板。
对于这个function我们也不去了解底层,然后这里的_1和_2是一个占位符号,这个占位符号针对的是你传入的参数例如上面的那个,我将第一个参数写死了,接下来就职剩下了一个参数,所以你只能使用一个_1,这就是为什么这个参数针对的是你所传入的参数。
那么如果存在多个参数,能不能直接绑定中间的参数呢?
这样是可行的。此时这里就相当于_1,10和_2和func绑死,而_1和_2由我们传递。
对于绑定成员函数需要注意一下:可以绑对象,也可以绑对象的地址。一般选择绑对象(可以使用匿名对象)。
c++11还有线程库,和智能指针以后会额外写博客。
希望这篇博客能对您有所帮助,写的不好请见谅,如果发现了任何的错误,欢迎在评论区指出,我一定改正。