第七章 为改变思考方式而改变
7.1 指针空值--nullptr
7.1.1 指针空值:从0到NULL,再到nullptr
传统C头文件里NULL是一个宏定义:
在函数重载同时出现int和char *参数版本的函数时,使用NULL作为参数调用函数会调用int参数版本,而不是我们想要的char*参数版本。 引起该问的元凶是字面常量0的二义性,既可以是一个整型,也可以是一个无类型指针void*。除非对字面常量0进行强制类型转换b=并调用,否则编译器总是会优先把0看作是一个整型常量。
c++出于兼容性考虑,并没有消除字面常量0的二义性。但是引入了作为指针空值类型的常量nullptr,指针空值类型被命名为nullptr_t。nullptr是关键字,nullptr是有类型的,只能被隐式转化为指针类型,所以nullptr做参数可以成功调用char*版本的函数。
7.1.2 nullptr和nullptr_t
c++11标准不仅定义了指针空值常量nullptr,也定义了其指针空值类型nullptr_t。c++11标准严格规定了数据间的关系:
1.所有定义为nullptr_t类型的数据都是等价的,行为也是完全一致的。
2.nullptr_t类型数据可以隐式转换成任意一个指针类型。
3.nullptr_t类型数据不能转换为非指针类型。
4.nullptr_t类型数据不适用于算术运算表达式。
5.nullptr_t类型数据可以用于关系运算表达式,但仅能与nullptr_t类型数据或者指针类型数据进行比较,当且仅当关系运算符为==、<=、>=等时返回true。
7.1.3 一些关于nullptr规则的讨论
nullptr类型数据所占用的内存空间大小跟void*相同。
nullptr到任何指针的转换是隐式的,而(void*)0则必须经过类型转换后才能使用。
nullptr_t对象的地址可以被用户使用。虽然nullptr也是一个nullptr_t的对象,但它被定义为一个右值常量,因此用户不能获得nullptr的地址。
7.2 默认函数的控制
7.2.1 类与默认函数
在c++中声明自定义的类,编译器会默认生成一些被称为默认函数的成员函数,包括:
构造函数、拷贝构造函数、拷贝赋值函数(operator=)、移动构造函数、移动拷贝函数、析构函数。
c++编译器还会为以下这些自定义类型提供全局默认操作符函数:
operator ,,operator &,operator &&,operator *,operator ->,operator ->*,operator new,operator delete。
一旦实现了这些函数的自定义版本,则编译器不会再为该类自动生成默认版本。声明了带参的构造函数版本,必须声明不带参版本以完成无参的变量初始化。声明了自定义版本的构造函数,有可能导致自定义类型不再是POD的。
C++11中,可以在默认函数定义或者声明时加上"= default"来显式地指示编译器生成该函数的默认版本。另一方面,有时候又希望可以限制一些默认函数的生成,譬如有时候需要禁止使用拷贝构造函数,c++11中,可以在默认函数定义或者声明时加上“= delete”来显式地指示编译器不生成函数的缺省版本。
7.2.2 “= default”与“= deleted”
c++11中,也可以在类定义外显式指定缺省版本,这样可以对一个class定义提供多个实现版本,通过选择性编译,从而在提供缺省函数和自定义版本间切换。
c++11并不要求编译器为“operator ==”之类的函数提供缺省实现,但是将其声明为显式缺省的话,编译器会按照某些标准行为为其生成所需的版本。
显式删除可以避免用户使用一些不应该使用的类的成员函数,也可以避免编译器做一些不必要的隐式数据类型转换,例如:
隐式删除不应该和explicit同用。在下面的例子中:
显式删除导致显式构造的cc变量编译出错,但是Func调用中,编译器会尝试隐式地将char转为int,从而调用一次ConvType(int)构造函数,因而能通过编译。
显式删除并不局限于缺省版本的类成员函数或者全局函数上,对于普通函数,依然可以通过显式删除来禁止类型转换。
7.3 lambda函数
7.3.1 lambda的一些历史
7.3.2 c++11中的lambda函数
lambda函数的语法定义:
捕捉列表由多个捕捉项组成,以逗号分割,有如下形式:
[var]表示值传递方式捕捉变量var,[=]表示值传递方式捕捉所有父作用域的变量,[&var]表示引用传递捕捉变量var。[&]表示引用捕捉所有父作用域的变量。[this]表示值传递方式捕捉当前this指针。还可以进行组合,例如[=,&a,&b]表示引用传递方式捕捉a和b,其他变量以值传递方式捕捉。
默认情况下lambda函数总是一个const函数,mutable可以取消其常量性,使用时参数列表不可省略。
不需要返回值的时候也可以连同符号->一起省略。
c++11标准规定在块作用域以外的lambda函数捕捉列表必须为空,而在块作用域中的lambda函数仅能捕捉父作用域中的自动变量。
7.3.3 lambda与仿函数
在c++11之前,我们在STL中会用到一种特别的对象,称之为函数对象,或者仿函数functor,也就是重定义了成员函数operator()的一种自定义类型对象。在使用它的时候,在代码层面跟函数的使用一样,但本质却是一种对象。仿函数是编译器实现lambda的一种方式。现阶段,通常编译器会把lambda函数转化为一个仿函数对象。
7.3.4 lambda的基础使用
lambda可用于实现局部函数。
7.3.5 关于lambda的一些问题及有趣的实验
如果需要捕捉的值称为lambda函数的常量,通常需要使用按值传递的方式捕捉。反之,需要捕捉的值称为lambda函数运行时的变量,则应采用按引用方式进行捕捉。
lambda的类型并非简单的函数指针或者自定义类型,lambda的类型被定义为闭包的类,每个lambda表达式会产生一个闭包类型的临时对象(右值)。不过c++标准允许lambda表达式向函数指针的转换,前提是函数没有捕捉任何变量,且函数指针所示的函数原型,必须跟lambda函数有这相同的调用方式。
lambda函数的常量性及mutable关键字。现有c++11标准中lambda等价的是有常量成员函数operator()的仿函数,捕捉列表中的变量都会成为等价仿函数的成员变量,而常量成员函数中改变其值是不允许的,因而在按值捕捉的变量在没有声明为mutable的lambda函数中改变其会导致编译器报错。可以通过mutable修饰符消除其常量性,不过更推荐使用引用捕捉。
7.3.6 lambda与STL
当循环次数较多时,内联的lambda函数比函数指针性能好。函数指针应用范围相对狭小,特别是需要具备一些运行时才能决定的状态时,以前或许会使用仿函数,现在则可以选用lambda。
7.3.7 更多的一些关于lambda的讨论
在现有c++11中,lambda不是仿函数的完全替代者,这点很大程度上是由lambda的捕捉列表的限制造成的。仿函数可以被定义以后在不同的作用域范围内取得初始值,这使得仿函数天生具有跨作用域共享的特征。lambda函数被设计的目的,就是要就地书写,就地使用。