目录

前言:

1 统一列表初始化

2 声明

2.1 auto

2.2 decltype

2.3 nullptr

2.4 stl的部分变化

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的赋值方法:

	int a = 1;
	int b = { 1 };

要注意的是,这里的是列表初始化,而不是初始化列表。

C++11中委员会扩大了列表初始化的范围,从标准库里面的vector等stl容器到用户自定义类型或者是内置类型都可以这么初始化了,所以一个整型,一个数组可以初始化的方式就有很多种了:

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,构造函数变成了显式的,禁止了隐式类型转换,如下:

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可以作为返回值了,这个点为某些场景提供了便利,但是对于这种场景:

auto Func4();

auto Func3()
{
	return Func4();
}

auto Func2()
{
	return Func3();
}

auto Func1()
{
	return Func2();
}

请问Func1的的返回值是什么?这种场景就很绕了,你需要不停的去看每一层的数据类型,到头来甚至还是要你自己去推导,反而不利于代码的可读性了。

auto我们的应用场景可能是用来推导迭代器的数据类型,但是绝不是应用于这么复杂的场景:

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有时候又太真实,反而不利于我们学习,比如:

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,声明类型的意思,用法如下:

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是引用类型的,所以就会报错了。

类似的表达式还有:

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 右值引用和移动语义

上面的两个大点,可以说是简单了解,过一下即可,今天的重点是右值部分,相信同学们在学习容器的时候就已经听说过右值的大名了,今天它就来了。
在此之前,我们先了解一下,什么是左值?

左值的定义是可以对该变量取地址的话,那么该变量就是左值,如下:

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;
}

这些都是可以取地址的,所以这些变量统统叫做左值,我们平常的使用的引用都是左值引用,因为引用的都是左值,那么什么是右值呢?

右值与左值相对,即不可以取地址的叫做右值

例如:

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的话,左值就可以引用右值了:

int main()
{
	const string& s = string("aaa");
	return 0;
}

同样的,右值可以给左值取别名吗?

int main()
{
	int a = 0;
	int&& ra = a;//不可以
	int&& rb = move(a);
	return 0;
}

 一般是不可以的,但是对左值a使用move方法,就可以实现右值引用,move是什么后面再提,move就有点像强行将左值转到右值。

那么为什么引入右值呢?左值引用的短板是什么?

不可否认的是左值引用减少了一次拷贝构造,提升了效率,但是对于这种情况:

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的空间继续延续下去,这就是右值引用的恐怖之处。

那么,右值引用的本质是什么呢?

int main()
{
	int a = 0;
	int&& rb = move(a);
	cout << &rb << endl;
	return 0;
}

代码能跑通,所以说右值引用的本质是左值

当我们使用右值引用进行构造的时候,叫做移动构造,右值引用赋值的时候叫做移动赋值,这就是移动语义

因为右值引用的本质是左值,这个就很坑了,比如模拟实现的list,明明用的是右值,push_back的时候,调用了insert,但是传给insert的参数看起来是右值,但是本质的属性还是左值,就又会到左值的函数那里,那么好,cv一份右值insert的函数,里面涉及到了new节点,也就是构造一个节点出来,所以构造函数还不能写一个,还要写一份左值的一份右值的。

所以右值引用的本质是左值这个实现是给人坑住了。

那么引入了两个概念叫做 万能引用和完美转发

刚才因为右值引用的本质是左值引用,我们想让他保持右值的属性,就可以用到完美转发,如:

void insert(T&& val)
{
	//...
}
void Push_back(T&& val)
{
	//...
	insert(forward<T> (val));
}

即forward<T>,就是完美转发,有模板参数就加T,没有就不加,这样可以保持原生属性,那么什么是万能引用呢?

即两个&&,有人就问了,这不是右值引用吗?

是的,实际上来说是万能引用(在模板参数里面),也叫引用折叠,如:

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函数的参数注意看,是两个引用,这个时候就说了,它是万能引用,不管传的是左值还是右值都可以引用了,想要保持原生属性只需要完美转发一下就可以,这个函数模板我们提供了,剩下的就是编译器要做的事了,那么也可以这样:

template<class T>
void PerfectForward(T&& t)
{
	Fun((T&&)(t));
}

这个笔者解释的也不太清楚,但是确实可以这样,了解一下。

但是使用右值引用之前请避免这种场景哦:

int main()
{
	string s1("111");
	string s2 = move(s1);
	return 0;
}

至于为什么,请看监视窗口~ 


感谢阅读!

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐