当前位置: 首页 > article >正文

C++:C++11新特性---右值引用

文章目录

  • 初始化方式
  • 显示查看类型
  • initializer_list
  • decltype
  • 左值引用和右值引用
    • move
    • 左右值引用的场景
  • 万能引用和完美转发

本篇总结C++11新特性

初始化方式

C++11对参数列表的初始化有了更明确的定义,可以这样进行定义

// 列表初始化
void test1()
{
	// 旧版本
	int x = 0;
	// 新版本
	int y{ 0 };
	int z = { 0 };
	int arr[]{ 10,20,30 };
	int* pa = new int[5]{ 1,2,3,4,5 };
}

在对类的赋值的时候,也可以这样进行赋值

struct t
{
	int a;
	int b;
};

void test2()
{
	t* pt = new t[5]{ 1,2 };
}

显示查看类型

// 查看类型
void test3()
{
	int a = 1;
	cout << typeid(a).name() << endl;
	auto it = map<int, int>().begin();
	cout << typeid(it).name() << endl;
}

运行结果:

int
class std::_Tree_iterator<class std::_Tree_val<struct std::_Tree_simple_types<struct std::pair<int const ,int> > > >

initializer_list

这是什么呢?如何理解这个类型?先看一下在什么场景中会出现这个东西

void test4()
{
	auto lit = { 1,2,3,4 };
	cout << typeid(lit).name() << endl;
}

那么这个东西是干什么的呢?有什么用呢?

在这里插入图片描述
在C++11中,对于STL的各类容器的构造函数中,新增了这样的构造方式,有点类似于一个数组,它里面可以存储任意类型的数据,然后可以交给vector来实现构造,因此下面就要对{}进行一个对比

void test5()
{
	// 利用initializer_list进行初始化
	vector<int> v{ 1,2,3,4,5 };
	auto lit = { 1,2,3,4 };
	vector<int> vc(lit);

	// 调用参数初始化列表进行初始化
	int arr[]{ 1,2,3,4,5 };
}

这两种写法看似,但是实际上底层是完全不同的两种实现的方式

decltype

// decltype的一些使用使用场景
template<class T1, class T2>
void F(T1 t1, T2 t2)
{
	decltype(t1 * t2) ret;
	cout << typeid(ret).name() << endl;
}

void test6()
{
	const int x = 1;
	double y = 2.2;
	decltype(x * y) ret;
	decltype(&x) p;
	cout << typeid(ret).name() << endl;
	cout << typeid(p).name() << endl;
	F(1, 'a');
}

简单来说,就是可以把你要新定义的一个类型进行人为的定义,定义成一个你想让它变成的类型,使用场景也不算多,但是可以这样进行使用

左值引用和右值引用

首先要解决一个问题,什么是左值?

之前可能会说,左值就是等号左边的值,这肯定是不对的,左值是一个表示数据的表达式,可以获取它的地址,也可以进行赋值,通常来说它出现在赋值符号的左边,右值不能出现在赋值符号的左边,定义的时候,const修饰符修饰的变量不可以对它进行赋值,但是可以对它取地址,因此,可以说左值引用就是给左值的引用,给左值取别名

常见的左值

int* p = new int(0);
int b = 1;
const int c = 2;

因此我们说,可以对这些起一个别名

int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;

常见的右值

10;
x + y;
fmin(x, y);

而对这些起别名就是右值引用

int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);

对于右值引用来说,可以理解为,右值本身是不能取地址的,但是给右值取别名之后,右值就会被存储到某些地方,此时就可以取到它的地址

左值引用只能引用左值,不能引用右值,如果想引用右值需要用const

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

move

右值只能引用右值,不能引用左值,但是右值可以引用move之后的左值

void test7()
{
	// 右值引用只能右值,不能引用左值。
	int&& r1 = 10;
	// error C2440: “初始化”: 无法从“int”转换为“int &&”
	// message : 无法将左值绑定到右值引用
	int a = 10;
	//int&& r2 = a;
	// 右值引用可以引用move以后的左值
	int&& r3 = std::move(a);
}

左右值引用的场景

那左右值引用能干啥呢?

左值引用

对比下面两种传参方式

void func(const string& str);
void func(string str);

从传参的效率上就不一样了,对于传引用来说,每次传参的代价是很低的,只需要把变量的地址给过去就可以了,但是对于传值来说就不一样了,传参的代价是相当高的,需要把原始的参数拷贝给新的值

也就是说,左值引用做参数减少了拷贝,提高效率的使用场景和价值

左值引用的缺陷

那左值引用有什么缺陷呢?

第一个是,当函数返回的对象是一个局部对象的时候,是不可以使用传引用返回的,因为这个变量出了作用域就被销毁了,因此不能使用传左值引用,只能进行传值返回,例如下面的场景

	mystring::string to_string(int x)
	{
		mystring::string ret;
		while (x)
		{
			int val = x % 10;
			x /= 10;
			ret += ('0' + val);
		}
		reverse(ret.begin(), ret.end());
		return ret;
	}

此时的string是一个局部变量,如果使用传引用返回会访问一个不存在的地址,这是不被允许的,但如果采用传值返回,又会导致增加了拷贝构造的次数,并且可能还是两次

在新的编译器中,函数得到的这个string在返回的时候,会直接赋给新的值,调用一次拷贝构造,而在旧一点的编译器中,从函数的返回值会先拷贝构造一次到一个临时常量,再从这个临时常量拷贝构造一次到外部定义的string中

而使用移动构造,可以解决这个问题,编译器会默认使用最匹配的参数进行调用,因此会优先使用移动构造

搭配move函数

move函数的作用,就是单纯的把左值转换成右值引用,由此来实现移动语义

那这样有什么用呢?该如何理解这个意思呢?

如果有下面的代码:

string s2(s1)

这样的语句代表的含义是,调用拷贝构造,这里的s1是一个左值

但是如果要是改成这样

string s2(move(s1))

这样就不一样了,把s1放move函数中,这样就可以把s1当成一个右值,而右值是可以调用移动构造的,这样就可以不用调用拷贝构造浪费空间,而是可以直接的把值置换到我们所需要的s2里面,但是这样其实是不好的,这样会导致,虽然确实把s2的值填充了,但是却把s1的值架空了

简单来说,移动构造就是把资源全部偷过来,把原来的资源都架空

再举一个例子:

int main()
{
list<string> lt;
string s1("1111");
// 这里调用的是拷贝构造
lt.push_back(s1);
// 下面调用都是移动构造
lt.push_back("2222");
lt.push_back(std::move(s1));
return 0;
}

如果只是简单的调用s1,那么s1会被当成是一个左值,而左值会调用的是拷贝构造,但是如果把它强制转换成右值,那么就会调用的是移动构造,很明显,移动构造的使用成本是要比拷贝构造低很多的

万能引用和完美转发

前面关于右值引用中和前面有比较大不同的一点就是出现了&&符号,如果把这个符号看成是右值引用的标识符,也是不对的,C++11在模板中也新增了关于&&符号,这个符号代表的是万能引用,而不是右值引用,简单来说就是,既能接收左值也能接收右值

模板的万能引用只是提供了一个可以接收左值和右值的能力,一般来说是不可以两个都接收的
实际的使用中,引用类型的作用会限制接收的类型,会变成左值,而如果想要在传递的过程中保持右值的属性,就需要用到万能引用和完美转发

下面做一个实验来验证功能

void check(int& t)
{
	cout << "左值引用" << endl;
}

void check(int&& t)
{
	cout << "右值引用" << endl;
}

void check(const int& t)
{
	cout << "const左值引用" << endl;
}

void check(const int&& t)
{
	cout << "const右值引用" << endl;
}

void test8()
{
	const int a = 1;
	const int& b = 1;
	int c = 0;
	check(a);
	check(b);
	check(c);
	check(1);
	check(move(a));
	check(move(b));
}

在这里插入图片描述

结果也是符合预期的,把上述代码进行适当更改

void check(int& t)
{
	cout << "左值引用" << endl;
}

void check(int&& t)
{
	cout << "右值引用" << endl;
}

void check(const int& t)
{
	cout << "const左值引用" << endl;
}

void check(const int&& t)
{
	cout << "const右值引用" << endl;
}

void func1(int t)
{
	check(t);
}

void test8()
{
	const int a = 1;
	const int& b = 1;
	int c = 0;
	func1(move(a));
}

此时运行结果是左值引用,这是为什么?其原因是进入func1函数后,函数参数就从右值变成左值了,此时它的右值属性就会消失,因此出现了下面的用法:

template<class T>
void func1(T&& t)
{
	check(t);
}

此时运行结果是const左值引用,这说明它保持了const属性,但是依旧没有保持右值的属性,右值的属性依旧被退化成左值了

再进行改良

template<class T>
void func1(T&& t)
{
	check(forward<T>(t));
}

此时运行结果就是const右值引用了,而新增的这个forward其实就是完美转发,它可以保障原来的属性,把原来这个值的属性转发出去,严格意义来说保持的是左值和右值,而如果没有万能引用运行结果依旧会丢失const属性


http://www.kler.cn/a/148735.html

相关文章:

  • 民事诉讼中,火灾事故认定书并非不可推翻,其证明力弱于鉴定意见
  • 新版Apache tomcat服务安装 Mac+Window双环境(笔记)
  • 博物馆实景复刻:开启沉浸式文化体验的新篇章
  • 操作系统实验:在linux下用c语言模拟进程调度算法程序
  • [Mysql] Mysql的多表查询----多表关系(上)
  • Go 语言中,golang结合 PostgreSQL 、MySQL驱动 开启数据库事务
  • Notion for Mac:打造您的专属多功能办公笔记软件
  • 小狐狸ChatGPT付费创作系统V2.3.4独立版 +WEB端+ H5端最新去弹窗授权
  • 【开题报告】基于uniapp的瑜伽学习交流小程序的设计与实现
  • 近五年—中国十大科技进展(2018年—2022年)
  • Spring Cloud 版本升级记:OpenFeignClient与Gateway的爱恨交织
  • 【C++ Primer Plus学习记录】while循环
  • 批量将本地N个英文Html文档进行中文翻译-操作篇
  • 移动应用开发介绍及iOS方向学习路线(HUT移动组版)
  • OpenCV | 模版匹配
  • SpringCloudAlibaba整合Gateway实现网关
  • stm32 TIM
  • 【Docker项目实战】使用Docker部署Plik临时文件上传系统
  • 车载以太网-DHCP
  • Python中的datetime库
  • 硬件工程师助理怎么买器件
  • 08-学成在线项目中统一异常处理的规范
  • C# WPF上位机开发(乘法计算小软件)
  • 面试:MyBatis问题
  • 论文阅读——DDeP(cvpr2023)
  • 2017年五一杯数学建模C题宜居城市问题值解题全过程文档及程序