1.C++11的发展历史
C++11 是 C++ 的第⼆个主要版本,并且是从 C++98 起的最重要更新。它引⼊了⼤量更改,标准化了既有实践,并改进了对 C++ 程序员可⽤的抽象。在它最终由 ISO 在 2011 年 8 ⽉ 12 ⽇采纳前,⼈们曾使⽤名称“C++0x”,因为它曾被期待在 2010 年之前发布。C++03 与 C++11 期间花了 8 年时间,故⽽这是迄今为⽌最⻓的版本间隔。从那时起,C++ 有规律地每 3 年更新⼀次。

2.列表初始化
2.1C++98传统的{}

C++98中的一般数组和结构体可以用{}进行初始化,而在C++11中对{}进行了延伸。
2.2C++11中的{}
1.C++11以后想统⼀初始化⽅式,试图实现⼀切对象皆可⽤{}初始化,{}初始化也叫做列表初始化。
2.内置类型⽀持,⾃定义类型也⽀持,⾃定义类型本质是类型转换,中间会产⽣临时对象,最后优化了以后变成直接构造。
3.{}初始化的过程中,可以省略掉=
4.C++11列表初始化的本意是想实现⼀个⼤统⼀的初始化⽅式,其次他在有些场景下带来的不少便
利,如容器push/inset多参数构造的对象时,{}初始化会很⽅便。

这就是C++11中对内置类型的支持,不过我们在日常使用中也不怎么这样用,这种了解一下即可。
主要是对自定义类型的支持有很大的改变:


这里我们创建一个Date类,此时我们就可以用第二张图的方式进行初始化。
这里本质是{2025,1,1}构造了一个Date的临时对象,临时对象又通过拷贝构造赋值给d1,但是我们之前也讲过,这里编译器会优化为直接构造:

可以看出并没有调用拷贝构造函数,只调用了构造函数。

像这种d2引用的就是{2024,7,25}产生的临时对象,这里加上const也是因为引用本身可以被修改,而临时对象具有常性,不能被修改,所以加上const就可以使引用的权限被缩小,变得不能被修改。

这里还需要注意的是在C++98中支持单参数的隐式类型转换,并且可以不加{}。

=符号在C++11中也是可以省略的,这种就看个人习惯了,但是必须要有{}的情况下才可以省略=符号。

像这种没有{},程序就会报错。


相较于有名对象和匿名对象传参,{}传参更有性价比。
2.3C++11中的initializer_list

上面的初始化已经很方便,但是初始化参数的数量取决于你定义的成员变量的个数,简单来说有数量限制,所以对于容器初始化还是不太方便,
⽐如⼀个vector对象,我想⽤N个
值去构造初始化,那么我们得实现很多个构造函数才能⽀持,
vector<int> v1 =
{1,2,3};vector<int> v2 = {1,2,3,4,5};
这样写起来就太麻烦了,故
C++11库中提出了⼀个std::initializer_list的类,
auto il = { 10, 20, 30 }; // the
type of il is an initializer_list
,这个类的本质是底层开⼀个数组,将数据拷⻉
过来,std::initializer_list内部有两个指针分别指向数组的开始和结束。


通过调试我们可以看出
initializer_list中有两个指针_First和_Last来指向数组的开始和结束。
容器⽀持⼀个std::initializer_list的构造函数,也就⽀持任意多个值构成的
{x1,x2,x3...}
进⾏
初始化。STL中的容器⽀持任意多个值构成的
{x1,x2,x3...}
进⾏初始化,就是通过std::initializer_list的构造函数⽀持的。


并且
initializer_list是支持迭代器访问的,我们通过打印出迭代器的地址和栈上数据地址的比较可以看出,initializer_list所开辟的数组是在栈上的。

这里拿vector容器来演示,就可以通过
initializer_list来进行初始化,不过v1和v2在写法语义是不太一样的,v1是直接构造,v2是构造临时对象+临时对象拷贝复制v2,但是这步操作会被编译器优化为直接构造,这点大家要弄清楚。
而v3引用加const依旧是权限的问题,这里就不过多赘述了。
3.右值引用和移动语义
C++98的C++语法中就有引⽤的语法,⽽C++11中新增了右值引⽤语法特性,C++11之后我们之前学习的引⽤就叫做左值引⽤。⽆论左值引⽤还是右值引⽤,都是给对象取别名。
而在学习右值引用之前我们先认识一下左值和右值。
3.1左值和右值
左值
是⼀个表⽰数据的表达式(如变量名或解引⽤的指针),⼀般是有持久状态,存储在内存中,我
们可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时const
修饰符后的左值,不能给他赋值,但是可以取它的地址。


上面就是我们常见的左值,是可以取地址的。
右值
也是⼀个表⽰数据的表达式,要么是字⾯值常量、要么是表达式求值过程中创建的临时对象
等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。



而这些就是右值的一些例子,并且右值是不可以取地址的,如果取地址就会报错。
说了这么多,那么右值该如何人判断呢?
一般有三种情况属于右值:
1.字面值常量:也就是内置类型,如:int,double,char等
2.临时对象:如上面的fmin函数调用,就会产生临时对象,而产生临时对象一般有类型转换,函数返回值/调用,表达式三种情况
3.匿名对象:最后的string就是匿名对象
值得⼀提的是,左值的英⽂简写为lvalue,右值的英⽂简写为rvalue。传统认为它们分别是left
value、right value 的缩写。现代C++中,lvalue 被解释为loactor value的缩写,可意为存储在内
存中、有明确存储地址可以取地址的对象,⽽ rvalue 被解释为 read value,指的是那些可以提供
数据值,但是不可以寻址,也就是说左值和右值的核⼼区别就是能否取地址。
3.2左值引用和右值引用
Type& r1 = x; Type&& rr1 = y;
第⼀个语句就是左值引⽤,左值引⽤就是给左值取别
名,第⼆个就是右值引⽤,同样的道理,右值引⽤就是给右值取别名。

上图就是左值引用和右值引用的基本使用。
那么左值引用能否引用右值或者右值引用能否引用左值呢?
答案是可以的,
左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值;右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤move(左值)。


左值引用加const可以引用右值,这就解释了为什么前面文章中的内容我在实现时参数都为加const的左值引用,就是可以让其即可以传左值,也可以传右值。
右值引用引用左值需要用到的move函数:

move是库⾥⾯的⼀个函数模板,move的作用简单来说就是强制类型转换:

其作用就和这样写一样。
注意:
变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,右值引⽤变量
表达式的属性是左值,也就是说一个右值引用引用一个右值后,这个右值引用变量就变成左值了。
语法层⾯看,左值引⽤和右值引⽤都是取别名,不开空间。从汇编底层的⻆度看上面代码中r1和rr1
汇编层实现,底层都是⽤指针实现的,没什么区别。底层汇编等实现和上层语法表达的意义有时是
背离的,所以不要然到⼀起去理解,互相佐证,这样反⽽是陷⼊迷途。
3.3引用延长生命周期
我们知道临时对象和匿名对象生命周期都只有当前那一行,到了下一行就会被销毁。

如果不能延长其生命周期的话,上图中的r2和r3不就变成了野引用,所以引用可以延长生命周期。
右值引⽤可⽤于为临时对象延⻓⽣命周期,const 的左值引⽤也能延⻓临时对象⽣存期,但这些对象⽆法被修改。
而被延长的临时对象生命周期就会和表达式变量一样,也就是和r2,r3一样,在main函数结束时才会被销毁。
3.4左值和右值的参数匹配


大家可以想一下这里调用的都是什么函数。

答案就如上图所示,和函数模板那里很相似,没有最适合的时,就用能用的,一旦有最合适的,就用最合适的,所以这里传过去的右值都会调用右值引用的函数。

再看这两个函数应该调用那些函数?

可以看出结果一个调用了左值引用的函数,一个调用了右值引用的函数。
原因上面已经说过,x在引用右值后就变为左值了,再使用move函数就将其强制类型转化了。
右值引⽤变量在⽤于表达式时属性是左值,这个设计这⾥会感觉跟怪,下⼀⼩节我们讲右值引⽤的
使⽤场景时,就能体会这样设计的价值了。
3.5右值引用和移动语义的使用场景
3.5.1左值引用的主要使用场景


左值引⽤主要使⽤场景是在函数中左值引⽤传参和左值引⽤传返回值时减少拷⻉,同时还可以修改实参和修改返回对象的价值。左值引⽤已经解决⼤多数场景的拷⻉效率问题,但是有些场景不能使⽤传左值引⽤返回,如上面的addStrings和generate函数,C++98中的解决⽅案只能是被迫使⽤输出型参数解决。那么C++11以后这⾥可以使⽤右值引⽤做返回值解决吗?显然是不可能的,因为这⾥的本质是返回对象是⼀个局部对象,函数结束这个对象就析构销毁了,右值引⽤返回也⽆法概念对象已经析构销毁的事实。
上面两个函数在c++98时期这样写代价是非常大的,我们上面说过函数返回值会产生临时对象,这时就会调用拷贝构造函数来开辟新的空间在把数据一个个复制过去,而如果要将其返回值赋值给一个变量,那么又要重复上面的操作来进行,所以效率是很低的,而下面右值引用的使用场景就解决了这个问题。
3.5.2移动构造和移动赋值
移动构造函数是⼀种构造函数,类似拷⻉构造函数,移动构造函数要求第⼀个参数是该类类型的引
⽤,但是不同的是要求这个参数是右值引⽤,如果还有其他参数,额外的参数必须有缺省值,一般我们也不会传其它的参数。
移动赋值是⼀个赋值运算符的重载,他跟拷⻉赋值构成函数重载,类似拷⻉赋值函数,移动赋值函
数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤。
在讲之前我们先看没有移动构造的情况:
这里我们自己实现了一个string类,就和前面string章节写的一样,所以这里就不再演示。



这是没有移动构造之前的方式,两次深拷贝+两次析构释放。

因为编译器会进行优化,所以在vs编译器下我们看不到拷贝构造函数的调用,下面我们通过linux来观察:

这里就是在linux下关闭编译器的优化就会显示拷贝构造的调用了,通过上面的图我们也可以看出没有移动构造的情况下和我们上面说的一样,两次拷贝构造+两次析构。
说了这么多,移动构造该如何写?又是怎么发挥作用的呢?

这就是移动构造的写法,和我们的现代写法一样,直接交换两个string的指针即可,不需要再额外开辟空间,一个个再将数据复制过去。
我们再来看有了移动构造之后的过程:

此时的拷贝构造就都变为了移动构造,可能有人会疑惑:传参的移动构造我能理解,毕竟是右值,但是为什么返回值str也会去调用移动构造呢?
这里就要解释一下:编译器会自动将str识别为右值,这点大家记着即可,也没有办法解释,编译器就是这样用的。
到这里大家就可以设想一下有了移动构造之后效率会比没有高很多,所以c++11之后就完美解决了c++98的问题,既不用开辟新的空间,产生新的资源,效率也高很多。
我们再来看移动赋值:


移动赋值和移动构造的思路是一样的,也是利用swap函数交换即可。
这种情况下就要调用=符号重载来实现赋值,vs编译器下的就不展示了,被编译器优化后也看不出来什么,这里就直接展示linux环境:

这是没有使用移动构造和移动复制的情况下,可以看出调用了拷贝构造和=符号的拷贝赋值。

这是使用移动构造和移动复制的情况下,此时就不再调用拷贝构造和拷贝赋值,和移动构造一样,移动赋值同样大大提高了效率。
讲了移动构造和移动赋值,我们回到上面我们感觉很奇怪的点,为什么右值引用变量会变为左值?
我们思考一下,右值一般来说是不能被改变的,不管是临时对象还是匿名对象,都具有常性,那么怎么能交换它们的资源呢?
当时的语法也是遇到这样的问题,这和右值的概念相悖,所以才有了变量表达式都为左值的规定,这样才能交换它们的的资源。
以上就是C++11(一)的内容。