目录
一、C++11的概念
在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),用C++03这个名字取代了C++98,使得C++03成为C++11之前的最新C++标准名称。不过由于C++03只是在C++98标准的基础上对漏洞进行修复,语言的核心部分没有改动,所以一般会将C++98和C++03合称为C++98/03标准。
C++11,则是在C++98之后的一次对C++的大量改动。相较于C++98/03,C++11带来了极大的变化,其中包括了大约140个新特性以及对C++03标准中约600个缺陷的修正,这使得C++11更像是一个从C++98/03中孕育出的一种新语言。stl库就是C++11才引入的。
相比较而言,C++11能更好地用于系统开发和库开发,语法更加泛化和简单化,更加稳定安全。不仅功能更强大,而且能提升程序员的开发效率,在实际中也使用的比较多。当然,在这篇文章中不可能将C++11的所有新特性都一一阐述,所以这里就主要是讲解一些C++11中新增的比较重要和好用的新特性。
二、统一的列表初始化
1.{ }初始化
在C++98中,标准允许使用花括号"{}"来对数组或结构体进行统一的列表初始化:
但到了C++11之后,C++11扩大了用花括号进行列表初始化的使用范围,使其可用于所有的内置类型和用户自定义类型。使用初始化列表时,可添加等号(=),也可以不添加。
这也就是说,在C++11后,不止数组和结构体可以用{}初始化,变量也可以了。并且可以不使用赋值符号:
可以看到,在支持C++11的编译器下, 结构体、数组和变量都可以不带赋值符号使用{}初始化。
当然,在实际中,并不建议大家像上面那样对变量用{}初始化和省略赋值符号,因为这样写虽然没有问题,但是会导致代码的可读性降低。大家只需要在看到有人这样写了后能够看懂就行,不需要修改像上图那样写。
当然,上面的那些用法都不重要。重要的是,有了{}列表初始化后,我们就可以更方便的对new出来的空间初始化了:
如果没有{}列表初始化,这里要初始化p1所指向的结构体数组就会比较麻烦。但上图中直接使用了{}后,就能非常方便简洁的完成初始化了。如果大家使用的vs2013,可能这里会无法初始化,原因是2013对C++11的特性支持的不太好,可能会有bug。
2.initializer_list<int>
大家在已经应该见过如下的代码。这个代码运行后可以初始化对应数量的类型空间。
但是大家有没有想过,为什么vector可以支持这样使用呢?在vector的构造函数中也并没有支持这么的参数的构造啊。其实这是因为,这一连串的数据“1, 2, 3, 4, 5, 6”都被识别成了一个“initializer_list<int>”类:
打开文档,查看这个类:
所以,在C++11之后,用{}进行初始化时,一般都会被识别为这个类。对于这个类,大家可能把它看成一个容器,底层就是一个常量数组。返回的是数组的起点指针。
而vector支持用这种方式进行初始化,是因为它在C++11中新增了一个支持用initializer_list<int>进行构造的构造函数:
因此,在支持了initializer_list<int>构造的类中,实例化对象时使用{},如果能和构造函数匹配,就是调的构造函数,如果不能匹配,就会被识别成一个类。当然,库中的很多容器都支持了这种方式的构造。由此,就有了很多初始化方式,如下图:
可以看到,只要支持了用initializer_list<int>构造, 就能够用{}嵌套初始化了。这种初始化方式就比以前方便了很多。甚至如果你愿意,只需要给自定义类加上使用initializer_list<int>的构造函数,就也能支持这种方式的初始化了。
三、decltype
decltype关键字会将变量的类型声明为表达式指定的类型。也就是说,使用这个关键字,在声明一个变量的类型时,无需写具体类型,可以使用一个表达式。
大家应该学过typeid().name(),这个函数只能用于获取类型的字符串。这个字符串几乎没有任何作用,除了打印出来明确某个变量的类型:
而decltype可以看成typeid().name()的升级版,它可以自动推导出传入的表达式的类型,并且可以将结果直接用于其他变量:
当然,在正常情况下,我们肯定不会这样去声明一个变量的类型。因此,这个关键字一般用于无法确定变量的具体类型时使用,例如函数的参数是模板参数:
在上面的情况中,因为test()函数有两个不同的模板参数,所以无法确定经过计算后的返回值的类型,此时就可以使用decltype关键字来声明类型。
当然, 虽然decltype看起来很不错,在某些场景下用起来也很舒服,但是类型场景一般是很少见的。因此,decltype关键字的使用也是很少的。
四、lambda表达式
1. lambda表达式的出现原因
大家应该都接触过C中的函数指针,函数指针这个东西用起来也比较麻烦,因此很多人都不是很喜欢用。
上图中就是一个函数指针。很多人可能以为这个函数的返回值是void,其实这个函数的返回值是一个函数指针。因为当一个函数指针的返回值为函数指针时,就需要将它的返回值写到函数指针名的后面。
当然,这只是函数指针的一种用法,在实际中,函数指针还有一些其他的用法。由于函数指针的使用不仅很麻烦,有时可能还不易看懂,所以在C++11中就推出了lambda表达式,以期望替代函数指针。当然,lambda表达式依然无法取代函数指针。
2. lambda表达式的使用
一个lambda表达式的书写格式为:[capture-list](parameters)mutable->return-type{statement}
大家可能不是很明白这个表达式中的各个部分的含义。这里就来解释一下:
2.1 捕捉列表
[capture-list]:捕捉列表。该列表位于lambda表达式的起始位置,编译器通过[]来判断接下来的代码是否为lambda函数。在[]内可以填入在这个表达式之前的变量,以示捕捉该变量供lambda表达式使用。
在捕捉列表中的变量,必须在这个lambda表达式之上:
在捕获列表中的变量,其实是原变量的一份拷贝,修改捕获列表中的变量不会影响到原变量的值:
如果想修改原变量,就要用引用捕捉:
这一点上其实和普通函数有点相似。传值给参数,在函数体内修改对应变量不会影响原变量;传引用给参数,在函数体修改对应变量会改变原变量的值。
在捕捉列表中,只有传值捕捉和传引用捕捉两种方式。传值捕捉无法修改原变量;传引用捕捉可以修改原变量。
[val] :值传递方式捕捉变量val
[=]:值传递方式捕获所有父作用域中的变量(包括this)
[&val]:引用传递捕捉变量val
[&]:引用传递捕捉所有父作用域中的变量(包括this)
捕捉列表是不允许重复捕捉。例如如果用了“=”全体传值捕捉,就不能在用处传值捕捉捕捉已经被捕捉的变量:
但是,捕捉列表允许混合捕捉,即传值捕捉与引用捕捉混合使用:
这种捕捉的含义就是在上面的作用域的所有变量除了a变量以传值方式捕捉外,其他变量全部使用传引用捕捉。
注意,捕获列表中的值是捕捉过来的变量,不能在捕获列表中修改变量的值:
2.2 参数列表
(parameters):参数列表。参数列表其实就和普通函数的参数列表一致,用于传需要在函数中使用的参数。如果没有参数需要传递,可以连同()一起省略。
2.3 mutable
mutable:在默认情况下,lambda函数中的捕获列表的参数是const修饰的,mutable可以取消它的参数的常性。当添加了这个修饰符后,参数列表不可省略,即使参数列表为空,也要写上()。
可以看到,当lambda表达式捕获一个变量后,如果修改这个变量,是不被允许的。因为lambda表达式中捕获列表中的参数是const修饰的,不允许修改。
要修改就必须加上mutable取消常性:
但是也是有例外的。如果参数列表中捕获的变量不是外部变量的拷贝,而是外部变量的引用,就可以修改:
有人可能以为这里是传地址,因为在以前,“&”在变量名前都是取地址,在类型的左边时才是引用。在这里虽然没有类型,但依然是传引用。通过以引用的方式捕捉,就可以在不加mutable的情况下修改值。
这里是引用不是取地址的原因是,这里并不是函数中的传参,而是捕捉,捕捉上面的变量以供lambda表达式使用。在捕捉中,只有两种方式——传值捕捉和传引用捕捉。没有传地址捕捉的概念
2.4 返回值类型
->returntype:返回值类型。简单来讲,->后面的类型就是这个lambda表达式的返回值类型。无论该lambda表达式是否有返回值,这个内容都可以不写。编译器会自动推导。
2.5 函数体
{statement}:函数体。就是该lambda表达式所要执行的内容。在这个函数体里面,不仅可以使用它所提供的参数,还可以使用捕获列表所捕获的变量。
2.6 使用方式
在这些组成部分里面,捕捉列表和函数体都是必须写的,其他内容可写可不写。因此,一个最简单的lambda表达式就是[]{}。该lambda表达式没有任何作用
有了上面的知识,如果我们想写一个比较两个值的lambda表达式,就可以写成:
返回值可写可不写,如果你想方便看返回值,也可以写成:
有了这个lambda表达式,我们怎么调用呢?很简单,使用auto即可:
要注意,lambda表达式其实是一个可调用对象,底层就是一个匿名对象。同时,lambda表达式虽然有类型,但是用户是写不出类型的,由编译器自动生成。所以只能用auto+变量名来接收lambda表达式或直接使用。
用auto+变量名接收后,会生成一个仿函数对象。因此,在下面的调用中其实就是在调用一个仿函数对象。
3. lambda表达式的使用场景
假设现在有如下程序:
假设这里要求对这个vector中的数据的所有内容都进行一次升序和降序排序,这里可以使用sort来排序。但是,由于它是要求按照不同的标准进行排序,这就要求我们写出不同的比较标准。每种标准都要写两个函数以供升序和降序。在这种情况下,有些人就可能写出如下的代码:
使用的代码命名风格无法体现出这个函数的具体作用是什么,这可能就会导致在阅读代码时,难以明白该仿函数的具体作用是什么,有注释的情况下还好,如果没有注释,就每次都需要上翻查看函数体中的内容,阅读起来非常不方便。
在这种需要使用的函数功能重复度高,且需要写多个的情况下,就可以使用lambda表达式:
运行该程序:
当它运行完第一次排序后,这里就是一个升序排序。如果继续向下运行,这里就是降序排序,这里就不演示了。
因为lambda表达式可以看成一个匿名对象,所以在可以传对象的场景下,都可以直接使用lambda表达式
4. lambda表达式的底层
lambda表达式的底层其实就是一个仿函数。要证明这一点也很简单。先准备如下一个程序:
在这个程序中,有一个仿函数对象和一个lambda表达式。运行该程序后,转到反汇编分别查看r1和r2的汇编:
虽然我们看不懂反汇编,但是可以看看上图中圈出来的位置。这里面的call其实就是在调用函数。可以看到,r1和r2的call其实本质都是在调用一个“operator()”,这其实就是一个仿函数。因此,通过查看汇编的方式,也就能够知道,lambda表达式的底层其实就是生产了一个仿函数并调用该仿函数。
上文中也说过,lambda表达式的类型是由编译器自动生成的,用户是无法知道的。为了验证这一点,我们在上面的程序的基础上再增加一个lambda表达式:
运行起来后查看反汇编:
从这里可以看到,这两个lambda表达式的类型分别为<lambda_1>和<lambda_2>。是不同的。在我的编译器中类型是从1开始,但是在其他编译器中的lambda表达式类型可能就不是从1开始。不同的编译器的处理方式可能有所不同。
五、可变参数模板
1. 可变参数包与函数形参参数包
C++11的新特性可变参数模板能够让我们创建可以接收可变参数的函数模板和类模板。当然,由于可变模板参数比较抽象,使用起来也需要一定的技巧。在当前,我们只需要掌握一些基础的可变参数模板特性即可,在未来如果有需要,大家可以继续深入学习。
这个可变参数模板其实对标的就是C语言中的可变参数。我们以前经常使用的printf()的参数其实就是可变参数:
这里的“...”其实就是指可变参数。
在C++11中提供的可变参数模板就比较奇怪。它分别提供了一个可变参数模板包和一个函数形参参数包。
在声明模板参数包时,要将...放在参数包前面;但在使用时,却要将...放到模板参数包后面,看起来就就会比较奇怪。
在stl容器中的很多接口都是使用了可变参数模板的:
如果想用引用,就可以用“&&”。右值引用,且是在模板中,这就是一个“万能引用”,既可以接收左值,也可以接收右值:
2. 获取参数包的模板参数个数
如果想获取一个可变参数模板的参数个数,可以使用“sizeof...(函数形参参数包)”:
注意,这里的sizeof后面的...是不能被省略的:
3. 获取参数包中的参数的方法
根据大家以前学习C++的经验,可能想当然的认为参数包中的参数可以通过下标的方式获取:
确实,如果可以通过下标的方式获取参数包内的参数,就会非常方便。但实际上,这种获取方式是无法编译通过的, C++中并不支持。
要获取参数包中的内容,就需要要解析这个参数包。这里有两种解析方法。
3.1 增加模板参数
第一种解析方法,就是在模板中新增一个模板参数。
这样看大家可能看不懂。其实它的逻辑很简单。showlist有一个模板参数和一个模板参数包。当向模板中传数据时,会先将将第一个参数传给T,剩余的其他参数传给...Args。此时,val就会得到第一个参数的值。当再次调用该函数时,将参数包中的参数传过去。此时,参数包中的第一个参数被传给T,剩余参数被传给...Args。重复如上动作,直到传一个空的参数包,此时showlist()中没有参数,匹配到上面的无参showlist(),打印换行符。
通过如上增加一个模板参数的方式,就可以以类似递归的方式将所有参数解析出来。
3.2 参数包展开
第一种方式虽然可以获取参数包中的参数,但是在库中使用了可变参数模板的地方并没有增加一个参数模板:
如果我们也想达到这种效果,就可以使用参数包展开:
在这里,就是用一个数组来接收参数。在这个数组里面的“(PrintArgs(args), 0)”是一个“逗号表达式”,它的值取右边的最后一个值。所以在这里,是用0来初始化这个数组。而这个逗号表达式后面跟着...,这就表示这个数组的大小就是这个参数包中参数的个数。而这个逗号表达式在计算时就会去执行PrintArgs(args),获取这个参数包中的第一个参数。
可以看成,参数包中有几个参数,就会用0来初始化几次这个数组中对应位置的值。每次初始化时都会去计算逗号表达式以实现调用PrintArgs(args)来获取参数。
当然,如果大家嫌麻烦,也可以不用0来初始化,而是不初始化直接调用PrintArgs(args):
但要注意,如果用这种写法,PrintArgs(args)就需要带返回值,让这个数组使用PrintArgs(args)的返回值来进行初始化。
可以看到,这种方式如果不带返回值,数组a无值可初始化,就会出现报错。
如果大家想更直观的看到这里是用PrintArgs(args)的返回值来初始化,我们可以写出如下程序:
通过调试,就可以看到数组a中存储的值,和预期相符:
对于可变参数模板,大家现在只需要能够看懂和能够用就可以了。因为一般来讲,我们基本不会自己写可变参数模板,只会碰到需要使用的场景。
4. 容器中的emplace接口
认识了可变参数模板后,就可以来看看容器中的emplace接口了。在stl库的很多容器都提供了emplace开头的接口:
这些接口有两个特点,一个是采用“完美引用”传参,另一个就是使用了可变参数模板。
上面的几个接口都是对应容器中的插入接口。它们与传统的push_back()和insert()接口相比,在传左值的效率上并没有什么区别。但是在传右值时,就会有点优势。
因为emplace接口使用的是右值引用,所以它在插入数据时会将传入的右值视为将亡值,直接交换双方的数据,而不是像传统接口那样传左值,通过拷贝来获取数据。
emplace接口的优势其实也仅仅体现在传右值时会有一点点的性能提升上。其他并没有什么优势。
要注意,emplace接口虽然使用了可变参数模板,但是它并不支持传多个参数:
可以看成这是编译器检查的结果,不允许它传多个参数进去。
当然,如果是传pair<class K, class V>的数据,在传统接口中,我们必须要自己构造一个pair对象传进去;但使用emplace接口就可以不用自己构造,而是让编译器去构造:
总的来讲,虽然emplace接口看起来很不错,在特定情况下,比如传二维数组这种需要深拷贝的数据时会有性能优势外,其他方面与传统接口相比并没有什么差别。
六、function包装器
1.function的含义
function包装器,也叫做适配器。C++中的function本质上是一个类模板,也是一个包装器。那么包装器有什么作用呢?先来看以下一段代码;
在上面的useF函数模板中,一共实例化出了三个函数。运行该程序:
可以看到,这里三个count的地址都是不一样的,这也就说明这三个count是三个不同的变量,即三个不同的函数实体中的同名变量。
那如果我们不想像这样实例化三个函数,而是只实例化出一个函数呢?此时就可以使用包装器:
运行该程序:
可以看到,此时count的地址就是一样的,说明只实例化出了一个函数。
2. function的用法
function本身也是一个类模板,要使用就需要包含<functional>头文件:
function一般都是使用第二个:
在这个类里面,Ret是一个模板参数,表示返回值;...Args是参数包,表示该函数里面的参数。
但是它的使用和普通类有点区别,看起来有点像模板特化。使用时是按照“function<Ret(Args...)>”的格式来使用的:
括号左边的是返回值,括号里面的则是参数。
在上面的程序中,用5个包装器实例化了5个不同的函数。在用包装器时要注意,如果传类的成员函数给包装器,要给它们加上类域,让编译器能够找到成员函数的位置。同时,对于静态成员函数,在类域前可以不加&;但是普通成员函数必须要加&,建议在使用时最好静态和普通成员函数都加上&。并且普通成员函数因为还有一个隐含的this指针,在包装器中要多加一个类名,不传指针是因为this指针只能隐式传递。然后在使用时要传一个匿名对象进去。
包装器一般用于各种可调用对象的类型统一。包装器是一个很重要的东西,在未来大家学习网络编程时,就可能会经常使用到包装器以使网络的不同命令使用同一个包装器执行不同的动作。
七、 bind适配器
1. bind适配器的含义
function可以理解为一个类模板,bind就可以理解为一个函数模板。它可以接收一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。简单来讲,就是bind可以接收一个可调用对象,然后修改这个可调用对象参数顺序,再将这个新的对象返回。
在bind里面,fn是可调用对象, 后面的就是一个参数包,表示这个可调用对象的参数。
2. bind的使用
在使用bind时,要配合命名空间placeholders一起使用。因为bind中的参数部分并不是传类型,而是传placeholders里面的内容:
在这个命名空间中,_1就表示第一个参数,_2就表示第二个参数,以此类推。
bind适配器可以和function包装器配合使用。先写出如下代码:
对于以上程序,运行起来的结果肯定是1,,这没什么好说的:
但是,如果将_1和_2的位置交换后再运行程序:
可以看到,结果为-1。这就说明bind将传入的参数位置修改了。
当然,bind也可以用于绑定固定参数。例如我们用包装器包装了一个类成员函数,要使用这个包装器,就需要向里面传类名和类的匿名对象:
但有了bind,我们就可以将这个成员函数绑定在这个包装器中:
bind适配器其实并没什么太大作用,大家y有所了解即可。