目录
1、{}初始化
2、auto
3、decltype
4、nullptr
5、范围for
6、STL容器
7、右值引用
①左值引用和右值引用
②移动构造
③移动赋值
④万能引用与完美转发
8、新的类功能
9、可变模版参数
10、lambda表达式
捕捉列表的使用
[val]:传值捕捉
[&val]:传引用捕捉
[=]
[&]
混合使用
11、包装器
function包装器
bind
C++11其实就是C++03的下一个版本,在2011年完成了C++标准,所以起名为C++11
而C++增加了140多个特性,下面只对最常用的一些特性做以说明
1、{}初始化
C++11规定一切皆可{}初始化
第一种是我们经常使用的,而二三种就是C++11规定的,=也可以省略
关于结构体:
可以看到三种方式都调用了Time的构造函数初始化
关于vector、list等容器的初始化:
可以看到是非常方便使用的,不需要我们一个一个数据去push_back了
原因是C++11增加了一个默认容器initializer_list,花括号括起来的可以当做是initializer_list的类型
并且C++11给所有容器的构造函数增加了用initializer_list初始化
list为例:
所以上面我们用花括号括起来的数据可以直接初始化vector、list等容器
容器方面对于initializer_list还有其他更方便的用法:
例如上面所举例的Time结构体,我们以前如果有一个vector<Time>,只能如下图初始化:
现在使用initializer_list就可以进行下面的方式初始化:
同样在map中,也就不需要一次次的insert了:
也是非常的方便
所以最后说明一点:C++11的一切对象都可以列表初始化,但是一般的对象还是按照以前的方式初始化,使用容器时有需求可以适当使用列表初始化
2、auto
auto我们是经常用到的,可以自动识别类型
说起auto,就要想到范围for
auto一般使用的场景是一个变量的类型非常长,可以使用auto简便些:
从上图可以清楚看出两种方法的难易程度
但是在一定程度上auto也降低了代码的可读性,有的用户清楚unordered_map的begin返回值是什么,如果不太清楚的用户就只能通过查找资料从而确定it的类型
3、decltype
decltype可以推导一个对象的类型, 再去定义变量
例如:
b对象的类型就是a的类型int
4、nullptr
nullptr的出现是为了解决早年定义的NULL出现的bug
因为早年C++中NULL被定义为字面常量0,而0既能表示为指针常量,又能表示成整型常量
所以为了更加安全的角度考虑,增加了nullptr作为空指针
5、范围for
上面也提到过范围for,可以与auto联合使用
需要知道范围for的底层是迭代器就可以
6、STL容器
C++11新增了容器unordered_set和unordered_map
相比于set和map在效率上有很大提升,在哈希章节有提到
还有两个容器:array、forward_list
①array
array是静态的顺序表,vector是动态的顺序表
C++11创建出array这个容器,初衷是为了取代C语言中的数组,想要让C++更加容器化
例如:
arr1和arr2代表的是一样的含义
二者的区别是,C语言中的数组越界读一般检查不出来,越界写是抽查处理,有时会被检查出来
而array不论读或写,只要越界就能被检查出来
而array比C语言数组多了一个检查越界的作用却被用得不多有两个原因:
第一、人们C语言数组都用的很顺手,用习惯了,突然改变一个用法不太习惯
第二、如果想要类似功能,完全可以用vector + resize完成
②forward_list
我们学过的list是双向链表,而forward_list则是单向链表
并且forward_list提供了两个函数insert_after和erase_after
insert_after是在当前位置的后面插入,因为如果是在当前位置的前面插入的话,时间复杂度就变为O(N)了,因为需要找到当前位置的前一个结点
而erase_after的用法是删除掉当前位置的下一个位置的结点,理由与insert_after一样
所以这个forward_list设计的思路很勉强,并不好用
并且正常情况下,都会使用list,list相比较而言只是比forward_list多了一个prev指针,也不会在效率上慢多少
上面说的是C++11新创建的几个容器,下面说说容器内部的改变:
①、容器都支持initializer_list构造,支持初始化
②、提供了几乎没有用户的接口,例如cbegin、cend系列的接口
③、移动构造和移动赋值,提高效率
④、右值引用参数的插入,提高效率
其中的第③④点,在下面有具体讲解
7、右值引用
①左值引用和右值引用
首先需要知道的是:左值引用就是我们之前所学习的引用,并且无论左值引用还是右值引用,都是给对象起别名
关于左值:左值是可以获得它的地址或是可以对它赋值,但是定义时const修饰的左值是不能赋值的,但是仍然可以获得它的地址,所以左值特点是可以取地址的
像上图的a、b、pa、*pa都是左值
而左值引用就是对左值去别名:
aa、bb、ppa、ret就是给左值起的别名
右值不能出现在赋值符号的左边,也不能取地址
例如:字面常量、表达式返回值、函数返回值(非左值引用返回值)等
如下所示:
这三种分别对应上面所举的三种例子,都是右值
而右值引用就是对右值起别名(符号是&&):
上图的n1、n2、n3就是给右值起的别名
并且左操作数必须是左值,不能是右值,右值不能被修改,(左值中被const修饰的也不能被放到左边)像下面这种写法就会报错:
左值引用不能引用右值,const左值引用可以
例如:
因为左值引用本身就是可以被修改的,而右值是不能被改变的,所以给左值引用加上const就不能被改变,所以就不会报错了
这里的const引用的意义就是:有些函数是引用传参的,例如函数Func(T& num),这时只能传左值不能传右值,加上const,变为Func(const T& num),就左值和右值都能传入Func函数了
右值引用不能引用左值,但是可以引用move(左值)
引用的价值就是减少拷贝
左值引用在做参数和做返回值有以下好处:
做参数:a、减少拷贝,提高效率 b、做输出型参数
做返回值:a、减少拷贝,提高效率 b、引用返回,可以修改返回的对象(例如map的operator[])
但是也有些情况无法使用左值引用:
例如string中的to_string函数,string to_string(int val)
to_string函数传入一个整型val,返回的string是一个临时对象,离开作用域会被销毁,不能使用引用返回
C++98中给出的方法是to_string(int val,string& str),这样也能够提高效率,但问题就是不符合用户的使用习惯,相当于改变了函数的使用方法
因此to_string使用传值返回,效率不高,会发生深拷贝,并且如果遇到的返回值是vector<vector<T>>,效率会更低,所以引入了右值引用,可以解决上述问题
②移动构造
内置类型右值:纯右值
自定义类型右值:将亡值
例如上面所提到的to_string函数,它是传值返回:假设to_string函数return str,main函数中有一个ret接收这个值:string ret = to_string(1234);
这里原始逻辑是str拷贝构造给一个临时对象,这个临时对象再拷贝构造给ret,进行了两次深拷贝
编译器可以做以改进,变为了一个拷贝构造,相当于str直接拷贝构造给了ret
下面引入了右值引用后,string有了移动构造,即:string(string&& s),移动构造函数的参数是右值引用,移动构造的实现方法就是在移动构造内部调用swap(s),将原始的string和这个将亡值s做交换
有了移动构造后,同样有原始逻辑和编译器改进后的逻辑,原始逻辑调用两次移动构造,编译器改进后调用一次移动构造
移动构造就是将这个将亡值与我们自己的string对象swap,相比于深拷贝效率大大提升
③移动赋值
有移动构造自然就有移动赋值,还是以string举例:
普通的拷贝赋值是string& operator=(const string& s)
移动赋值是:string& operator=(string&& s)
移动赋值的实现也是在函数中使用swap(s),这个操作不仅交换将亡值s,进行资源移动,获得想要的结果,还将自己不需要的资源给了将亡值,可以在函数结束后帮助释放不需要的资源
所以原本需要进行深拷贝,深拷贝的代码是比较大的,现在有了移动构造和移动赋值后,只需要移动资源即可,代价很小,不需要深拷贝,效率大大提升
下面可以验证编译器在特定清空是使用移动构造的:
调试时可以观察到,还没有运行228行时,s1s2是正常的,都是"hello"
228行s2被move了,所以s2就变为了右值,当运行完228行后,可以观察到s2为空了,这就是因为调用了移动构造,在移动构造中s2s3进行了swap,所以s2资源转移给了s3,且s3得到了s2的资源,在生命周期结束后释放了s2的资源,所以s2就为空了
在学习了右值引用,明白了移动赋值和移动构造后,上面的to_string的传值返回问题就得到了解决,使用右值引用,相当于进行资源转移效率极高,并不会像之前一样出现深拷贝问题造成效率不高的问题,所以解决了传值返回的问题
正因为C++增加的右值引用的效率很高,所以每个容器的构造和赋值函数中都新增了右值引用的方法,下面列举几个:
等等
不光赋值和构造函数使用了右值引用,STL容器的插入接口也提供了右值引用版本
例如push_back、insert
右值引用的使用,使得在插入过程中,如果传递对象是右值,同样可以进行资源转移,减少拷贝,提高效率
④万能引用与完美转发
关于万能引用举个例子:在函数模版中,参数使用右值引用,这时称之为万能引用,也叫引用折叠
例如上图的函数模板,参数部分t是右值,func函数重载,满足哪个func的参数要求,就进入哪个func,用来验证传入t的属性
传参时无论左值还是右值都能传入Test中,且传入后都会变为左值引用,即原本是左值或右值,传入Test后,都变为左值;原本是const左值或const右值,传入Test后,都变为const左值
而如果想要保持t引用对象的属性,这时就要引入完美转发的概念,即在Test中,使用t时加forward<T>,如下图:
完美转发后,符合传入t对象的属性
8、新的类功能
C++11前,有6个默认成员函数,这6个函数我们不写编译器会自动生成:
构造函数、析构函数、拷贝构造函数、拷贝赋值函数、取地址重载、const取地址重载
C++11后又多了2个,变为了8个默认成员函数:
移动构造函数、移动赋值运算符重载
移动构造和移动赋值就是针对右值拷贝的,这两个新增的默认成员函数自己生成的条件和上面六个函数不同,如下:
①、如果自己没有实现移动构造函数,并且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个,编译器才会自动生成一个默认移动构造。
默认的移动构造中,对于内置类型完成值拷贝,自定义类型的成员则需要看这个成员是否实现了移动构造,如果实现了就调用移动构造,如果没实现就调用拷贝构造
②、移动赋值和移动构造非常类似
如果自己没有实现移动赋值重载函数,并且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个,编译器才会自动生成一个默认移动赋值。
默认的移动赋值中,对于内置类型完成值拷贝,自定义类型的成员则需要看这个成员是否实现了移动赋值,如果实现了就调用移动赋值,如果没实现就调用拷贝赋值
也有一个关键字,可以强制生成:default,例如强制生成Date类的移动构造:
Date(Date&& d) = default
如果不想让编译器执行某个函数,也有个关键字delete可以实现,例如不想让Date实现拷贝:
Date(const Date& d) = delete
上面的default和delete关键字功能是C++11新增的功能,了解即可
final和override关键字在继承那一节说到过用法,也是C++11提供的新功能
final:修饰一个类不能被继承,修饰虚函数不能被重写
override:放在子类中,检查子类的虚函数是否重写,没重写会报错
9、可变模版参数
可变参数这个概念在printf、scanf就出现过
表示不知道需要几个参数,所以语法中就给了可变参数这个概念,即表示零到多个参数
所以就有了一个基本可变参数即函数模版:
template<class ...Args>
void test(Args... args)
Args是一个模版参数包,args是一个函数形参参数包
参数包可以包括0到任意个模版参数
如上图,可以往test中传入不同数量、不同类型的参数,test中的sizeof...(args)是计算传入了几个参数,需要注意sizeof的格式,三个点在括号前面
10、lambda表达式
lambda又叫匿名函数
lambda底层其实就是一个仿函数的类型
lambda表达式书写格式:
[capture-list] (parameters) mutable -> return-type { statement }
capture-list是捕捉列表,parameters是参数列表,return-type是返回值类型,statement是函数体实现
我们可以发现,lambda的组成和函数差不多,相比于函数少了函数名,多了捕捉列表
->return-type:返回值类型,在明确返回值类型的情况下,一般都会省略,由编译器进行推导
parameters:参数列表,无参的时候可以省略
mutable:可以取消常量属性,也就是可以被修改,因为lambda函数总是一个const函数,平时也不太需要,可以省略,但是在使用mutable时参数列表不能为空
capture-list(捕捉列表)和statement(函数体)是不能省略的
所以最简单的lambda是:[]{};即该lambda不做任何事情
下面举一个简单的lambda的例子,简单的减法:
返回值->int是可以省略的,也可以写做auto Sub = [](int a, int b){return a - b; };
如果函数体实现需要多行代码,也可以像平常写函数一样,一行一行展现,例如交换函数:
图中的三种方式都可以,将函数体实现分成多行展现,可读性更强
下面举个使用mutable的例子:
通过观察可以发现,我们在使用lambda函数时,将参数列表为空,在捕捉列表中写了a,b(需要注意的是捕捉列表里的变量必须是同一个作用域里的才能捕捉到),然后在()后面加上了mutable
如果不加mutable是不能实现swap函数的,因为lambda函数是一个const函数,const是不能交换的,所以想要交换需要加上mutable
但是最终观察打印结果发现a和b的值并没有交换,原因是:加上mutable后变量可以交换,但是也只是在这个swap函数里面交换a和b,并不影响外面的a和b
因此上面总结时说到,一般不使用mutable都会省略,因为在大部分情况下mutable确实没有起到作用
lambda函数在使用仿函数的情况下也非常有用,例如有一个学生的结构体,里面有姓名、年龄、身高等属性,我们想要使用sort进行排序,是需要传入仿函数的,因为我们可以排序的参考项很多,姓名、年龄、身高都可以作为排序的参照,所以按照以前使用的方法,如果排列方法都想拥有,需要写六个仿函数,分别是姓名、年龄、身高的升序和降序,并且这六个仿函数还需要考虑起名的问题,如果名称起的不好也会造成使用方面的问题
如果使用lambda函数,直接在sort中传入lambda即可,例如:
上图中,第一个红框就是按年龄升序排列,第二个红框就是按身高降序排列
并不需要考虑这几个仿函数的名称怎么取比较合适这种问题,可读性是比较高的
捕捉列表的使用
[val]:传值捕捉
传值捕捉相当于拷贝捕捉过来的,上面的swap例子就是传值捕捉
[&val]:传引用捕捉
上面所举例子最终并没有成功交换a和b,需要改变捕捉列表才能实现:
在捕捉列表中,a和b前都加上了&,表示传引用捕捉,就能够成功交换a和b了
[=]
[=]表示值传递的方式捕捉作用域中所有的变量,包括this(当前函数中的,包括全局变量)
[&]
[&]表示引用的方式捕捉作用域中所有的变量,包括this(当前函数中的,包括全局变量)
混合使用
[=, &x, &y]:表示以引用的方式捕捉x和y,值传递的方式捕捉其他所有变量
[&, a, b]:表示以值传递的方式捕捉a和b,以引用的方式捕捉其他所有变量
需要注意的是,不允许这种写法:[=, x, y],因为=本身就已经表示以值传递的方式捕捉所有变量,x、y表示的意思也是以值传递的方式捕捉,所以就重复这个含义了,不允许
同样[&, &a, &b]也是不被允许的,所以这里的混合使用,必须前后是不同的捕捉方式才能被允许
这里除a外,剩下的变量都是引用捕捉,所以可以改变大小
11、包装器
function包装器
function包装器是在头文件functional里的,类模板原型是这样的:
template class Ret, class... Args>
class functionRet(Args...)>;
其中Ret是返回值类型,Args是参数的参数包
看下面的样例引入包装器概念:
有个函数模板Board,里面有个静态变量num,++num与&num用来判别实例化了几份对象出来
通过观察结果:
可以发现函数模版Board实例化了三份对象,因为每一个静态成员num的地址都不同
那么如果想让这个函数模板只实例化一份对象,就需要用到包装器了
这里插入一点,有一个概念叫事件响应,类比于游戏中的体现就是按一个键就会释放对应的技能,如果将这个键当做string存入map中,那所发生的这个响应就是相当于要调用某个函数,但是这里的函数可能是很多种样式的,map的第二个类型不好传入,所以这里就需要用包装器包装一下,无论什么类型都可以统一传入
function包装器需要传入返回值类型和参数包
如下所示:
function<int(int, int)>,其中int(int, int),第一个int是返回值类型,括号中的是参数包
如果想包装类中的成员函数:
静态成员函数直接使用类作用限定符::即可
普通成员函数不光需要类作用限定符::,还需要在前面加上&,且传入时需要多传入一个对象进去,因为成员函数并不能直接调用,需要一个对象才能调用
所以在理解了包装器的使用后,我们上面实例化三份的代码就可以引入包装器:
分别给Board函数模版传入的第一个参数变为了f1、f2、f3,观察运行结果:
可以发现num的地址都一样,且每次++num也都是上一次++后的值,说明只实例化出了一份
所以包装器就是无论你可调用的对象是函数指针、函数对象、lambda表达式对象等等,都可以包装成统一的类型
bind
bind其实就是一个函数适配器
原型如下:
fn就是可调用对象,args就是参数包
bind里定义了一个命名空间placeholders,表示占位
其中的_1、_2、_3...就代表绑定对象的形参,即_1、_2、_3分别代表第一个、第二个、第三个...形参
使用_1、_2时,可以placeholders::_1,如果觉得太长了,可以using namespace placeholders,将这个placeholders命名空间展开,这样就可以在代码中直接使用_1、_2了
bind可以改变参数顺序:
可以看到,原本是5-2==3,经过bind,将_1,_2更换位置,再执行bindSub时就变为2-5==-3了
_1代表第一个参数,_2代表第二个参数,_1,_2更换位置就表示第一个和第二个参数更换位置了
bind可以改变参数个数:
上面说到过如果想包装类中的普通成员函数,需要多一个参数:
如果我们想要类中的Sub函数使用时也和Add函数一样,不需要额外传一个参数,这时用bind就能做到:
我们知道每次调用f1时第一个参数都必须是一个对象,因为成员函数需要对象才能调用,所以使用bind时传入Cal(),匿名对象,绑定这个参数,然后再传入_1、_2,第一二个形参,这样就可以做到在调用时,不用多传入一个参数,直接传入数值即可
需要注意的是,bind只能绑定死固定的参数,这里的Cal()每次使用Sub时都要传,所以可以绑定
下面这种情况也适用:
Mul函数有三个参数,其中第三个参数num是固定的,也想像上面的Sub和Add函数一样使用,就可以使用bind,将第三个参数直接给定数值绑定死,这样使用f3时,就不需要传入第三个参数了