C++11新特性(一)右值引用

@


一、C++11简介

在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称。不过由于TC1主要是对C++98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C++98/03标准。从C++0x到C++11,C++标准10年磨一剑,第二个真正意义上的标准珊珊来迟。相比于C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率。

二、列表初始化

2.1 C++98中{}的初始化问题

在C++98中,标准允许使用花括号{}对数组元素进行统一的列表初始值设定。比如:

int array1[] = {1,2,3,4,5};
int array2[5] = {0};

对于一些自定义的类型,却无法使用这样的初始化。比如:

vector<int> v{1,2,3,4,5};

就无法通过编译,导致每次定义vector时,都需要先把vector定义出来,然后使用循环对其赋初始值,非常不方便。

C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。

2.2 内置类型的列表初始化

int main()
{
	// 内置类型变量
	int x1 = { 10 };
	int x2{ 10 };
	int x3 = 1 + 2;
	int x4 = { 1 + 2 };
	int x5{ 1 + 2 };
	// 数组
	int arr1[5]{ 1,2,3,4,5 };
	int arr2[]{ 1,2,3,4,5 };
	// 动态数组,在C++98中不支持
	int* arr3 = new int[5]{ 1,2,3,4,5 };
	// 标准容器
	vector<int> v{ 1,2,3,4,5 };
	map<int, int> m{ {1,1}, {2,2,},{3,3},{4,4} };
	return 0;
}

列表初始化可以在{}之前使用等号,其效果与不使用=没有什么区别。

2.3 自定义类型的列表初始化

  1. 标准库支持单个对象的列表初始化
class Point
{
public:
	Point(int x = 0, int y = 0) : _x(x), _y(y)
	{}
private:
	int _x;
	int _y;
};
int main()
{
	Point p{ 1, 2 }; // 单个对象直接使用列表初始化
	return 0;
}
  1. 多个对象的列表初始化
    多个对象想要支持列表初始化,需给该类(模板类)添加一个带有initializer_list类型参数的构造函数即可。注意:initializer_list是系统自定义的类模板,该类模板中主要有三个方法:begin()、end()迭代器以及获取区间中元素个数的方法size()。
    在这里插入图片描述
    什么是initializer_list类型?
initializer_list<int> ilt = { 1, 2, 3, 4, 5, 6, 7, 8 };

所以对于多对象可直接使用列表初始化

Point* ptr2 = new Point[2]{{1, 1}, { 2, 2 } };
pair<string, string> kv("left", "左边");
map<string, string> dict = { { "insert", "插入" }, {"sort", "排序"}, kv, make_pair("list", "列表")};

三、变量类型推导

3.1 为什么需要类型推导

在定义变量时,必须先给出变量的实际类型,编译器才允许定义,但有些情况下可能不知道需要实际类型怎
么给,或者类型写起来特别复杂,比如:

// 类型很长,再次使用该类型定义很麻烦
std::map<std::string, std::string> m{{"apple", "苹果"}, {"banana","香蕉"}};

// 想要知道一个由auto自动推导得到的变量的类型
auto x = ....;

3.2 decltype类型推导

3.2.1 为什么需要decltype

auto使用的前提是:必须要对auto声明的类型进行初始化,否则编译器无法推导出auto的实际类型。但有时候可能需要根据表达式运行完成之后结果的类型进行推导,因为编译期间,代码不会运行,此时auto也就无能为力。

template<class T1, class T2>
T1 Add(const T1& left, const T2& right)
{
	return left + right;
}

如果能用加完之后结果的实际类型作为函数的返回值类型就不会出错,但这需要程序运行完才能知道结果的实际类型,即RTTI(Run-Time Type Identification 运行时类型识别)。
C++98中确实已经支持RTTI:

typeid只能查看类型不能用其结果类定义类型
dynamic_cast只能应用于含有虚函数的继承体系中

运行时类型识别的缺陷是降低程序运行的效率。

3.2.2 decltype

decltype是根据表达式的实际类型推演出定义变量时所用的类型,比如:

  1. 推演表达式类型作为变量的定义类型
int main()
{
	int a = 10;
	int b = 20;
	// 用decltype推演a+b的实际类型,作为定义c的类型
	decltype(a+b) c;
	cout<<typeid(c).name()<<endl;
	return 0;
}
  1. 推演函数返回值的类型
void* GetMemory(size_t size)
{
	return malloc(size);
}
int main()
{
	// 如果没有带参数,推导函数的类型
	cout << typeid(decltype(GetMemory)).name() << endl;
	// 如果带参数列表,推导的是函数返回值的类型,注意:此处只是推演,不会执行函数
	cout << typeid(decltype(GetMemory(0))).name() <<endl;
	return 0;
}

decltype最大的用处就是

  • 可以使用未知类型变量的类型去定义一个同类型对象
  • 当做模板参数类型传入vector<decltype(a + b) > v

四、默认成员函数控制

在C++中对于空类编译器会生成一些默认的成员函数,比如:构造函数、拷贝构造函数、运算符重载、析构函数和&和const&的重载、移动构造、移动拷贝构造等函数。如果在类中显式定义了,编译器将不会重新生成默认版本。有时候这样的规则可能被忘记,最常见的是声明了带参数的构造函数,必要时则需要定义不带参数的版本以实例化无参的对象。而且有时编译器会生成,有时又不生成,容易造成混乱,于是C++11让程序员可以控制是否需要编译器生成。

4.1 显式缺省函数

在C++11中,可以在默认函数定义或者声明时加上=default,从而显式的指示编译器生成该函数的默认版本 ,用=default修饰的函数称为显式缺省函数。

class A
{
public:
	A(int a) : _a(a)
	{}
	// 显式缺省构造函数,由编译器生成
	A() = default;
	// 在类中声明,在类外定义时让编译器生成默认赋值运算符重载
	A& operator=(const A& a);
private:
	int _a;
};
A& A::operator=(const A& a) = default;

4.2 删除默认函数

如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且不给定义,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本 ,称=delete修饰的函数为删除函数。

// 禁止编译器生成默认的拷贝构造函数以及赋值运算符重载
A(const A&) = delete;
A& operator(const A&) = delete;

五、右值引用

5.1 右值引用概念

C++98中提出了引用的概念,引用即别名,引用变量与其引用实体公共同一块内存空间,而引用的底层是通过指针来实现的,因此使用引用,可以提高程序的可读性。
为了提高程序运行效率,C++11中引入了右值引用,右值引用也是别名,但其只能对右值引用。

语法: &&
const int&& ra = 10;

左值引用给左值取别名
右值引用给右值取别名

5.2 左值与右值

左值与右值是C语言中的概念,但C标准并没有给出严格的区分方式,一般认为:可以放在=左边的,或者能够取地址的称为左值,只能放在=右边的,或者不能取地址的称为右值,但是也不一定完全正确。

int main()
{
	int a = 10;
	int b = 20;
	// a和b都是左值,b既可以在=的左侧,也可在右侧,
	// 说明:左值既可放在=的左侧,也可放在=的右侧
	a = b;
	b = a;
	const int c = 30;
	// 编译失败,c为const常量,只读不允许被修改
	//c = a;
	// 因为可以对c取地址,因此c严格来说不算是左值
	cout << &c << endl;
	// 编译失败:因为b+1的结果是一个临时变量,没有具体名称,也不能取地址,因此为右值
	//b + 1 = 20;
	return 0;
}

因此关于左值与右值的区分不是很好区分,一般认为:

  1. 普通类型的变量,因为有名字,可以取地址,都认为是左值。
  2. const修饰的常量,不可修改,只读类型的,理论应该按照右值对待,但因为其可以取地址(如果只是const类型常量的定义,编译器不给其开辟空间,如果对该常量取地址时,编译器才为其开辟空间),C++11认为其是左值。
  3. 如果表达式的运行结果是一个临时变量或者对象,认为是右值。
  4. 如果表达式运行结果或单个变量是一个引用则认为是左值。

总结:

  1. 不能简单地通过能否放在=左侧右侧或者取地址来判断左值或者右值,要根据表达式结果或变量的性质
    判断,比如上述:c常量
  2. 能得到引用的表达式一定能够作为引用,否则就用常引用。

C++11对右值进行了严格的区分:

  • C语言中的纯右值,比如:a+b, 100
  • 将亡值。比如:表达式的中间结果、函数按照值的方式进行返回。

一般可以根据是否可以取地址来判别左右值,可以取地址的位左值,不可以取地址的位右值。

常见右值:字面常量(如10)、表达式返回值(x+y)、传值返回的函数的返回值(add(x+y) ).

5.3 引用与右值引用比较

在C++98中的普通引用与const引用在引用实体上的区别:

int main()
{
	// 普通类型引用只能引用左值,不能引用右值
	int a = 10;
	int& ra1 = a; // ra为a的别名
	//int& ra2 = 10; // 编译失败,因为10是右值
	const int& ra3 = 10;
	const int& ra4 = a;
	return 0;
}

普通引用只能引用左值,不能引用右值,const引用既可引用左值,也可引用右值。
C++11中右值引用:只能引用右值,一般情况不能直接引用左值。但是右值引用可以引用move之后的左值。

int a = 10;
int &&ra = move(a); // move改变a的左值属性为右值
int main()
{
	// 10纯右值,本来只是一个符号,没有具体的空间,
	// 右值引用变量r1在定义过程中,编译器产生了一个临时变量,r1实际引用的是临时变量
	int&& r1 = 10;
	r1 = 100;
	int a = 10;
	int&& r2 = a; // 编译失败:右值引用不能引用左值
	return 0;
}

问题:既然C++98中的const类型引用左值和右值都可以引用,那为什么C++11还要复杂的提出右值引用呢?
主要是为了后续的转移资源,减少深拷贝的次数。

5.4 值的形式返回对象的缺陷

先回顾一下左值引用的作用:

  • 做参数,基本完美,避免使用指针
  • 做函数返回值,不够完美,无法返回右值

这里的代码为了好理解,不设计真正的深拷贝,内存管理,但右值语法都能体现。

#include <iostream>
using namespace std;
class A
{
public:
	A(int x = 1) : _x(x)
	{}
	A(const A& a)
	{
		_x = a._x;
		cout << "A(const A& a) -- 深拷贝" << endl;
	}
	A& operator=(const A& a)
	{
		_x = a._x;
		return *this;
	}
private:
	int _x;
};

A addA(int x, int y)
{
	A res(x + y);
	return res;
}

int main()
{
	A ans = addA(2, 3);
	return 0;
}

在这里插入图片描述
可以发现,res在函数体内被构造很快又被析构,但是它的数据其实是需要的,这就产生了构造析构的资源浪费,程序的效率也会降低。
在这里插入图片描述

5.5 移动语义

C++11提出了移动语义概念,即:将一个对象中资源移动到另一个对象中的方式,可以有效缓解该问题。

把将亡值的资源给一个有用的资源。
在这里插入图片描述
这里交换两个变量的数据,使得拷贝次数减少,以及减少不断申请释放空间的消耗。

在C++11中如果需要实现移动语义,必须使用右值引用。给A添加移动构造

A(A&& a)
{
	_x = a._x;// 交换资源,保留将亡值的资源,让将亡值释放一份空的资源,
	cout << "A(const A& a) -- 移动构造" << endl;
}

在这里插入图片描述

在这里插入图片描述

所以移动构造大大减少了深拷贝的次数,减少了资源的浪费。

STL容器都有移动构造
在这里插入图片描述

注意:

  1. 移动构造函数的参数千万不能设置成const类型的右值引用,因为资源无法转移而导致移动语义失效。
  2. 在C++11中,编译器会为类默认生成一个移动构造,该移动构造为浅拷贝,因此当类中涉及到资源管理时,用户必须显式定义自己的移动构造。

除了移动构造还有移动赋值

A& operator=(const A& a)
{
	_x = a._x;
	cout << "A& operator=(const A& a) -- 普通赋值" << endl;
	return *this;
}
A& operator=(A&& a)
{
	_x = a._x;
	cout << "A& operator=(A&& a) -- 移动赋值" << endl;
	return *this;
}

在这里插入图片描述

5.6 右值引用引用左值

按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::move()函数位于 头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。

注意:

  1. 被转化的左值,其生命周期并没有随着左值的转化而改变,即std::move转化的左值变量lvalue不会被销毁。
  2. STL中也有另一个move函数,就是将一个范围中的元素搬移到另一个位置。
#include <string>
int main()
{
	string s1("hello world");
	string s2(move(s1));
	string s3(s2);
	cout << s1 << endl;
	cout << s2 << endl;
	cout << s3 << endl;
	
	return 0;
}

以上代码是move函数的经典的误用,因为move将s1转化为右值后,在实现s2的拷贝时就会使用移动构造,此时s1的资源就被转移到s2中,s1就成为了无效的字符串。
在这里插入图片描述

单独使用move并不会对变量有影响

string s1("hello world");
move(s1); // s1仍然是左值
string s2(move(s1)); // 强转为右值后进行移动语意

5.7 完美转发

由于右值引用后会开辟空间,所以变成了左值,在参数传递的过程中会出现右值属性丢失的情况。
比如:

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<typename T>
void PerfectForward(T&& t)
{
	Fun(t);
}

int main()
{
	int a;
	PerfectForward(a);            // 左值
	PerfectForward(std::move(a)); // 右值
	cout << endl;
	const int b = 8;
	PerfectForward(b);		      // const 左值
	PerfectForward(std::move(b)); // const 右值
	return 0;
}

在这里插入图片描述

完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。

完美转发是目标函数总希望将参数按照传递给转发函数的实际类型转给目标函数,而不产生额外的开销,就好像转发者不存在
一样。

所谓完美:函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;如果相应实参是右值,它就应该被转发为右值。 这样做是为了保留在其他函数针对转发而来的参数的左右值属性进行不同处理(比如参数为左值时实施拷贝语义;参数为右值时实施移动语义)。

C++11通过forward函数来实现完美转发forward<T>(v)
比如:

在这里插入图片描述

5.8 右值引用作用

C++98中引用作用:因为引用是一个别名,需要用指针操作的地方,可以使用指针来代替,可以提高代码的
可读性以及安全性。
C++11中右值引用主要有以下作用:

  1. 实现移动语义(移动构造与移动赋值)
  2. 给中间临时变量取别名:
int main()
{
	string s1("hello");
	string s2(" world");
	string s3 = s1 + s2; // s3是用s1和s2拼接完成之后的结果拷贝构造的新对象
	stirng&& s4 = s1 + s2; // s4就是s1和s2拼接完成之后结果的别名
	return 0;
}
  1. 实现完美转发

六、新的类功能

C++11 新增了两个:移动构造函数和移动赋值运算符重载。

针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:

  • 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载都没有实现。那么编译器会自动生成一个默认移动构造。
  • 默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如
    果实现了就调用移动构造,没有实现就调用拷贝构造。
  • 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载都没有实现,那么编译器会自动生成一个默认移动赋值。
  • 默认生成的移动赋值函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
  • 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

s_persist

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值