作者:几冬雪来
时间:2024年3月13日
内容:C++11讲解
目录
前言:
在前不久我们结束了对C++中哈希的讲解,而哈希的完结也代表着C语言和C++学习的一个阶段的结束。应该阶段的结束也必然会有一个阶段的开始,今天我们就来了解和学习一下C++11的知识。
C++11的介绍:
在多年前学习C语言的人中,几乎是所有的人使用的C++的版本为C++98或者C++03等等。哪怕到后来的C++03中,也不过是在C++98的基础上增添一些无关紧要的内容和特性,并没有实质性的改动。
但是随着C++11版本的出现,C语言迎来了一场大变动。
那么什么是C++11呢?对比C++03和C++98,C++11进行了哪些操作呢?接下来的图将一一的为你介绍。
如图对比起C++03与C++98,C++11增加了新的特性并且进行了许多的缺陷修正,同时C++11的版本在效率上也得到了提升。
C++11特性:
在上图那张表中我们也有说到,对比C++03和C++98,C++11更新了140个新的特性。而特性的增多也就意味着有些操作符会发挥出不一样的作用。
那么这里我们就介绍一下有关C++11的一些特性。
{}的列表初始化:
这里先讲解的C++11的特性就是{}的初始化,在以前学习C++又或者是C语言的时候{}也可以为数组或者结构体元素进行统一的列表初始化。
但是这些操作都是在C++98和C++03版本下进行的,那么C++11的特性这里对{}进行了什么修改呢?
如图,在上图左手边的代码图就是C++98或者C++03中花括号(“{}”)的使用方法,一般都是让它进行数组的初始化等操作。
而右手边的代码图就是C++11中的花括号的使用。
在C++11中我们可以看出花括号不仅可以用来初始化数组与结构体元素进行初始化,同时也能进行单个整形的初始化操作,并且在使用花括号进行初始化操作的时候不需要赋值操作符的运用。
这里就是C++11中的一个特性——一切皆可用{}初始化,并且可以不用书写赋值操作符。
并且这里也能对结构体元素进行初始化,在C++11中它们的本质都是调用构造函数。
auto:
然后就是我们在未学习C++11之前经常使用的类型auto。
auto的作用就是使编译器能自动帮我们调用变量的类型。
如上图,变量p接收的是变量i的地址,因此这里auto接收的就是一个整形指针的类型。
auto虽然在学习C++11之前就使用上了,但是在日常书写代码的时候还是会经常被使用到的。
initializer_list:
接下来要讲解的是C++11中的另一个特性——initializer_list。
这里我们先来看两串代码,有些人认为上边的两句函数的同一种语法。
但是其实它们是不同的语法支持的,p1是多参数构造函数的隐式类型转换,因为在Point中只有两个参数,所以这里只能传两个参数。
而v1调用的是构造函数,这里可以调用构造的原因是C++11中新增加了一个类型。
像这里的il就被识别为了initializer_list类型。
这里像v1和il用花括号括起来的列表编译器就可以将其识别为initializer_list类型。
并且initializer_list里面也有一些成员。
这里根据上图我们也可以将initializer_list当成一个容器。
这个地方我们是赋值符号后面的是常量区数组,存在于常量区,如果在书写的时候给予auto让编译器去直到推导它的类型,C++中会把这个类型识别为initializer_list。
同样的在编写代码的时候我们也可以将类型auto换成initializer_list。
因此代码的本质是调用initializer_list的构造函数。
在编译的过程中,编译器特殊支持,它会去取这段空间的开始地址和结束的地址给_start和_finish。
因此我们可以看出initializer_list的底层实际上是两个指针。
而vector能调用构造函数是因为在vector中支持initializer_list的使用,initializer_list中没有规定列表中有多少个值。
这里地方我们借由vector的头文件来构造vector。
但是从图中来看我们并不能这样书写,因为这里依旧将vector的类型识别为initializer_list类型,要想支持就需要提供它的构造函数。
这里提供的就是参数是initializer_list的构造函数。
然后就里面的数据都编译一遍即可,这里可以直接使用迭代器。
decltype:
在下来的第三个特性,这里讲解的是C++11中的decltype。
在学习decltype之前我们要先了解一下typeid。
在上图我们可以看见i的类型是整形,p使用auto类型来推导传址的i,同样的pf也使用auto来推导malloc的类型。
而想要看到p和pf的类型,这个地方就需要使用到typeid来反映它们的类型,代码的书写如上图,typeid后面的括号放我们要查询的变量,最后.name()就能进行查询。
但是这里的typeid仅仅只能用于反映出该变量的类型。
如果想要使用typeid借由pf的类型来定义一个新的变量的话,这个地方的操作是不被编译器所允许的,typeid的作用只能用来拿到这个字符串。
那么这里像借由pf的类型来定义一个新的变量的话要怎么做呢?
这里用pf来定义一个新变量的方法有两种。
第一种是使用auto,让编译器自动帮忙推算,而还有另一种方法就是这里C++11的特性——decltype。
但是使用auto和使用decltype的结果并不一样,在C++中使用auto是进行的赋值操作,不仅仅是定义同时还进行了初始化的操作。但是decltype不一样,这个地方只会定义类型而不会进行初始化操作。
这里的decltype可以推出对象的类型和表达式的类型,又或者是定义变量,还能作为模板实参。
nullptr:
继C++11的auto之后,在这个地方我们有要介绍一个在学C++11之前就使用到的特性,也就是nullptr。
这里的nullptr可以说是为了以前的C++打的一个补丁。
在学习C++11之前,或者学习C语言的时候,经常会把空定义为NULL,但是在C语言时期,NULL不仅代表着空,它可能还表示为数字0。
因此在C++11中为了防止上面的情况发生,加入了nullptr这个特性,它代表的意思是空指针,并没有为0的这个用法。
而且这里会出现这个问题,宏也有一定的责任,因此在后面书写C++代码的话可以使用constenum inline去代替它。
C++11中STL的变化:
在讲解完了C++11的特性之后,接下来我们就来讲一下C++11对STL的影响,使得STL有什么方面的变化。
C++11新容器:
首先就是在C++11中增加了一些成员结构和容器,类似之前学习的unordered_map和unordered_set就是在C++11中新增加的。
这里的array之前也有讲到过,就是一个非类型模版参数的静态数组。
而这里的forward_list是类似于unordered_set于unordered_map的出现,它这里的实现是一个单链表。
但是后二者在编译C++中极少被使用到。
C++11新接口与老接口升级:
在介绍完了C++11中的新容器之后,接下来就是对C++11中的新接口进行介绍。
如图,在学习迭代器的时候我们对这些新接口有些许的了解,像这里的cbegin和cend等都是C++11的新接口。
但是这些新接口的作用在begin和end都能实现,因此这些C++11的新增加的接口我们也几乎不对它们进行使用。
但是并不是C++11的新接口都是没有使用价值的。
在这个地方C++11为所有的容器都增加了emplace系列的接口。
这里的emplace针对于插入操作,也就是所有插入操作都会涉及到emplace系列的接口,类似在insert中就增加了emplace容器,而在puch_back中则是增加了emplace_back容器。
并且在emplace中还涉及了两种语法的作用,一种是右值引用,而且另一种语法则是模板的可变参数。
而C++11并不是只是增加了新的接口,在C++11中连带着老一辈的接口也得到了优化升级。
因为emplace的增加,编译器多出了两种语法,右值引用能做到性能提升的作用。而且我们可以将右值引用运用到push_back或者pop_back中,使它们的性能也得到提升。
在这里所有的容器的变化在于——它们都提供了移动构造和移动赋值。这里就使得有些场景下,深拷贝的性能大幅度提高。
右值引用于移动语义:
在上边我们讲解到了C++11提供的emplace涉及到了右值引用的语法,接下来这里就要对右值引用做一个了解和学习。
首先在编译器中存在右值引用的话,同理也就存在着左值引用。
左值与右值:
首先就是对左值和右值进行一个了解。
什么是左值?什么是右值?这里我们可以依靠一组代码来认识它们。
这里我们通过代码可以看出来,在编译器中类似上图第一句代码,这里的变量a就为左值,而整形值则为右值。
但是并不是说左边的值就是左值,右边的值就是右值这样简单的表示。像第二句代码中,左边的b依旧是左值,但是右边的变量a我们无法定义它是左值还是右值。
这里就总结出来了一个结论,在左边的值一定为左值,但是在右边的值则不一定为右值。
在这里因为可以确定左值,所以先对左值进行了介绍,在编译器中如果该数据我们可以获取它的地址,那么这个值就是左值。
这里的左值并不一定要可以赋值,有一些不能赋值的数据也可以为左值。左值可以出现在赋值符号的左边,也可以出现在赋值符号的右边。
在大致了解完了左值之后,接下来就是对右值进行了解。
这里的右值和左值相反,它不能进行取地址,也不能出现出现在左边。
且右值通常是一个表达式,如字面常量,表达式的返回值等等。
像上面的三组代码都是我们的右值,我们都不能对它们进行取地址的操作。
但是这里有一组代码算例外。
像这里的一串字符串,按我们上边对左值和右值的了解来对其进行判断的话,这个地方大概率会将其定性为是右值。
但是其实不然,在判断左值和右值中字符串有些特殊,这里的代码并不是右值,看似是一个字符串,但是我们可以去取它的首元素的地址,因此这里的字符串被认定为左值。
左值引用与右值引用:
了解左值和右值之后,接下来就是对左值引用和右值引用的学习了。
在学习C语言和C++的时候我们那个时候就接触到了引用这个操作,引用在这里可以理解为取别名。
而这里所说的左值引用我们可以将其直接翻译为——帮左值取别名。
像上边的这句代码就是我们的左值引用的代码。
既然左值引用在这里的意思是帮助左值去别名,那么我们也可以根据左值引用的意思推导出右值引用的作用是帮助右值取别名。
那么给右值取别名的代码是怎么样书写的?
这里就是我们右值引用的书写。
对比左值引用,右值引用的最大区别就是在类型这里使用了两个取地址符号。这也是给右值取别名的手法。
而又因为左值引用和右值引用的代码比较相似,因此也就延伸出来了一个问题,左值引用能不能为右值取别名。
在这里如果是按正常的方法使用左值引用的代码去帮右值取别名的话,这个操作是不被允许的。
类似上图的第一句代码就会报错,但是只需要在类型的前面加上const语法的引用,这里就能做到使用左值引用为右值取别名的操作。
既然左值引用在一些特殊处理下可以为右值进行取别名的操作,那么能不能反过来用右值引用为左值进行取别名的操作?
这里通过上面的图可以看出,如果还是寻常的直接使用右值引用对左值进行取别名是行不通的,这里也要进行一些特殊处理。
像第二句代码一样,如果想要让右值引用为左值取别名,这个地方需要在左值之前添加上一个move。
这个move来源于库中的函数。并且这里的move的使用可能会对它修饰的左值造成一些影响,这些我们到后面再谈。
左值引用与右值引用的重载:
接下来有一个问题,就是左值引用与右值引用会不会构成函数重载?
这里我们依旧取一段代码来进行观察。
这里我们从上边的两张图中可以看出,这里参数为普通的左值引用与右值引用并不会发生冲突,二者可以进行函数重载的操作。
并且这里将左值引用前加上一个const语法,让其左值引用的代码也可以为右值取别名的操作也并不会影响到函数的重载。
同样的,如果这里只有一个const左值引用,代码也是可以正常进行的,这里会走左值引用是因为匹配程度,如果这里有右值引用的重载就走右值引用的重载,如果没有右值引用的重载,但是有const左值引用的重载走走它的重载。
左值引用与右值引用的使用场景与价值:
在介绍完了左值引用和右值引用之后,接下来我们就来讲一讲两种引用作用的场景与它的价值。
首先就是左值引用,一般左值引用都用于做参数与做返回值,而它可以有效的减少拷贝。
但是左值引用还是有一些问题没有解决好。
像这里,我们的左值引用就不能处理这种问题,这里因为str的生命周期到了,它已经销毁了。引用的空间变成了野空间。
然后就是左值引用的处理不当的问题。
如上图,这里我们借助了string的代码进行操作。
这段代码是将func中的值进行执行后返回给ret2,但是在进行这个操作的过程中,它所带来的代价有些巨大。
在这里左值引用的str是一定会被销毁的,因此要让其成功返回给ret2这个地方只能对它进行拷贝的操作。
这里要完成上面的操作就需要进行三次深拷贝,首先最后要返回到ret2,在这个过程中会有一次拷贝构造来存放str的数据。
而str,ret2与拷贝构造都是有指针指向一块空间的,这里要成功对ret2进行赋值。
首先要将str的值给拷贝构造的空间,同时str的空间进行销毁,而后再把拷贝构造的值给给ret2,然后拷贝构造的空间进行销毁。
而这里解决这个问题的方法就是在string的函数中对operator=再写一串代码。
此处下面的这句代码就是新增加的代码,如果值为左值就走上边的重载函数,如果为右值就走下方的重载函数。
纯右值与将亡值:
因为上边在解决左值引用没处理到位的问题的时候,我们提供了一个operator=的函数重载来处理。
而且这里也就引申出来右值引用的一个新的知识——将亡值与纯右值。
简单的来说,纯右值就是内置类型的右值,而自定义类型的右值被称为将亡值。
那么什么是自定义类型的右值呢?
类似这里的运算符重载与函数调用都隶属于自定义类型的右值。
虽然它们看上去是表达式与函数的返回值等,但是其本质都是调用函数,然后生成的临时构造的数据进行传送作为我们的右值,而这个地方的右值就是将亡值,它与匿名对象类似。
依旧拿这张表来举例,将亡值的意思可以理解为它的字面意思。
像上图的赋值给ret2的func如果为右值的将亡值,那么它的生命周期就只在这一行。如果往下走的话,func就会被析构。
移动构造:
在了解完了纯右值与将亡值之后,接下来就是要对讲解右值引用的移动构造。
根据上边的说法,如果变量ret2赋值的是一个左值,那么这里进行的操作就是一个深拷贝的操作,但是如果赋值的是一个右值的话,这里就需要进行一个移动拷贝的操作。
这里的移动构造并不需要和深拷贝一样进行构造与赋值操作,它的做法是将资源进行转移。移动构造将变量的数据拷贝过来的同时,将原先它自己的数据交给变量,让其帮忙带走析构。
在这个地方我们就可以去书写移动构造的大致代码。
这里依旧需要拷贝,但是它的代价远小于深拷贝,并且它是一个将亡值,在给予完了变量的数据后,还要将原先的数据带走。
接下来就是看一下它的代价,如果是在没有移动拷贝代码的情况下,这里进行的是两次深拷贝,一次拷贝构造,一次赋值。
但是如果存在移动构造的话,这里只进行一次深拷贝,外加一次移动拷贝。
这里的右值引用并不是直接起作用的,而是将要拷贝的操作变为不拷贝。这里这样做就能成功的提高它的效率。
接下来我们来看一个场景。
这里在有拷贝构造和移动构造的前提下来看这一串函数。
如果在以前没有进行优化的情况下,函数返回的值str第一步进行的是拷贝构造的深拷贝,第二次是移动构造的浅拷贝,这里的移动构造只启到了一步作用。
而编译器在优化之后,连续的构造/拷贝构造,我们都将其合二为一。而在优化后的合二为一之后,按我们的理解这里的操作可能拷贝构造,因为这里的str是左值,要进行的操作是拷贝构造,而移动构造无法起到作用。
而且为了防止过多的出现这种情况,虽然str是左值,但是它作为func表达式的返回值。在没优化之前是拷贝的对象去做返回值,但是在优化过后,拷贝的对象被优化掉了。
因此编译器进行二次优化,它把str识别为右值的将亡值,并且str也满足将亡值的条件。
如上图,如果编译器没有进行优化,这里要进行的就是一次拷贝构造的深拷贝和一次移动构造的浅拷贝。
但是在优化之后,这里只需要进行一次移动构造就能完成,这也是我们所说的右值引用的提供效率的方法,也是展现右值引用价值的场景之一。
移动赋值:
在讲解完了移动构造之后,接下来就是对移动赋值的讲解。
在学习完了移动构造之后,我们可以同理得出移动赋值的操作。
这里的移动赋值并没有和移动构造一样有合二为一的优化, 但是这里的编译器也进行了特殊处理,把str识别为了右值——将亡值。
这里的中间处,函数会生成一个临时对象,但是因为str被识别为右值,因此这里进行的操作是移动构造。
下来我们来看看它的拷贝的结果。
这里的拷贝次数并没有因为优化而减少,但是它的拷贝代价要降低了许多。
如果没优化之前是两次深拷贝,需要拷贝两段空间后,再去释放久空间,但是这里优化变成了自己转移资源。
完美转发:
在了解完了右值这个概念之后,接下来就来讲解一下完美转发。
而只要学习完美转发之前,需要我们构造出使用完美转发的场景。
在上图的左侧有着左值引用,const左值引用,右值引用与const右值引用,而左边是我们要进行调用的内容。
同时这里也涉及到了一些知识,首先就是右侧的PerfectForward函数中的参数,从图上以及学习过的知识来看,它为一个右值引用。
这里我们可以用这个函数来接收右值,那它是否能用来接收左值呢?按照之前的知识来讲,这种操作理论上是不行的,但是实际上这里也能实现左值引用。
因为函数这里要表达的已经不是右值引用了,在模版中的这种写法不叫右值引用,它有一个全新的名字——万能引用。
这里的万能引用顾名思义,它可以用来接收右值,也能用来接收左值。
这里会出现这种情况是因为它的类型并不是具体的类型而是模版。
如果这里将模版换成一个固定的类型,那么它在这里代表的就只能是右值引用了,因为这里是被写死的。
如果使用模版的话,实参是左值,它就是左值引用,如果实参是右值,那么它就是右值引用,同时我们也能将其称为引用折叠。
如果PerfectForward调用的是一个常量,那么还是就生成一个右值引用,如果调用的是左值则生成左值引用,同理如果是const左值或者右值的话,模版依旧会根据我们的类型进行生成。
但是这里实际上的结果出乎我们的意料。
在终端显示的结果中我们可以看出,哪怕函数中输入的是常数或者右值,最后显示出来的都是左值引用。
接下来就是找到它的问题所在。
在去除了模板并将函数写死之后,我们将main函数中3个无法运行的代码都注释掉,但是剩下两个函数运行通过的结果依旧是左值引用。
这里就要通过这些问题来科普一些知识。
在图中我们可以看到,这里r接收的是变量a,也就是说r为左值,而rr接收的是a的move,也就是rr的右边是一个右值,但是从输出的结果来看,rr和r的地址属于同一个,也就是说明从属性上来说二者都隶属于左值。
这里因为,引用的底层是指针,而move的使用是让改变量的传回的时候传回的值是右值,并不是让这个值的属性变为右值。
右值引用变量的属性会被编译器识别为左值,否则在移动构造的场景下,无法完成资源的转换,必须修改。
回到原先开始的地方,在函数参数中的t的作用是右值引用,但是这个地方t的属性并不是右值,而是左值。
在引用完了的下一层fun函数中t的属性被认定为左值,因此它匹配的也就是左值引用。
如果这里想让这里的变量t保持它原有的属性需要怎么做?这里C++11就提供给了一个东西,也就是接下来讲解的完美转发。
而完美转发和move内容类似,它是一个库里面的函数。
上图就是完美转发的写法,如果t为左值引用,下面的t就保持它左值的属性,反之如果是右值引用则保持右值的属性。
lambda:
在讲解完了完美转发之后,接下来就来讲解一下lambda。
lambda在某种程度上是用来替代函数指针,甚至与在某些场景下可以替代仿函数。
在C++中lambda一般是一个局部匿名函数对象。
而我们的lambda的写法如上,是由捕捉列表,参数列表,返回值和函数体一起构成的。
那么这里要怎么调用lambda?
这里就需要使用到C++11中新加入的特性auto,因为它是一个对象,我们不能将其写死,只能交给编译器去自动推测。
而lamdba一般用于一些排列的场景,就例如在很久之前,对水果的价钱,数量等进行比较排序,在程序员中有些人是不书写注释或者函数名随便取,这样会造成阅读困难。
lambda的出现就是解决了这种问题。
这里还要注意一个点,那就是lambda的返回值通常可以省略。
也就是将->与返回值给省略掉,只留下“{}”中需要return的内容即可。
并且lambda在某些地方可以代替lambda进行使用。
这里如果在没有学习lambda之前,这里如果想用sort对某件物品的价格和评价进行比较后排列出升降序的话,仿函数是必不可少的一个操作。
但是在学习lambda,我们就能用它来代替这个地方的仿函数,这里不需要auto推出来后再传,因为这里lambda本身就是一个对象。
lambda表达式语法:
在初步了解完了lambda之后,接下来就要学习一下lambda的常见语法。
在大概看完了lambda表达式各部分的作用后,接下来就是对lambda的深入讲解好了解。
前面我们对lambda进行了初步的讲解,知道了lambda的来历,lambda的作用还有lambda在C++中的书写形式。
首先就是在这里lambda的返回值,例如上图,如果这里肯定了返回值的函数体的类型是整形,这个地方可以加入整形的返回值。
但是一般lambda是不需要加入返回值的。
第二个要注意的点就是接收的类型,这个地方也是一定要使用auto去推测lambda传递过来的类型。
接下来就是lambda的另一个问题,那就是lambda的函数体中能不能实现多语句。
显然,如上图,lambda是可以完成多语句的书写的,只不过在书写的过程中一般要将每个语句单独书写为一行,这样能够方便阅读。
接下来还有一个问题,那就是既然lambda中可以实现多语句,那么能不能在原本的lambda中再加入一个函数呢?
根据我们测试出来的结果来看,lambda的函数体中不仅可以写入多语句,并且也能写入函数,但是写入的函数要分两种情况。
如果写入的函数位于局部的话,这里编译器会继续报错,但是如果函数是位于全局的话,编译器是可以正常运行的。
捕捉列表:
在什么说到再lambda中可以写入函数,但是写入的函数有要求存在,也就是只有位于全局函数写入lambda才不会报错,而局部的函数不能直接写入lambda中。
那么要如何将处于局部的函数写入到lambda中,这里我们采用了一种叫捕捉列表的操作。
如图,这里的add1和add2都是进行的传参,将实参传递给形参,然后让lambda对形参进行操作。
这个地方新书写了一个变量,这里如果想将变量交给lambda里面去计算的话,直接在函数体中加入中国值是行不通的。
如果想使用这个值就需要使用到捕捉列表。
这里的书写方法就是将想要使用的词写入到“[]”内,这样就能在函数体中对其进行使用,而且“[]”也被称为捕捉列表。
而这个操作可以理解为lambda捕捉了rate,也能对这个变量进行直接的使用。
在讲解完了捕捉列表的书写方法后,接下来就来看看捕捉列表的几种捕捉方式。
首先就是刚刚示范的捕捉列表,这种捕捉列表的操作被称为传值捕捉。
与之相对的,有传值捕捉的话,也就有传引用捕捉,相比较传值捕捉,传引用捕捉传的对象大小要更加的大。
像这里我们不想在参数列表中进行传参的操作,但是还是想把变量x和y进行交换的话,这里就能直接将x与y写入捕捉列表中。
同时,因为参数列表中没有数据,所以它也可以省略不写。
但是从代码上的结果来说,我们确实借助捕捉列表实现了参数的传递,但是函数体内无法对x与y进行修改。
这是因为捕捉列表的x与y并不是原先的变量x与y,这里lambda调用的时候是需要建立栈帧的,是一个实际的函数调用,类似传值调用。
而因为传值调用的缘故,这里的x与y都是const类型的。
如果实在想对其进行使用,在这里可以应用一个单词mutable。
这里mutable代表着异变的意思。
如果因为捕捉的对象是const属性不能进行修改,那么加入一个mutable就可以对对象其进行修改了。
而mutable书写的位置则是位于参数列表和函数体的中间。
在加入mutable之后,我们的程序可以正常运行了,但是还是不能对x和y进行交换,主要原因在于mutable。
matable只是对其进行了异变,简单来说只处理了const的问题,但此处依旧是传值调用,因此这里就要回到原先的地方。
为了解决这个问题,就需要lambda的传引用捕捉去完成。
如果在捕捉列表中,想要传递的变量过多的话,这里就能使用捕捉列表的另一种方法,也就是在捕捉列表中加入一个“=”。
这样就能做到捕捉所有的变量,同理这里也可以用引用传递来进行捕捉,不同与值传递的捕捉列表中加入“=”,引用传递的捕捉则是在捕捉列表中加入“&”进行操作。
并且在这里两种引用捕捉并不是只能单独进行,像上面的两种捕捉全部的方法就能结合起来一起使用。
而且它们的写法也是十分的独特。
像这里就是传值捕捉和传引用捕捉的结合,这里捕捉列表中先是写入了&符号,再在它的后面书写变量a。
这里所表达的意思就是就是除了变量a使用传值捕捉以外,其他的变量都使用传引用捕捉进行操作。
从下面的结果也能看出来因为a是传值捕捉,因此不能对其进行修改,而其他变量是传引用捕捉可以进行修改。
如果想使用lambda去调用函数的话,也只需要将函数名写入到捕捉列表中即可。
同时在书写lambda内容的时候需要注意一个点,那就是两个lambda表达式之间不能相互赋值,即使它们看起来类型相同。
可变模板参数:
在C++11中的接下来我们要学习一个知识,也就是可变模板参数,在C++11中可变模板参数很少被使用到,但它也是C++11中的一个语法。
在日常使用的库中printf等就是C语言中的可变参数。
类似这里的printf就是函数的可变参数,我们可以借助printf打印出多个变量,在语法层面也可以将其计算为一次特殊处理。
这个地方printf的底层会用一个数组将参数存放起来,在编译器编译的时候会根据实参的数量开设一个数组,而printf则会去访问数组后依次将其取出。
函数的可变参数在C语言中就有被使用,到了C++11中就需要模板的可变参数,但模板参数和函数参数可以说十分相似。
区别在于函数的参数传递的是多个对象,而模板的参数则是多个类型。
这里包装器的书写的方法就是在template后边加入<class ...Args>或者<typname ...Args>,这里的Args是可以随便取名的,但是这里一般取名为Args,因为它是英文参数的缩写。
Args就是参数包,而args是参数值。
如果在传递时候只传递了一个参数,那么参数包中就只有一个类型,而且下边的(Args ...args)则使用这个类型定义了一个形参,如果参数包中有两个类型则用两个类型定义两个对象,它们是互相对应的。
这里就是可变模板参数,这里的参数包包含着0~n个参数。
这个地方结果的数量总比参数的数量少一个的原因是因为在这里的第一个参数传给了value,如果想让其相等,也可以将value去掉,但是这样会做到无法取出它的值。
那我们怎么去取到那些值呢?
这里如果想要取出args中的值,直接使用for循环加下标的方式是没办法做到的,要想取出它的里面的值,唯一的方法就是使用递归的方法。
因为value的存在,因此第一个参数传给value,后面的参数传递给参数包,接下来再走ShowList函数,直到参数只剩下一个参数,这个时候就只需要走最上边的函数,接下来进行递归操作取值即可。
包装器:
在上边了解和学习完了lambda之后,我们能发现lambda虽然在有些方面十分的好用,但是它还是有它的劣势,也就是类型的控制上。
而面对C++中存在各种各样的可调用类型,如函数指针,仿函数与lambda,而为了统一控制这些类型,有人就设计出来了包装器。
function:
这里就写一段代码来举例一下。
这里F为模板,而f为可调用对象,而上边说到这里的可调用对象可以是函数指针,仿函数与lambda的其中一种。
接下来再写一下函数指针,仿函数与lambda的代码。
这里的函数名指的就是函数指针。
而在调用useF的时候可以传函数指针,或者仿函数和lambda表达式,因为F的类型不同,因此它的函数模板1会被实例化成3份。
而会出现上述问题主要就是类型并没有统一的结果。
处理这个问题的方法就是采用包装器function,而function的作用就是将函数指针,仿函数与lambda表达式进行包装(打包)。
因为函数模版实例化的个数过多,因此就需要C++11提供包装器function来解决,而且书写function之前,应该先书写function的头文件。
其次,它是一个类的声明,是一个类。第一个模板参数Ret代表的是返回值的类型,Args是模板的可变参数。
在这个地方function是一个包装器,包装器的本质可以理解为一种适配器,也就是说它可以对我们的函数指针,仿函数与lambda进行相关的包装。
那么function该如何使用?
这样的书写方式就可以将包装器写出来。
这里要使用function就要去声明它的返回值和参数,如果返回值和参数都是double类型,则这里的返回值Ret与可变参数都书写为double类型。
这里就使用了包装器定义f1,f2和f3包装了函数指针,lambda表达式与仿函数。
在以前如果想把可调用对象存放到vector中基本是不可能做到的事情,vector只能用于存放可调用对象的函数指针。
而使用了包装器之后,vector就能借助包装器来进行使用了。
这里我们借由for循环取vector达到每个对象,而vector的每个对象是包装器,包装器中又包装函数指针,lambda和仿函数。
同时我们还有一种书写方法。
在这个地方在初始化的时候不用创造一个包装器的对象,就像图中的f1的包装器就是对lambda进行包装。
这里就解决了可调用对象的类型问题,在这之前,虽然我们有lambda,函数指针等可调用对象,但是可调用对象的类型并不好写,尤其是可调用对象必须声明的时候,就比如将可调用存放到容器中。
如果没有包装器,在容器中写入函数指针,它调用的就是函数指针,并不会去调用另外两者。而书写了包装器的容器后,三则都可以存放进去,这就是包装器的价值bind
bind:
在讲解完包装器function之后,接下来我们来了解一下包装器的另一个词——bind。
bind也位于function这个头文件中,它也是一个包装器,而bind的作用在这里是用来调整参数的。
具体是怎么做的呢?
比如这里有一个函数,而函数中要实现参数a减去b的操作,接下来想实现两个参数互换,达成b减去a的操作,这个地方就需要使用到bind。
同样如果要使用到bind的话,这里就离不开placeholders,placeholders在此处是一个命名空间,而且命名空间中声明了标识符。
就类似图中placeholders的后边加入“::”操作符之后,它会给出很多“_”加上数字的内容,这里的placeholders:: _1就代表了第一个参数,同理_2,_3等也代表着参数。
在下面就能书写要传递的值去给编译器计算。
这里bind的底层就类似于传递,在下边的函数中,常量10与5就分别传给了bind中的placeholders::_1与placeholders::_2,而后二者又将依次其传递给a与b完成计算。
想完成参数颠倒的技术的话,bind在这个地方的做法就是将bind中的placeholders::_1与placeholders::_2二者调换顺序。
它们二者的传递的顺序会发生改变。
在没有修改顺序之前,函数中的10对应的是bind中的placeholders::_1而后传给a,5对应的是placeholders::_2后传给b。
但是当placeholders::_1与placeholders::_2修改顺序之后,这里的10依旧传递给的placeholders::_1,但是不一样的是当placeholders::_1传递的时候对应的不是a而是b,同理placeholders::_2也是如此。
再在接下来有一些场景,如果在这个地方我们不想传个别参数呢?
如图,该函数有三个参数,此处如果正常传递就正常的使用bind,但是要是这个地方只需要使用到其中两个参数要怎么做?
而这里要实现上面操作的话,只需要修改bind中的参数个数即可,在placeholders::_1与placeholders::_2的后面再加上一个常量。
而后下面的函数传参就只需要写两个参数,placeholders::_1与placeholders::_2的传值逻辑的操作还是一样,不同的是Plus中的double类型的rate直接由bind中的参数传递。
但是这个地方要注意的一个地方。
如上这个地方如果将两个要传递函数的参数放在不去书写的参数rate后面的话,这里bind的书写方法就需要进行修改,要传递的常量就要放在placeholders::_(数字)的前面。
这个地方就衍生出来了一个问题,那就是placeholders::_(数字)的前边已经存在一个参数了,placeholders::_(数字)的数字要从多少开始。
这里从图中来看,即使在bind中placeholders::_(数字)的前面已经有参数存在,这里要传递的参数依旧从placeholders::_1开始书写。
这是因为固定参数不参与这里的排序。
再接下来还有一个场景。
如图,如果这里的函数是静态成员函数的话,bind包装器又应该怎么去书写?
这个地方如果在bind中直接书写函数名sub后编译器是会发生报错的,这里要书写函数就要去声明它的域。
其他函数能成功的运行是因为它们都位于全局域。
如同,去声明它的域后,在这里我们才能对域中的函数进行使用。
同样的传递的函数是非静态成员函数,且函数中的参数个数比写入的参数个数要多一个,这种情况下代码又应该如何去书写呢?
如图,调用的函数是非静态成员函数,在参数列表中有3个形参,但是这里写入形参的时候是书写两个,因此bind中还是要加入一个形参。
但是从图中的结果来看,这样子书写编译器并不能成功的运行起来。
在这个地方首先要解决的问题就是取地址符号,如图在声明域之前加上一个取地址(&)符号来解决这里的问题。
这是因为C++中规定了非静态的成员函数取地址之前要加入取地址符号,同时这里静态的成员对象也可以选择加上该符号。
但是从上面来看这里哪怕进行了一处修改,代码依旧是编译不过。
这里会有这样的问题主要原因是因为这里是非静态成员函数,如果是静态成员函数,这里的参数个数为3个,但是是非静态成员函数的话,此处的参数个数应该为4个。
所以这里要借助SubType定义一个对象,然后在bind中将其一起传递过去。
同时这里也可以使用传对象和指针的方法将其传递过去。
这里可以这样做的原因在于bind的底层是一个仿函数,所以这个地方可以这样子实现。
代码:
#include<iostream>
#include<vector>
#include<map>
#include<string>
#include<functional>
using namespace std;
//struct Point
//{
// int _x;
// int _y;
//};
//
//struct Point
//{
// Point(int x,int y)
// :_x(x),
// _y(y)
// {
// cout << "Point(int x,int y)" << endl;
// }
//
// int _x;
// int _y;
//};
//
//int main()
//{
// int x = 1;
// int y = { 2 };
// int z{ 3 };
//
// int a1[] = { 1,2,3 };
// int a2[]{ 1,2,3 };
//
// Point p0(0, 0);
// Point p1 = { 1,1 };
// Point p2{ 2,2 };
// return 0;
//}
///
//int main()
//{
// vector<int> v1 = { 1,2,3 };
// Point p1 = { 1,1 };
//
// auto il = { 10,20,30 };
// cout << typeid(il).name() << endl;
//
// //bit::vector<int> v2 = { 1,5,6,7,4 };
// return 0;
//}
//int main()
//{
// int i = 10;
// auto p = &i;
// auto pf = malloc;
//
// cout << typeid(p).name() << endl;
// cout << typeid(pf).name() << endl;
//
// auto ptr = pf;
// decltype(pf)pg;
//
// return 0;
//}
void fimin(int a, int b)
{
}
//int main()
//{
// double x = 1.1, y = 2.2;
// 10;
// x + y;
// fimin(x, y);
//
// "xxxxxx";
//
// return 0;
//}
//int main()
//{
// //左值引用
// int a = 0;
// int& r1 = a;
//
// return 0;
//}
//int main()
//{
// //右值引用
// int&& r5 = 10;
// double x = 1.1, y = 2.2;
// double&& r6 = x + y;
//
// return 0;
//}
//int main()
//{
// int& r2 = 10;
// const int& r1 = 20;
//
// return 0;
//}
//int main()
//{
// int a = 0;
// int b = 1;
//
// int&& r1 = a;
// int&& r2 = move(b);
//
// return 0;
//}
//void func(const int& r)
//{
// cout << "void func(const int& r)" << endl;
//}
//
//void func(int&& r)
//{
// cout << "void func(int&& r)" << endl;
//}
//
//int main()
//{
// int a = 0;
// int b = 1;
// func(a);
// func(a + b);
//
// return 0;
//}
//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;
//}
//
模板——万能引用,既可以接收左值也能接收右值
如果具体类型则是写死的PerfectForward(int && t)
//template<typename T>
//void PerfectForward(T&& t)
//{
// //完美转发,t是左值引用,保持左值属性
// //完美转发,t是右值引用,保持右值属性
// fun(forward<T>(t));
//}
//
//int main()
//{
// PerfectForward(10);
//
// int a;
// PerfectForward(a);
// PerfectForward(std::move(a));
//
// const int b = 8;
// PerfectForward(b);
// PerfectForward(std::move(b));
// return 0;
//}
//
int main()
{
int a;
int& r = a;
int&& rr = move(a);
cout << &r << endl;
cout << &rr << endl;
return 0;
}
//struct Doods
//{
// string _name;
// double _price;
// int _evaluate;
//
// Goods(const char* str, double price, int evaluate)
// :_name(str),
// _price(price),
// _evaluate(evaluate)
// {
//
// }
//};
//
//int main()
//{
// vector<Goods> v = { {"pingguo",2.1,5},{"xiangjiao",3,4},{"chengzi",2.2,3} };
//
//
// return 0;
//}
//void func()
//{
//
//}
//
//int main()
//{
// int a = 0, b = 2;
// double rate = 2.5;
// auto add1 = [](int x, int y)->int {return x + y; };
// auto add2 = [](int x, int y) {return x + y; };
// auto add3 = [rate](int x, int y) {return (x + y) * rate; };
//
// cout << add1(a, b) << endl;
// cout << add2(a, b) << endl;
// cout << add3(a, b) << endl;
//
// auto swap = [](int& x, int& y)
// {
// int tmp = x;
// x = y;
// y = tmp;
//
// //cout << add1(x, y) << endl;
// func();
// };
//
// return 0;
//}
//int main()
//{
// int x = 0, y = 2;
//
// auto swap1 = [&x, &y]()
// {
// int tmp = x;
// x = y;
// y = tmp;
// };
// swap1();
//
// int a = 0, b = 1, c = 2, d = 3;
// auto func = [&, a] {
// a++;
// b++;
// c++;
// d++;
// };
//
// return 0;
//}
//template<class F,class T>
//T useF(F f, T x)
//{
// static int count = 0;
//
// return f(x);
//}
//
//double f(double i)
//{
// return i / 2;
//}
//
//struct Functor
//{
// double operator()(double d)
// {
// return d / 3;
// }
//};
//
//int main()
//{
// cout << useF(f, 11.11) << endl;
//
// cout << useF(Functor(), 11.11) << endl;
//
// cout << useF([](double d)->double {return d / 4; }, 11.11) << endl;
//
// function<double(double)> f1 = [](double d)->double {return d / 4; };
// function<double(double)> f2 = f;
// function<double(double)> f3 = Functor();
//
// //vector<function<double(double)>> v = { f1,f2,f3 };
// vector<function<double(double)>> v = { [](double d)->double {return d / 4; } ,f2,f3 };
// double n = 3.3;
// for (auto f : v)
// {
// cout << f(n++) << endl;
// }
//
// return 0;
//}
//template <class T,class ...Args>
//void ShowList(T value, Args... args)
//{
// cout << sizeof...(args) << endl;
//}
//
//int main()
//{
// ShowList(1);
// ShowList(1, 2);
// ShowList(1, 1.1, 2.2);
//
// return 0;
//}
int Sub(int a, int b)
{
return a - b;
}
//double Plus(int a, int b, double rate)
//{
// return (a + b) * rate;
//}
double Plus(double rate, int a, int b)
{
return (a + b) * rate;
}
class SubType
{
public:
static int sub(int a, int b)
{
return a - b;
}
int ssub(int a, int b, double rate)
{
return rate * (a - b);
}
};
int main()
{
//function<int(int, int)> rSub = bind(Sub, placeholders::_1, placeholders::_2);
function<int(int, int)> rSub1 = bind(Sub, placeholders::_2, placeholders::_1);
function<double(int, int)> Plus1 = bind(Plus, 4.0, placeholders::_1, placeholders::_2);
/*function<double(int, int)> Plus2 = bind(Plus, placeholders::_1, placeholders::_2, 4.2);
function<double(int, int)> Plus3 = bind(Plus, placeholders::_1, placeholders::_2, 4.5);*/
cout << Plus1(5, 3) << endl;
/*cout << rSub1(10, 5) << endl; */
SubType st;
function<double(int, int)> Sub = bind(&SubType::ssub, &st, placeholders::_1, placeholders::_2, 3.3);
function<double(int, int)> Sub = bind(&SubType::ssub, st, placeholders::_1, placeholders::_2, 3.3);
function<double(int, int)> Sub = bind(&SubType::ssub, SubType(), placeholders::_1, placeholders::_2, 3.3);
return 0;
}
结尾:
到这里我们的C++11的内容就基本讲解完了,从讲解C++11开始到C++11完全讲解完毕,中间经历了很长的一段时间,但是最后还是讲解完了,时间跨度太长,可能要更多的进行对C++11内容的复习,同时C++11中也存在许多需要学习的内容,最后希望这篇文章能太郎些许帮助。