C++笔记-C++11(三)
紧接上文,C++11还有一些知识我们需要了解一下:
5.新的类功能
5.1默认的移动构造和移动赋值
原来C++类中,有6个默认成员函数:构造函数/析构函数/拷⻉构造函数/拷⻉赋值重载/取地址重
载/const 取地址重载,最后重要的是前4个,后两个⽤处不⼤,默认成员函数就是我们不写编译器
会⽣成⼀个默认的。C++11 新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
如果你没有⾃⼰实现移动构造函数,且没有实现析构函数 、拷⻉构造、拷⻉赋值重载中的任意⼀
个。那么编译器会⾃动⽣成⼀个默认移动构造。默认⽣成的移动构造函数,对于内置类型成员会执
⾏逐成员按字节拷⻉,⾃定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调⽤
移动构造,没有实现就调⽤拷⻉构造。
上面看着有三个函数,但是我们之前也说过这三个函数一般都是同时出现的,出现析构函数,说明这个类中有资源需要释放,自然就要写拷贝构造和拷贝赋值,变相地降低了我们的记忆成本。
下面我们来看一个例子:



前面两个例子我就不多说了,主要看最后一个,此时在Person类中我们并没有实现 析构函数 、拷⻉构造、拷⻉赋值重载中的任何一个,此时就会生成默认的移动构造函数去调用string类中的移动构造函数。
结果也正如我们所言,确实调用了string的移动构造。


而如果写了三个函数中的一个,比如我上面写的拷贝构造函数,那么就不会生成默认的移动构造函数,而是会走Person类中的拷贝构造,进而去调用string中的拷贝构造,结果也证实了这句话,确实没有调用string中的移动构造,而是调用了拷贝构造。


移动赋值和移动构造一样,在没有三个函数时会去调用相应的移动赋值。


相应的有这三个函数中的任意一个,也就不会生成默认移动赋值,会去调用相应的拷贝赋值。
5.2成员变量声明时给缺省值
成员变量声明时给缺省值是给初始化列表⽤的,如果没有显⽰在初始化列表初始化,就会在初始化列表⽤这个缺省值初始化,这点我们在类和对象就已经讲过,这里就不过多赘述了。
5.3default和delete
C++11可以让你更好的控制要使⽤的默认函数。假设你要使⽤某个默认的函数,但是因为⼀些原因
这个函数没有默认⽣成。⽐如:我们提供了拷⻉构造,就不会⽣成移动构造了,那么我们可以使⽤
default关键字显⽰指定移动构造⽣成。
我们来看一个例子:



和上面一样,此时我们运行就如上图所示,只会调用拷贝构造函数,那么我们要想让其调用相应的移动构造呢?
我们上面讲的生成默认的移动构造是一个办法,那么还有什么办法呢?

如上图所示,我们可以利用default关键字来强制生成一个Person类的移动构造函数,这样也能调用string中的移动构造。
而default的作用也就体现出来了,就是强制生成一个函数,当然不局限于上面的移动构造,可以时构造函数等等。
而delete与它正好相反,不让编译器强制生成某个函数:

就比如输入输出流,我们可以看到,在ostream中是不允许生成拷贝构造函数的,因为如果能拷贝构造会出现很多复杂的问题。
6.lambda
6.1lambda表达式语法
lambda 表达式本质是⼀个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。lambda 表达式语法使⽤层⽽⾔没有类型,所以我们⼀般是⽤auto或者模板参数定义的对象去接收 lambda 对象。
我们之前还学过的函数指针,仿函数也都是可调用对象,现在又多了一种lambda。
lambda表达式的格式: [capture-list] (parameters)-> return type {function boby }。
capture-list: 捕捉列表,该列表总是出现在 lambda 函数的开始位置,编译器根据[]来 判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下⽂中的变量供 lambda 函数使 ⽤,捕捉列表可以传值和传引⽤捕捉,具体细节7.2中我们再细讲。捕捉列表为空也不能省略。
parameters: 参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连
同()⼀起省略。
-> return type: 返回值类型,⽤追踪返回类型形式声明函数的返回值类型,没有返回值时此
部分可省略。⼀般返回值类型明确情况下,也可省略,由编译器对返回类型进⾏推导。
{function boby }: 函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以
使⽤其参数外,还可以使⽤所有捕获到的变量,函数体为空也不能省略。
我们来看一个简单的lambda例子:


上面就是写了一个简单的lambda来实现两个数相加的结果,使用起来就和仿函数一样。有人会感觉lambda的格式和我们之前学的传统的格式不太一样,看起来就不像是C++的代码,这种感觉没错。
如果学过python,就会感觉很熟悉,C++这个语法就是抄的python,所以才会显得这么奇怪,而C++11过后,我们称C++11之前的C++是传统C++,C++11及之后我们称为现代C++,也是因为C++也在不断地从其它语言中汲取新的语法。
回归正题,这个语法的格式我们要记清楚,我们上面也提到了参数列表和返回值可以省略:


从上图我们也可以看到即使不传,程序也能正常运行,这点的话看个人习惯,懒得写就不写,不过该写的时候还得写。
6.2捕捉列表
上面的lambda我们只介绍了[],并没有给出怎么用,下面我们来看捕捉列表该如何使用:
lambda 表达式中默认只能⽤ lambda 函数体和参数中的变量,如果想⽤外层作⽤域中的变量就
需要进⾏捕捉,而捕捉方式我们分为三种。
第⼀种捕捉⽅式是在捕捉列表中显⽰的传值捕捉和传引⽤捕捉,捕捉的多个变量⽤逗号分割。[x,
y, &z] 表⽰x和y值捕捉,z引⽤捕捉。


上面的例子中我们就使用了传值捕捉和传引用捕捉,a为传值捕捉,b为传引用捕捉,并且传值捕捉是不能修改的,就相当于传过来的a被const修饰。

第⼆种捕捉⽅式是在捕捉列表中隐式捕捉,我们在捕捉列表写⼀个=表⽰隐式值捕捉,在捕捉列表
写⼀个&表⽰隐式引⽤捕捉,这样我们 lambda 表达式中⽤了那些变量,编译器就会⾃动捕捉那些
变量。



如上就是第二种捕捉方式,这种方式写起来更为简便,写个=或者&,想用哪个就用那个,不过还要注意用=的话,依旧是不能修改所用的变量的,&可以修改。
第三种捕捉⽅式是在捕捉列表中混合使⽤隐式捕捉和显⽰捕捉。[=, &x]表⽰其他变量隐式值捕捉,
x引⽤捕捉;[&, x, y]表⽰其他变量引⽤捕捉,x和y值捕捉。当使⽤混合捕捉时,第⼀个元素必须是
&或=,并且&混合捕捉时,后⾯的捕捉变量必须是值捕捉,同理=混合捕捉时,后⾯的捕捉变量必
须是引⽤捕捉。
这种方式就是满足比如说我用=来捕捉,但是其中的某个变量我想用引用,反之&也是一样的情况。


这里我就用了=来捕捉,但是a和b我想用引用来对其进行修改,用上面这种方式即可,另一种情况我就不一一列举了。
lambda 表达式如果在函数局部域中,他可以捕捉 lambda 位置之前定义的变量,不能捕捉静态
局部变量和全局变量,静态局部变量和全局变量也不需要捕捉, lambda 表达式中可以直接使
⽤。这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空。


这样用是没问题的,但是:


如果将全局变量或者静态局部变量写入其中的话就会报上面的错误,说的没必要写是你写了也不行。
有人可能会有疑惑:那传值捕捉的值就没有办法在函数体内对其进行修改吗?
有的, 默认情况下, lambda 捕捉列表是被const修饰的,也就是说传值捕捉的过来的对象不能修改, mutable加在参数列表的后⾯可以取消其常量性,也就说使⽤该修饰符后,传值捕捉的对象就可以 修改了,但是修改还是形参对象,不会影响实参。使⽤该修饰符后,参数列表不可省略(即使参数为 空)。

如上图所示,在参数列表后面加上mutable关键字,就可以对a进行修改,正如上面所说,参数列表是不能省略的,即使为空。

不加参数列表就会直接报错。
说了这么多,那么lambda有什么用呢?我们接着往下看:
6.3lambda的应用
在学习 lambda 表达式之前,我们的使⽤的可调⽤对象只有函数指针和仿函数对象,函数指针的
类型定义起来⽐较⿇烦,仿函数要定义⼀个类,相对会⽐较⿇烦。使⽤ lambda 去定义可调⽤对
象,既简单⼜⽅便。
lambda 在很多其他地⽅⽤起来也很好⽤。⽐如线程中定义线程的执⾏函数逻辑,智能指针中定
制删除器等, lambda 的应⽤还是很⼴泛的,不过这都是后话了,以后讲到了我们再说。
我们来看一个例子:




比如上面我们定义一个商品类,我们如果想对商品进行排序,可以用我们上面的仿函数写法用价格来排序,也可以按评分来排序,不过我没写,这样写没问问题。
但是我们设想一下,如果未来在工作中,别人写的代码就比如这个,仿函数的名字给你定义成Compare1和Compare2,并且还没写注释,那你接收这个代码时是不是就很难受,即不知道是按升序排还是按降序排,也不知道是按什么值来进行比较,并且仿函数写起来也还得定义一个类。
这时候我们lambda的优势就显现出来了,我们来看:

此时我们利用lambda这样写,即简洁,又清楚明了,一看就知道是用什么比较的,并且是按升序还是降序也能一眼看出来。
我们通过调试也可以看出,确实是按照升序和降序来排的,我们也可以按照评分来排:
这次我们就按照评分来进行排序,当然我们上面也讲了lambda的应用并不是只有这一点,后面还有很多。
讲了应用,那么lambda的原理是什么呢?
6.4lambda的原理
lambda 的原理和范围for很像,编译后从汇编指令层的⻆度看,压根就没有 lambda 和范围for
这样的东西。范围for底层是迭代器,⽽lambda底层是仿函数对象,也就说我们写了⼀个
lambda 以后,编译器会⽣成⼀个对应的仿函数的类。
仿函数的类名是编译按⼀定规则⽣成的,保证不同的 lambda ⽣成的类名不同,lambda参数/返
回类型/函数体就是仿函数operator()的参数/返回类型/函数体, lambda 的捕捉列表本质是⽣成
的仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda 类构造函数的实参,当然隐式捕
捉,编译器要看使⽤哪些就传那些对象。
这个规则呢,我这里提一下,叫做uuid,大家感兴趣的可以去了解一下。
7.包装器
7.1function
function 是⼀个类模板,也是⼀个包装器。 function 的实例对象可以包装存储其他的可以调⽤对象,包括函数指针、仿函数、 lambda 、 bind 表达式等,存储的可调⽤对象被称为function 的 ⽬标 。若 function 不含⽬标,则称它为 空 。调⽤ 空 function 的 ⽬标 导致抛 bad_function_call 异常。

如果要用function这个语法就要包含<functional>头文件。
函数指针、仿函数、 lambda 等可调⽤对象的类型各不相同, function 的优势就是统⼀类型,对他们都可以进⾏包装,这样在很多地⽅就⽅便声明可调⽤对象的类型。
下面我们来看示例:




function的基本应用就如上图所示,用它要注意的就是它的格式,()外的int是返回值类型,()里面的两个int就是参数类型,而正如我们上面所说,function统一了它们的类型,就相当于在它们外面套了一层壳,实际传参后调用的还是它们本身。
并且要注意类型必须相同,也就是返回值类型和参数类型,否则就会报错。
我们上面演示了全局函数的用法,那类内的函数如何接收呢?



对于类内的静态成员函数,要使用的话要制定类域,并且在前面要加上&,然后就可以使用了。


而对于类内的普通成员函数,我们之前讲过,类内普通成员函数的参数中都有一个隐藏参数this指针,所以我们还要传一个参数,在上面我用了两种方式指针和对象,两种方式都是可以的。
为什么两种方式都可以呢?
虽然看着是传参给function,其实是传参给我们要调用的函数,我们在之前的类和对象中就讲过,如果过要调用类内的成员函数,对象和指针皆可以调用。
8.2bind
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的⼀个命名空间中。

有几个参数就有几个占位符,话不多说,我们来看一些示例:



首先我们要先声明_1,_2和_3,不然不能用,这点要注意。
格式就如上图所示,结合着auto来用,注意:_1就代表传入的第一个参数,_2代表传入的第二个参数,这很关键,与下面我们讲的知识有关。
上面我们说了bind可以用来调整参数顺序和参数个数,我们先来看调整参数顺序:


可以看到我们再调整过参数顺序后,结果也发生了改变,原因就如我上面所说的_1就代表传入的第一个参数,_2代表传入的第二个参数,_1还是1,_2也还是2,但是a和b就发生了改变,a变为2,b变为1,所以结果发生了改变,不过改变参数顺序用的不多,主要还是用它来调整参数个数。


什么叫调整参数个数呢?正如上图所示,其中的一个参数我直接进行赋值,就是把这个参数给绑定死了,所以我传参的时候传一个参数即可。


这里就是将第二个函数的三个参数依次绑定,算出的结果也不尽相同。
而我们学了bind之后,上面的调用类内函数的代码就可以这样进行优化:


老是传对象,看着也不舒服,直接传参数多好,所以我们就可以把this指针这个位置的参数给绑定死,这样我们再调用类内成员函数时就不需要再传对象或指针了。
以上就是C++11(三)的内容。