1.decltype
“decltype(表达式) 变量名”可以定义变量,这个变量的类型是()括号内表达式的类型,注意这个表达式不会执行,只会推导这个表达式的类型,这点和sizeof一样
2.nullptr
根据#define NULL 0,可知NULL会被预处理为0,在C++的重载函数中,NULL会导致歧义
nullptr的出现就是为了解决这个问题,在以后C++中最好别用NULL,防止不必要的错误
此外C++的指针不能像C那样隐式类型转换,检查很严格,需要手动强转,使用时需要额外注意
3.左值与右值
左值和右值是C++11以后引入的新的概念,区分两者的根本是左值可取地址,右值不可取地址
0,nullptr这种常量,A()这种匿名对象都不能取地址,它们叫做右值
引入了左值右值的概念后,我们对函数的传参传返回值就能更深入地分析了
传参时右值可以给左值赋值,但是引用的话必须要使用const修饰的左值引用才可以。
传参的过程可以理解为创建变量赋值的过程,即int& a = num和int& a = 1。
当“=”右边是左值时,“=”左边的左值变量会被直接赋值;当“=”右边是右值时,这个右值会先存到另一个临时对象中(右值),之后将这个右值赋值回“=”左边的变量
函数如果返回的是值,会先将这个值存到右值(临时变量)里,再将右值赋值给接收它的左值,因此引用的时候必须要使用const修饰的左值引用
对于所有的表达式,它们都会返回一个值,这个表达式的值都会先存到右值(临时变量)里,再将这个右值赋给左值变量。
上面展示的ret1和ret2在原理上一模一样,都是返回值先存在右值,再返回结果。
总结:(1)对于所有传参过程都需要理解为创建变量的过程,类似int a = num
当创建的变量a是非左值引用时,“=”右边如果是左值的话就会被直接赋值给这个创建的变量,如果是右值的话,会先这个值存到临时变量(右值)里,再赋值回去
当创建的变量a是左值引用时,“=”右边如果是左值的话会直接取别名,而如果是右值的话就需要在左值引用前用const修饰。
(2)对于所有的表达式返回值创建变量的过程,如int a = num
当创建的变量a是非左值引用时,“=”右边如果是左值的话就会被直接赋值给这个创建的变量,如果是右值的话,会先这个值存到临时变量(右值)里,再赋值回去,所有函数的非引用返回值都是右值,就算返回值是一个变量的形式
当创建的变量a是左值引用时,“=”右边如果是左值的话会直接取别名,而如果是右值的话就需要在左值引用前用const修饰。
其实传参过程的int a = num也是表达式返回值创建变量的过程,需要仔细体会
4.右值引用
(1)右值的基本用法
右值也有分类:纯右值,内置类型右值,包括常量值1、2、3;将亡值,类类型的右值,比如匿名对象A()
右值引用是给右值取别名,int&& r = x + y,跟左值引用对立。
const int& a = 0;虽然也能取别名,但这只是兼容写法,保证不会发生权限越界,并且在功能上非常有限
这其实很好理解,用const修饰的左值引用去引用右值,虽然通过这种方式我们依然可以给左值取别名,且这个变量r本身也是左值,但它的性质其实已经和右值没有区别了,如const int&可以说和右值在性质上没区别了,都是只能只读。
但是右值引用就不是了,简单来说它更像是将一个右值强转为左值,当我们修改强转后的左值变量,就可以去修改原来的这个右值。从另一个角度上来说,右值引用可以简单理解为是一种合法化的权限扩大,当我们使用const int&来适应右值时就是为了避免修改右值,而右值引用的独特之处又恰好在于可以扩增右值的修改权限,这也是右值引用存在的最大原因。我们也会发现,const int&&几乎没有任何意义,因为右值引用的存在就是为了修改右值的权限,加了const就相当于丢失了核心作用。
(2)左值引用引用右值
前面已经强调了,当左值引用引用左值时,就是直接取别名,保证类型一致,注意const int也叫左值,因为可以取地址,只是在取别名时保证类型相同即可。左值引用引用右值时,要限制访问权限,右值不可修改,所以在前面加上const,也能给右值取别名。
(3)右值引用引用左值
右值引用引用左值同样需要特殊处理,即move(左值),它本质上就是一个强转,将左值强转为右值,即move(左值)这个表达式的返回值和1,2,3一样都是右值,右值引用本身又相当于一次强转,再将这个右值转为左值。如int&& r = move(a)就是给a取了一个别名,a是左值,move(a)是右值,而r又是左值且为a的别名
(4)底层实现
左值引用、右值引用的底层都要开空间,实质上是传递指针,通过指针来修改值,而不是真的取别名,在汇编层面没有名字的概念,也更不存在取别名的操作,我们也可以理解为什么很多情况都推荐使用引用,因为指针的大小是固定的,都只有4/8个字节,在操作一些占用大的对象时开销更小。我们也能从中知道,所谓的move(),本质上也仅仅是为了通过语法编译。因此直接使用强转也是可以通过编译的。
(5)右值引用的意义
右值引用和左值引用最大的区别在于右值引用相当于把右值变成一个左值,是一个合法化的提权,当我们通过int&& rr定义时,就相当于将这个右值取了个左值别名,当修改rr时,我们就可以修改这个右值。而const int&只能兼容右值取别名,虽然是左值引用,但它的性质和操作右值没有区别。
其次右值引用相当于一个标志,引用的对象(右值)的作用域极小和生命周期极短,如A(),Fun(),1,2,3的作用域都只有那一行,出了那一行就会销毁。那么我们是否可以借助右值的特性,先使用右值引用给这个右值取一个别名(左值),通过这个别名,再使用swap,将我们需要的右值存储的数据和不需要的数据交换,这样右值里存的就是我们不需要的数据,当出生命周期后会自动销毁,而被交换的有用的数据就被我们保存了下来。这就是移动构造和移动赋值的根本原理,同时利用了右值短生命周期的特性以及右值引用强制提权的特性。
5.移动构造和移动赋值
(1)基本使用
下面是移动构造的一个模拟的场景
当拷贝时,如果拷贝的对象是一个临时对象时就会直接调用移动构造,这样会减少开销。当传值返回时,就会直接调用移动构造,这使得函数栈帧里面创建的值返回时能够实现数据交换,将有用的数据交换到外部变量,销毁时带走的是无用数据。
注意所有非引用的函数返回值都是右值,会隐式move,也可以手动move,不过没必要
最激进的优化是函数里的变量为main函数里返回值的别名,连移动都用不上,但不是所有编译器都会这么优化
移动赋值也是如此,在使用临时变量来赋值时,就会直接调用移动赋值,将有用的数据通过更快捷的方式交换回来,同时将无用数据交给这个临时变量,当执行下一行代码前这个临时对象会去调用析构函数,也不会造成内存泄漏
这几乎是完美的处理方式,可以极大降低程序的赋值、拷贝开销,特别是对于一些数据量大的对象,只需要交换几个指针就能完成整个对象数据的交换,在大型项目中更好用。
所有STL的容器都提供了移动构造和移动赋值,我们可以直接使用且没有任何学习成本,编译器会自动匹配合适的函数,只要是将亡值就会自动匹配到更合适的移动构造和移动赋值。
(2)默认移动构造和移动赋值
深拷贝的类,移动构造才有意义,因为移动构造移动赋值本质上是针对拥有大量数据的对象拷贝赋值进行简化操作,这往往意味着移动构造、移动赋值和深拷贝绑定,只有当容器里有使用指针管理大量数据时才有实现移动构造的必要,这也意味着构造、赋值、析构函数都要实现。
具体规则:在都不写移动构造、析构、拷贝构造、赋值重载时生成默认移动构造和移动赋值,内置类型按字节拷贝,自定义类型调它的移动构造。这个很好理解,析构,拷贝、赋值共5个成员函数一体,要么都写,要么编译器生成,规则的制订很符合实际需要。在使用中我们也不需要特别注意这条规则,这5个函数的相互关联性,只需要注意深拷贝时写一个移动版本就可以了。