目录
[1 统一列表初始化](#1 统一列表初始化)
[2 声明](#2 声明)
[2.1 auto](#2.1 auto)
[2.2 decltype](#2.2 decltype)
[2.3 nullptr](#2.3 nullptr)
[2.4 stl的部分变化](#2.4 stl的部分变化)
[3 右值引用和移动语义](#3 右值引用和移动语义)
前言:
在C++11之前,C++98的出现使得C++看起来更像是一门独立的语言,C++委员会成立后,对外宣称的是5年一个版本,但是呢,计划赶不上变化,03年发布了C++03,计划07年发布07版本,变数多了,就一直拖啊拖,拖到了C++11,也就是2011年才发布,搁了这么久,C++11也是憋了一个大的,但是挨骂也挨多了,于是呢,后面就想着,有点新东西了就发布新版本,比如之后的C++14 C++17,C++20等,目前来说,C++98 C++11 C++20都是大版本,其中20还没有那么大,毕竟是3年更新一次,C++11相对于98来说,更正了许多错误,引入了很多新特性,包含了约140个新特性,600个缺陷修正,所以这个大版本需要学习的有很多,目前很多公司都是以11作为基准版本的,学习也是很有必要的。
1 统一列表初始化
初始化列表在我们前面vector的时候就有所涉及了,但是当时我们介绍的不是那么深入,介绍了数组赋值的那个花括号里面的叫做initializer_list,在C++11版本支持这种自定义的赋值,但是呢,组委会可能有点强迫症?于是对于内置类型也加上了Initializer_list的赋值方法:
cpp
int a = 1;
int b = { 1 };
要注意的是,这里的是列表初始化,而不是初始化列表。
C++11中委员会扩大了列表初始化的范围,从标准库里面的vector等stl容器到用户自定义类型或者是内置类型都可以这么初始化了,所以一个整型,一个数组可以初始化的方式就有很多种了:
cpp
int main()
{
int a = 1;
int b = { 1 };
int c(1);
int d{ 1 };
int arr1[]{ 1,2,3,4,5 };
int arr2[] = { 1,2,3,4,5 };
return 0;
}
对于单参数的变量来说,可以圆括号构造初始化,也可以花括号列表初始化,花括号的赋值符号可以删除,这个设计看起来可能有点冗余了,但是呢,毕竟发明出来了,咱们想用就用,毕竟影响不大。
所以在列表初始化就的出来了一个结论:万物皆可列表初始化
但是呢,列表初始化实际上走的也是隐式类型转换,比如单参数的自定义类型,构造函数加上explicit,构造函数变成了显式的,禁止了隐式类型转换,如下:
cpp
class A
{
public:
explicit A(int a = 0)
:_a(a)
{}
private:
int _a;
};
int main()
{
A a1 = 1;
A a2 = { 1 };
return 0;
}
此时代码就会报错,即:
不能隐式类型转换了,也就不能隐式的拷贝构造了,但是呢目前来说,我们对于explicit是不怎么用到的,了解一下。
提到了列表初始化,就不得不提到initializer_list了,这里以vector为例子:
在C++11的版本,支持initializer_list的构造,这样对于对象的创建就更加简单了。
http://www.cplusplus.com/reference/initializer_list/initializer_list/
这是关于initializer_list的文档,详细介绍了什么是initializer_list,我们简单来说就是数据的集合。
但是这个点更新的,怎么说呢,实际上来说用处不是那么大,更像是一种强迫症,但是更新了咱们就学,这里并不费脑子。
2 声明
2.1 auto
在C++11中对于auto简单的更新了一下,比如auto可以作为返回值了,这个点为某些场景提供了便利,但是对于这种场景:
cpp
auto Func4();
auto Func3()
{
return Func4();
}
auto Func2()
{
return Func3();
}
auto Func1()
{
return Func2();
}
请问Func1的的返回值是什么?这种场景就很绕了,你需要不停的去看每一层的数据类型,到头来甚至还是要你自己去推导,反而不利于代码的可读性了。
auto我们的应用场景可能是用来推导迭代器的数据类型,但是绝不是应用于这么复杂的场景:
cpp
int main()
{
vector<int> v1;
vector<int>::iterator it1 = v1.begin();
auto it2 = v1.end();
return 0;
}
对于某些变量来说,数据类型实在是太长了的,咱们就用auto是无可非议的,但是一般来说,不用auto我觉得代码可读性更好,至少说我知道这里表达的是什么意思。
2.2 decltype
介绍decltype之前,我们先来讨论一下typeid的用法,typeid是用来展现数据的真实类型的,但是typeid有时候又太真实,反而不利于我们学习,比如:
cpp
int main()
{
int a = 1;
int* pa = &a;
int& b = a;
cout << typeid(pa).name() << endl;
cout << typeid(b).name() << endl;
return 0;
}
这里打印出来pa的类型是指针,但是b的类型是int,你说是int呢,他本身是a的别名,说是int没错,但是你说它是int的引用类型呢,也可以,这里可能就有点容易混淆了。
那么typeid是不能用来定义数据类型的,它的作用是展现数据的类型,C++11引入了一个新的关键字,decltype,可以用来声明数据类型,它的英文展开就是declare type,声明类型的意思,用法如下:
cpp
int main()
{
int a = 1;
int* pa = &a;
int& b = a;
decltype(a) c = 1;
decltype(b) d = a;
decltype(*pa) e = 2;//e是引用类型
return 0;
}
这里相对于其他的类型来说声明是没有问题的,但是对于某些表达式来说,比如*pa,解引用之后应该是指针类型,但是这里的话呢,e是引用类型的,所以就会报错了。
类似的表达式还有:
cpp
int main()
{
int a = 1;
decltype((a)) pp = a;//pp是引用类型
const int m = 1;
const int& n = m;
decltype(n) k = m;//k是const int的引用类型
return 0;
}
decltype对于const的行为这里没介绍,想学习的可以在C++Primer 2.5.3节进行学习。
2.3 nullptr
在C++中,对于NULL定义一直是宏定义为0,这就可能引入部分问题了,因为同时可以表示整型和指针类型,C++11中出于安全考虑加入了nullptr,表示空指针。
2.4 stl的部分变化
在C++11中引入了多个新的容器,比如array,forward_list,unordered_map,unordered_set,以及对于容器的构造函数等插入了新的函数重载,以及引入了其他函数,比如emplace。
但是呢,新添加的部分容器实现没有必要,比如array,array的底层是一个静态数组,相对于vector来说,只是对越界来说检查更为严格了而已,感觉引入的必要实现不是太大,,
对于forward_list来说,单向链表,已经有了list了,双向列表,再引入一个单向链表的意义不太大。
但是其他两个容器就很有学习的意义了,基于哈希封装的unordered_map/set。
还有引入了右值构造的函数,以及增加了cend cbegin函数返回const迭代器。
3 右值引用和移动语义
上面的两个大点,可以说是简单了解,过一下即可,今天的重点是右值部分,相信同学们在学习容器的时候就已经听说过右值的大名了,今天它就来了。
在此之前,我们先了解一下,什么是左值?
左值的定义是可以对该变量取地址的话,那么该变量就是左值,如下:
cpp
int main()
{
int a = 1;
int* pa = &a;
vector<int> v1;
const int c = 0;
v1[0];
cout << &a << endl;
cout << &pa << endl;
cout << &v1 << endl;
cout << &c << endl;
cout << &v1[0] << endl;
return 0;
}
这些都是可以取地址的,所以这些变量统统叫做左值,我们平常的使用的引用都是左值引用,因为引用的都是左值,那么什么是右值呢?
右值与左值相对,即不可以取地址的叫做右值。
例如:
cpp
int Func()
{
int a = 1;
return a;
}
int main()
{
int a = 1;
const int c = 0;
cout << &(a + c) << endl;
cout << &(Func()) << endl;
cout << &1 << endl;
cout << &string("aaaaa") << endl;
return 0;
}
这些都被叫做,右值,但是呢,右值分为纯右值和将亡值。
什么是纯右值呢?
例如,10,这种字面量或者返回的a拷贝临时变量就是纯右值,那么什么是将亡值呢?
将亡值例如tostring返回的是一个字符串,编译器会拷贝一个字符串,然后传到main函数里面去,拷贝的那个临时对象就叫做将亡值,因为拷贝完成,它就会销毁了。
这里可以总结一下,纯右值就是内置类型或字面量值,将亡值就是自定义类型。
提问,左值能给右值取引用吗?
对于一般的左值是不可以的,但是右值的话,比如临时对象,具有常性,所以const的话,左值就可以引用右值了:
cpp
int main()
{
const string& s = string("aaa");
return 0;
}
同样的,右值可以给左值取别名吗?
cpp
int main()
{
int a = 0;
int&& ra = a;//不可以
int&& rb = move(a);
return 0;
}
一般是不可以的,但是对左值a使用move方法,就可以实现右值引用,move是什么后面再提,move就有点像强行将左值转到右值。
那么为什么引入右值呢?左值引用的短板是什么?
不可否认的是左值引用减少了一次拷贝构造,提升了效率,但是对于这种情况:
cpp
string Func()
{
string str = "a";
return str;
}
int main()
{
string str = Func();
return 0;
}
最初,编译器会有三次开辟空间,Func()函数里面开辟一次str的空间,主函数里面开辟一次str的空间,临时对象开辟一次空间,所以操作是Func函数返回的str拷贝了一次给临时对象,临时对象再拷贝构造给str,这样就有两次拷贝构造,对于编译器来说就会优化为直接一次构造,也就是用Func()的str来构造主函数的str。
这是经典的左值构造:
在C++98时经常使用。
那么现在引入右值,可能是组委会觉得,左值拷贝构造效率还可以提升一点:
这里画图解释:
这是经典的一次左值拷贝,右值就很离谱了:
因为Func里面的str是一个将亡值,编译器知道了,然后我们使用的还是右值引用,所以,直接,
"起死回生"!为什么打引号呢?因为主函数的str占用了Func里面的str指向的空间,之后Func::str就销毁了,就像是一种器官移植,将str的空间继续延续下去,这就是右值引用的恐怖之处。
那么,右值引用的本质是什么呢?
cpp
int main()
{
int a = 0;
int&& rb = move(a);
cout << &rb << endl;
return 0;
}
代码能跑通,所以说右值引用的本质是左值。
当我们使用右值引用进行构造的时候,叫做移动构造,右值引用赋值的时候叫做移动赋值,这就是移动语义。
因为右值引用的本质是左值,这个就很坑了,比如模拟实现的list,明明用的是右值,push_back的时候,调用了insert,但是传给insert的参数看起来是右值,但是本质的属性还是左值,就又会到左值的函数那里,那么好,cv一份右值insert的函数,里面涉及到了new节点,也就是构造一个节点出来,所以构造函数还不能写一个,还要写一份左值的一份右值的。
所以右值引用的本质是左值这个实现是给人坑住了。
那么引入了两个概念叫做 万能引用和完美转发:
刚才因为右值引用的本质是左值引用,我们想让他保持右值的属性,就可以用到完美转发,如:
cpp
void insert(T&& val)
{
//...
}
void Push_back(T&& val)
{
//...
insert(forward<T> (val));
}
即forward<T>,就是完美转发,有模板参数就加T,没有就不加,这样可以保持原生属性,那么什么是万能引用呢?
即两个&&,有人就问了,这不是右值引用吗?
是的,实际上来说是万能引用(在模板参数里面),也叫引用折叠,如:
cpp
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<class T>
void PerfectForward(T&& t)
{
Fun(forward<T>(t));
}
int main()
{
int a = 1;
const int ca = 1;
PerfectForward(a);
PerfectForward(ca);
PerfectForward(10);
PerfectForward(move(ca));
return 0;
}
PerfectForward函数的参数注意看,是两个引用,这个时候就说了,它是万能引用,不管传的是左值还是右值都可以引用了,想要保持原生属性只需要完美转发一下就可以,这个函数模板我们提供了,剩下的就是编译器要做的事了,那么也可以这样:
cpp
template<class T>
void PerfectForward(T&& t)
{
Fun((T&&)(t));
}
这个笔者解释的也不太清楚,但是确实可以这样,了解一下。
但是使用右值引用之前请避免这种场景哦:
cpp
int main()
{
string s1("111");
string s2 = move(s1);
return 0;
}
至于为什么,请看监视窗口~
感谢阅读!