目录
C++11是C++编程语言继98年之后最重要的一次版本更新,那么这篇博客就来具体谈一谈都更新了什么
首先就是列表初始化,就是用花括号去初始化任何数据,比如整形,数组,结构体可以这样初始化
下面的日期类也是只调用构造
我们在new一个日期类对象的数组的时候就可以这么写
initializer_list
对于一些容器我们可以这样初始化
它其实是去调用构造的,在C++11后增加了一些构造函数
所以initializer_list其实是一个类型
如何让我们之前自己实现的vector支持这个构造呢,我们再写一个构造函数就可以
decltype
这个关键字可以用来推导类型并作为类型
我们之前也说过typeid,它可以告诉变量的类型,我们来举例用一下
我们可以看到x的类型中并没有const,但是p1的类型中有const,因为前者是修饰x的内容不改变,也叫顶层const;而后者是修饰p1指向的内容不被修改,也叫底层const。我们可以认为顶层const就是一种修饰,修饰变量的权限,而底层const是具体的变量类型。
nullptr
我们C语言有NULL,那为什么后来弄了一个nullptr呢?肯定是NULL会有一些问题,NULL其实在C语言中就是0去强转成void*的
在这种情况下,在C++中有时会有隐式类型转换,这样就会在指针和整数之间相互转换,而nullptr是一个特殊的类型,只能转化成指针类型,所以NULL就会出现一些问题,比如:
这样是可以的,但是NULL会进行隐式类型转换,nullptr不会
所以使用nullptr就会避免这种潜在的混淆和错误
右值引用
什么是右值
下面就到了右值引用的部分,之所以发明右值引用,肯定是因为之前的左值引用有一些问题不能解决,那下面我们就来具体的看一下
首先我们要明白什么是左值,什么是右值,其实左值和右值之间最根本的区别是能否取地址。有人可能说左值可以改变,右值无法改变,其实这是不对的,const修饰的左值也无法改变,而右值确实都无法改变。那我们下面先来举几个例子
之前我们说的都是左值引用,我们已经会写了,右值引用的话就是两个&符号
在这里我们需要记住右值被右值引用后,右值引用的属性是左值,可能很绕嘴和抽象,举上面的例子,10是右值,它被右值引用后rr1是左值,可以取地址,所以rr2和rr3都是左值。那么为什么会是这样呢?因为我们后面有应用场景就是换取右值的资源,所以它就得必须变成左值,因为右值是不可变的。
左值引用如何引用右值
左值引用不能引用右值,但是const左值引用可以,因为加const就证明不能修改了;同理,右值引用也不能引用左值,但是右值引用可以引用move之后的左值。我们来举个例子
左值引用解决了那些问题
说完了上面的一些基本语法,那接下来就来看看右值引用的应用,它究竟解决了那些左值引用无法解决的问题呢?
说之前,我们先来看看左值引用究竟解决了哪些问题,首先就是传参可以用左值引用传参,这样就解决了传参的拷贝问题;其次就是函数传返回值的话,如果出了函数这个返回值还在,那么就可以用左值引用返回。那我们就看一下用左值引用和不用左值引用的区别,利用下面这个代码
jxh::mystring& stradd(jxh::mystring& s) { static::jxh::mystring s2; for (int i = 0; i < s.size(); i++) { s2 += s.c_str()[i]+1; } return s2; } int main() { jxh::mystring s1("abcdefgh"); jxh::mystring s3; s3=stradd(s1); return 0; }
首先我在函数中不管是传参还是传返回值都用上左值引用,可以发现结果是
然后把引用去掉,再来看结果
我们可以发现多了两次拷贝构造,可以发现左值引用确实可以减少深拷贝,左值引用解决了上述问题
右值引用要解决问题
但是左值引用仍然有未解决的问题,那就是如果返回的是一个局部对象,那不能用左值引用了,这里仍然会有拷贝问题,就是局部对象要拷贝给一个临时对象,而这个临时对象很快就会被销毁,那么我们能不能利用这个临时对象中的资源呢?这是右值引用要解决的问题。
那么它具体是如何实现的呢?我们用这个代码举例
首先肯定不能右值引用返回,因为s2是左值,就算我们move一下也不行,因为s2毕竟要销毁;其次我们知道,s2会拷贝构造一个临时对象,然后临时对象赋值给s3,这里的拷贝构造和赋值全是深拷贝,我们可以从这里下手,我们让s2变成右值(为了让以前的代码也享受这样的资源节省,所以编译器会隐式将s2move),然后重载拷贝构造叫做移动构造,参数是右值引用,意思是移动走s2中的资源给临时对象,并且因为移动构造比拷贝构造的参数更匹配,所以就回去调用移动构造,我们可以这么写移动构造
mystring(mystring&& s) { swap(_start, s._start); swap(_size, s._size); swap(_capacity, s._capacity); cout << "移动构造——浅拷贝" << endl; }
并且临时对象也是右值,我们也可以利用上面的原理把它里边的内容转移给s3,这就叫做移动赋值
mystring& operator=(mystring&& s) { swap(_start, s._start); swap(_size, s._size); swap(_capacity, s._capacity); cout << "移动赋值——浅拷贝" << endl; return *this; }
上面的移动构造和移动赋值都是浅拷贝,基于上面的原理,我们就叫右值引用的移动语义,于是我们可以看看有移动语义和没有之间的差别有多大
我们可以看到移动语义确实减少了深拷贝,把没有必要的深拷贝都变成了浅拷贝,更充分的利用了申请的资源。
我们如果不在主函数先构造在赋值,而是直接用函数的返回值进行拷贝构造的话是这样的:
如果没有移动语义,那么s2会拷贝构造出一个临时对象,临时对象在拷贝构造出s3,但是之前编译器就已经会给我们优化了(直接在函数退出之前由s2构造出s3),就是优化成一次拷贝构造
之后有了移动语义,那么就是两次移动构造,同样优化成一次移动构造
C++11对右值概念进行了细分,为了方便理解,分为:
1.纯右值(内置类型的右值)如:10 x+y
2.将亡值(自定义类型的右值,其实就是生命周期非常短暂)如:匿名对象、传值返回函数
右值还有一个应用场景就是用于容器插入,对于有名对象插入的时候肯定要深拷贝一份,但是对于匿名对象和进行隐式类型转换的数据它可以直接移动拷贝
我们可以看到,确实是这么回事
我们上面说,右值被右值引用后,右值引用的属性是左值,因为左值才可以被改变,这样资源才能被转移,我们来证明一下
所以我们在调用移动构造和移动赋值时,那里的参数是左值,因为只有这样才能转移资源
mystring(mystring&& s) { swap(_start, s._start); swap(_size, s._size); swap(_capacity, s._capacity); cout << "移动构造——浅拷贝" << endl; } mystring& operator=(mystring&& s) { swap(_start, s._start); swap(_size, s._size); swap(_capacity, s._capacity); cout << "移动赋值——浅拷贝" << endl; return *this; }
引用折叠和完美转发
接下来是模板和引用的结合,我们知道模板可以进行推演,把活都交给编译器去干,所以就有了这么一个叫做引用折叠(也叫万能引用)的东西,就是不管是左值还是右值都可以传给这个模板,它的基本形式是这样的
别看这里有两个&,其实左值和右值都可以传进来这个函数,传左值两个&符号就变成了一个,所以叫折叠
这些都是可以传的,其实编译器在实例化这个模板函数的时候,将传左值,传右值都实例化了一份
但是还有一个问题,就是右值传过去t就变成了左值,但是我还想用它右值的属性在调一个函数怎么办呢?这时就用到了完美转发,就是把t放到里边,它原来是左值还是右值都会恢复回去,基本使用是这样的