C++11:从版本发展史、列表il初始化、移动语义和右值引用、引用折叠到完美转法的语法、使用、底层原理的全方面解析)
目录1.C的发展历史2.列表初始化3. std::initializer_list4.移动语义和右值引用4.1左值4.2右值4.3左值引用与右值引用4.4引用可以延长生命周期const 4.6左值和右值的参数匹配4.7移动构造和移动赋值4.7.1RVO、左值引用的优点与局限性4.7.2针对左值引用以及RVO移动构造、移动赋值的用处4.7.3拷贝赋值 与 移动赋值4.8右值引用和移动语义在传参的提效编辑4.9简单设计list的右值引用版本5.引用折叠 - 万能引用5.1引用折叠的语法规则与例子5.2引用折叠-扩大了模板泛用性 万能引用6.完美转发这篇讲解一下C11的关键语法。1.C的发展历史下面这张图展示了C11版本的发展历史。字体越大的版本就是越重要的版本。可以看到C98十分重要事实也是如此它是C发展的基础。而今天准备讲解的C11版本则是自C98以来最重要的更新。它引入了大量更改标准化很多语法在实践上也优化了C程序员体验语法不再那么抽象。每个版本是有规律地 三年一更新但是唯独C11等了很久因为早期C98也存在诸多不足需要大量优化以及添加加上早期也没有规律更新所以C11版本间隔了13年。可谓是“梅花香自苦寒来”啊此版本也是目前各大企业都广泛使用的版本。C发展历史就讲到这里了具体可以百度了解~ 下面正式讲解它的核心语法。2.列表初始化C98中一般数组和结构体可以用 { } 进行初始化。C11以后C委员会想统一初始化方式试图一切对象皆可用 { } 初始化。{ } 也叫列表初始化内置类型支持自定义类型也支持隐式类型转换多参数构造函数的隐式类型转换中间产生临时对象涉及两次深拷贝最后优化成了直接构造重点{ }列表初始化过程中可以省略 { }列表初始化是initializer_list 和 隐式类型转换 互补结合形成的他们长得像但不是一个东西看下面的超长图带你完全拿捏列表初始化3.std::initializer_list上面说过initializer_list也是列表初始化的一大组成部分并且前面我们也稍微了解过来看看对于vector如果想用N个参数去构造初始化那很折磨了它是封装的数组不会使用C98的原生数组的方式去初始化。对于此C11库中提出了一个std::initializer_list的类这个类的本质是底层开一个数组有头长度等价于头尾指针end封装了头指针长度将数据拷贝过来。有头尾指针所以它也支持迭代器遍历。STL容器中支持任意多个值构成的{1,2,3.....n}进行初始化得益于基于std::initializer_list的构造函数也就是il构造函数下面是il的基本组成64位下。为什么3个int算出来是16字节难道是类本身占了4字节是因为std::initializer_list 只有头指针数据的长度。那为什么是16字节呢指针头8长度12才对吧不对。因为不是直接累加长度而是看类型size_t64是8,32是4所以是88164.移动语义和右值引用之前的学习中我们学习的都是左值引用。现在准备学习右值引用这将是C11的最重要的新语法。4.1左值左值是一个表示数据的表达式如变量名解引用的指针一般是持久状态。他们存储在内存中我们可以获取他们的地址。左值可以取引用也可以取地址。受const修饰的左值不能给他赋值但可以取地址。下面是常见左值说明最后一个是char取地址char*不会正常显示地址强转为void*。第一个指针p解读p指向堆区new的1个int大小空间首地址,(0)初始化为04.2右值右值也是一个表示数据的表达式字面量常量表达式求值过程中创建的临时对象等。右值可以出现在赋值符号右边但不能出现在左边如10就是右值。右值不能取地址。左值英文简写lvalue右值英文简写rvalue 。传统认为分别是left valueright value。现代C中lvalue locator value 意思存储在内存中的有明确存储地址、可取地址的。rvalue read value 意思可以提供数据不可寻址的只读数据。C11又提出新的名称我们常见的右值都是纯右值move的左值是将亡值。下面是常见右值4.3左值引用与右值引用Type r1 x; //左值引用就是给左值取别名 Type rr1 y; //右值引用就是给右值取别名左值引用不能直接引用右值但是const左值引用可以引用右值因为右值本质是临时变量具有常性。右值引用不能直接引用左值但是右值引用可以引用move(左值)因为move移动语义的作用是把左值变成右值。move底层本质是强制类型转换把左值强转为右值。4.4引用可以延长生命周期const 右值引用可以延迟生命周期延长至于rr1一样也就是到局部域生命周期结束非常直观2,3行是匿名对象的执行当前行后就销毁了。const 左值引用和右值引用都可以延长生命周期。4.6左值和右值的参数匹配经过上面分析发现const 左值引用 和 右值引用 可以延长生命周期 和 引用右值。这也是函数参数尽量写 const 左值引用的原因目的是让右值也可以传参并且减少传值拷贝。有const 左值引用版本的函数右值可以勉强传参。如果有右值引用的重载函数右值就会优先选择右值版本的函数。下面举例子看看只有左值引用的函数右值不可以传参。有const左值引用的重载函数右值可以传参。有右值引用的重载函数右值可以传参并且优先调用右值版本4.7移动构造和移动赋值4.7.1RVO、左值引用的优点与局限性左值引用解决了很多效率问题1.传值传参左值引用传参 解决了拷贝问题2.传返回值传参左值引用返回 同样解决了拷贝问题部分场景不行在哪些场景下不可以使用引用呢比如说函数返回string但是返回值是string 会构成野引用出函数作用域string销毁。但是这点还好。即便不引用传参现代编译器几乎都进行了返回值优化在这场景下传值传参优化为直接构造极大改善了效率。但是像杨辉三角返回值vv动态二维数组就不一样了不仅不能传引用返回而且传值返回也无法优化类型复杂多重空间嵌套编译器会评估如果太麻烦的类型就不优化4.7.2针对左值引用以及RVO移动构造、移动赋值的用处那怎么办所以C也想到了这个问题于是设计了移动构造 和 移动赋值移动构造函数是一种构造函数类似于拷贝构造函数移动构造也要求第一个参数是自身引用不过是右值引用。如果有其他参数必须加缺省值移动赋值是一个赋值运算符重载类似于拷贝赋值函数移动赋值要求第一个参数为自身类类型的引用右值引用下面看看移动构造、移动赋值对传值传参返回的区别左边没有移动赋值移动构造就会两次拷贝构造两次都是深拷贝消耗巨大右边在移动构造移动赋值下右边是两次移动构造前后接管一片即将销毁的空间类似于临时对象匿名对象的空间不涉及深拷贝只是指针的交换效率巨高。RVO优化后两者效率一样。4.7.3拷贝赋值 与 移动赋值上面的图和下面的图很像区别只有一个上图ret是初始化下图ret是赋值。前者创建时初始化后者先存在再赋值。这就是为啥下面是拷贝赋值/移动赋值。4.8右值引用和移动语义在传参的提效这是它的提效。仔细分析看看。注意第四个左值move变成右值会给别人转移你资源的权限要谨慎使用资源被转走后可能会造成野引用野指针另外右值引用的属性是左值目的是让资源能够转移。看这里的swap(s) 需要加move吗 其实不用因为右值无法交换资源他们具有常性为了让右值可以交换资源语法层面上设计规定了右值引用变量的属性是左值左值就可以交换资源。所以右值引用的目的就是识别并安全地窃取临时对象的资源。4.9简单设计list的右值引用版本右值引用不能绑定临时右值作为缺省值深拷贝才需要右值引用存在缺陷代码复用率低。这样会不会反向优化呢总是要作出取舍的。移动构造移动赋值可以优化内存存储效率即便代码复用率低但还是功远大于过。并且下面的引用折叠后面的完美转发等都能优化掉这个小问题。5.引用折叠 - 万能引用5.1引用折叠的语法规则与例子C中不能直接定义引用的引用不能直接定义可以间接通过typedef、using、模板就会出现引用折叠多个引用嵌套一起怎么知道他们是什么引用先看结论右值引用的右值引用折叠成右值引用所有其他只要包括左值引用组合均折叠成左值引用由于规则的限定但凡交换一下都会报错。左值引用不能引用右值右值引用不能引用左值下面配合模板看看引用折叠的规则模板内是 T T自己传为了明显我们直接显式实例化模板看左边因为模板限定了是左值引用所以无论显式是左值右值引用折叠后都是左值引用这个是右值引用模板5.2引用折叠-扩大了模板泛用性 万能引用那这个有什么用看下面的例子下面是对T推导的解释结合下图模板函数内部T x a;传10地址不同说明没推导出int 也不是 int本质也是引用也共享地址 T是int传a地址相同说明T推导出了int a是左值不是int。T是int传move(a)地址不同说明没推导引用T是int传b地址相同并且x不能未演示结论如此说明T是const int传move(b)地址不同并且x不能说明T是 const int这就是引用折叠的作用扩大模板的泛用性实现了万能引用有了万能引用就能解决上面提到的移动构造移动赋代码复用率低。那看下面这个是万能引用吗 一样是T一样是模板那只留这个就行了吗这个不是万能引用。万能引用是传参时推导T的类型而这里的T已经固定了创建list的一瞬间类模板实例化的时候比如listintT的类型已经确定了无法推导T类型。区别万能引用就看T是否是调用此函数时才推导的如果已经决定那就不是。这样改完又出问题x要不要move左值引用不能move右值引用又需要move。怎么解决参考的完美转发。6.完美转发解决x是否move的问题需要完美转发。下面是它的声明完美转发本质是一个函数模板它也是通过引用折叠方式实现它也根据T类型如果T是左值那T就被推导T引用折叠为左值引用使函数内部 t 被强转为左值引用返回。如果T是右值T推导为T引用折叠为右值引用使函数内部t被强转为右值引用返回还是存在缺陷后面讲了emplace接口后它是上替就不需要这么写了。