首页 > 编程语言 >C++练级计划->《右值引用和移动语义》

C++练级计划->《右值引用和移动语义》

时间:2024-11-29 13:31:18浏览次数:8  
标签:右值 int 练级 左值 C++ 引用 构造 拷贝

目录

什么是左值右值?

什么是左值?

什么是右值?

左值引用和右值引用

左值引用

右值引用

右值引用使用场景和意义

使用场景:

左值引用的短板

右值引用和移动语义:

移动构造:

右值引用的使用场景+1:

完美转发

万能引用


什么是左值右值?

什么是左值?

左值是一个有具体地址的值。

  • 左值可以被取地址。
  • 左值既可以在赋值符号左边也可以在右边。
int main()
{
	//以下的p、b、c、*p都是左值
	int* p = new int(0);
	int b = 1;
	const int c = 2;
    int d = c;//左值左右都在
	return 0;
}

什么是右值?

  • 右值不可以取地址,且不能被更改
  • 右值只能出现在右边,只能是赋值的那个
int main()
{
	double x = 1.1, y = 2.2;

	//以下几个都是常见的右值
	10;
	x + y;
	fmin(x, y);

	//错误示例(右值不能出现在赋值符号的左边)
	//10 = 1;
	//x + y = 1;
	//fmin(x, y) = 1;
	return 0;
}

没被赋值的数字,然后没有变量接收的算式。

  • 右值可以看做一个临时常量或者一个常量值,10是一个常量,x + y fmin(x,y)二者都是一个临时变量,这些都是右值。
  • 可以看出这些值并没有被存储,所以右值不能被取地址,只有被接收后才能有地址但这时就变成左值了。
  • 右值不允许被取地址和更改

左值引用和右值引用

引用的本义就是给一个值起一个别名。

左值引用

就是给左值起别名,也是用&来声明

int main()
{
	//以下的p、b、c、*p都是左值
	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;
	return 0;
}

右值引用

就是给右值起别名,但是通过 &&两个来声明

int main()
{
	double x = 1.1, y = 2.2;
	
	//以下几个都是常见的右值
	10;
	x + y;
	fmin(x, y);

	//以下几个都是对右值的右值引用
	int&& rr1 = 10;
	double&& rr2 = x + y;
	double rr3 = fmin(x, y);
	return 0;
}

 通常来说右值是没有地址的但是在你给它起别名的那时候,他就被赋予了地址,这时其实和左值是类似的,只是在语义上他是右值。那就引申出来一个问题,

左值引用可以引用右值吗?

有两种情况:

1.没有const

就是说:


int& rr1 = 10;

这样是不可以的,因为左值引用是可以修改对应内容的,而右值是不能被修改的,这是一种权限放大所以不行。

2.有const

const int& rr1 = 10;

这时就可以了,因为 此时的右值不能更改,但是有rr1这个地址了,和右值引用类似。

右值引用可以引用左值吗?

 不可以,右值引用不可以引用左值,左值是可修改的,虽然这是权限缩小,但是语义上是不行的

但是有一个方法可以就是使用一个C++11配合右值引用的move(左值)函数,此时move后的左值就可以赋值给右值引用。

int main()
{
	int a = 10;

	//int&& r1 = a;     //右值引用不能引用左值
	int&& r2 = move(a); //右值引用可以引用move以后的左值
	return 0;
}

右值引用使用场景和意义

所以左值引用既可以引用右值又可以引用左值,那还要右值引用干嘛?

现在假设我们在一个string类中的构造函数都加上了一句打印信息

namespace aron
{
    //拷贝构造函数(现代写法)
		string(const string& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;

			string tmp(s._str); //调用构造函数,构造出一个C字符串为s._str的对象
			swap(tmp); //交换这两个对象
		}
		//赋值运算符重载(现代写法)
		string& operator=(const string& s)
		{
			cout << "string& operator=(const string& s) -- 深拷贝" << endl;

			string tmp(s); //用s拷贝构造出对象tmp
			swap(tmp); //交换这两个对象
			return *this; //返回左值(支持连续赋值)
		}
}

使用场景:

  • 左值引用做参数,防止传参时进行拷贝操作。
  • 左值引用做返回值同理
void func1(cl::string s)
{}
void func2(const cl::string& s)
{}
int main()
{
    //调用是自己实现的string类里面加了打印信息
	aron::string s("hello world");
	func1(s);  //值传参
	func2(s);  //左值引用传参

	s += 'X';  //左值引用返回
	return 0;
}

1.值传参时多调用了一次拷贝构造。

2.+=时如果用的传值返回也会调用一次拷贝构造。

因为string类的拷贝要求的是深拷贝(重新建立一块空间拷贝对象,浅拷贝:和原对象使用一块空间)。

这里看出左值引用已经可以了,但是有些深拷贝是不能避免的。

左值引用的短板

左值引用无法返回局部变量,因为局部变量出了作用域就销毁了,只能使用传值返回。

右值引用就是为了解决左值引用这个短板的

右值引用和移动语义:

解决方法就是给string类增加移动构造和移动赋值

移动构造:

顾名思义也是一个构造函数,这个构造函数的参数是右值引用的,它的做法就是把传入的右值的资源窃取过来,避免深拷贝。小偷构造用了swap函数

我们可以写一个移动构造函数:

namespace aron
{
	class string
	{
	public:
		//移动构造
		string(string&& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(string&& s) -- 移动构造" << endl;
			swap(s);
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};
}

和拷贝构造的区别:

  • 在没有移动构造前,都是用的拷贝构造左值引用做参数,所以不管传入右值还是左值,都会拷贝一次。
  • 增加移动构造后,移动构造的参数是右值引用,其实他就是拷贝构造的一个重载函数,所以如果传入右值时会优先调用移动构造(最匹配原则)。
  • 移动构造中调用swap对资源进行窃取转移,因此移动构造的代价比调用拷贝构造的代价小。

在我们需要返回一个局部变量时,本来如果是值返回,但是有移动构造后,直接把局部变量的内容swap过来,此时直接返回,就不用在和值返回一样拷贝构造一个新的返回。 

int main()
{
	aron::string s = aron::to_string(1234);
	return 0;
}

to_string中局部对象是一个左值,这个局部对象(将亡值)出不了作用域,这时如果传值返回的话就会在传出前在进行一次拷贝构造,而如果此时我们直接把它资源拿出来给s 不是更香。

所以“将亡值”我们可以用移动构造传出即可。减少了一次深拷贝

为什么移动构造比拷贝构造代价小? 

移动构造是右值时使用,直接把右值的资源换给新的对象即可,而拷贝构造就是重新造一个模子,即重新造一个对象 ,然后这个把这个对象的值换给新对象。

编译器对于这方面的优化

 上面不是说,值返回时一般是调用构造函数拷贝出来一个对象,然后把这个对象在调用一次深拷贝给到新对象。

 所以说这里应该进行了两次深拷贝,但其实很多编译器在这一步都会进行优化的一步到位。调用一次拷贝构造直接给到新对象

但这一步依然是深拷贝,但还是减少了一次。

而C++11出来以后这个优化依然成立

本来的两次拷贝构造,更换成了两次移动构造。然后优化成一次移动构造,但其实两次移动构造和一次构造的差别不是很大,因为都只是交换资源并没有进行深拷贝。

如果我们是用一个已经存在的对象来接收这个返回值。那这个2->1的优化就没了。

因为对象已经存在了,我们无法直接用拷贝构造给到这个新对象,只能用赋值构造然后给到这个对象,这个是语法问题。

就算是移动构造也需要在进行一次移动构造,然后在进行移动赋值。

移动赋值

 和移动构造,移动赋值就是赋值构造的重载函数其中参数是右值引用。

namespace aron
{
	class string
	{
	public:
		//移动赋值
		string& operator=(string&& s)
		{
			cout << "string& operator=(string&& s) -- 移动赋值" << endl;
			swap(s);
			return *this;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};
}

和原本的赋值构造的区别:

和移动构造一样。都是小偷,直接把右值的内容窃取出来。

有了移动构造和移动赋值后,就算是给一个已经存在的对象赋值都没有深拷贝了,因为用移动构造把"将亡值"的内容偷出来后,在用移动赋值把内容给到存在的对象。两次偷的就不用造模子,就快。

 STL中的容器在C++11后基本都引入了移动构造和移动赋值。原因就是为了快。

右值引用引用左值之前说要用move函数

template<class _Ty>
inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT
{
	//forward _Arg as movable
	return ((typename remove_reference<_Ty>::type&&)_Arg);
}

_Ty&&_Arg这里看似是一个右值引用,但是因为有了模版所以这里其实是一个万能引用。即这个引用既能是左值也能是右值。

右值引用的使用场景+1:

插入函数:

int main()
{
	list<aron::string> lt;
	aron::string s("1111");

	lt.push_back(s); //调用string的拷贝构造

	lt.push_back("2222");             //调用string的移动构造
	lt.push_back(aron::string("3333")); //调用string的移动构造
	lt.push_back(std::move(s));       //调用string的移动构造
	return 0;
}

上面的没出现右值前通通都是进行拷贝构造,都会进行一次深拷贝,而出现右值后,下面的三个直接调用移动构造把资源转移出去,就没有拷贝构造。

完美转发

万能引用

在使用模版的 &&就不是右值引用了,而是万能引用。左值右值都能接收。

template<class T>
void PerfectForward(T&& t)
{
	//...
}

万能引用很显然,右值引用是需要通过重载然后确定是右值然后调用右值引用版本,而万能引用则通过传入的参数进行推导

下面代码是通过万能引用拿到不同版本的重载函数:

void Func(int& x)
{
	cout << "左值引用" << endl;
}
void Func(const int& x)
{
	cout << "const 左值引用" << endl;
}
void Func(int&& x)
{
	cout << "右值引用" << endl;
}
void Func(const int&& x)
{
	cout << "const 右值引用" << endl;
}
template<class T>
void PerfectForward(T&& t)
{
	Func(t);
}
int main()
{
	int a = 10;
	PerfectForward(a);       //左值
	PerfectForward(move(a)); //右值

	const int b = 20;
	PerfectForward(b);       //const 左值
	PerfectForward(move(b)); //const 右值

	return 0;
}

但是我们实际调用了后发现,全都匹配到了const左值或者左值引用的版本。因为右值被引用后是会产生自己的地址,并且也可修改了,所以就被退化识别成了左值。

解决这个问题需要用到完美转发:

template<class T>
void PerfectForward(T&& t)
{
	Func(std::forward<T>(t));
}

通过调用std::forward<T>()就能解决。

标签:右值,int,练级,左值,C++,引用,构造,拷贝
From: https://blog.csdn.net/a1275174052/article/details/143993133

相关文章

  • C++关于二叉树的具体实现
    目录1.二叉树的结构2.创建一棵二叉树3.二叉树的先序遍历1.借助栈的先序遍历2.利用递归的先序遍历4.二叉树的中序遍历5.二叉树的后序遍历1.借助栈的后序遍历2.利用递归的后序遍历6.二叉树的层序遍历7.tree.h8.tree.cpp9.main.cpp1.二叉树的结构对于二叉树来说......
  • C++:多态的原理
    目录一、多态的原理1.虚函数表 2.多态的原理  二、单继承和多继承的虚函数表1、单继承中的虚函数表2、多继承中的虚函数表  一、多态的原理1.虚函数表 首先我们创建一个使用了多态的类,创建一个对象来看其内部的内容:#include<iostream>usingnamespacestd;......
  • C++二级抽测题目(答案+题目)
    今天我给大家出一套C++二级考题限时2.5小时,大家加油!!!题目1:温度转换说明编一程序,将摄氏温度换为华氏温度。公式为:f=9/5*c+32。其中f为华氏温度,c是摄氏温度。(5.2.12)输入格式输入一行,只有一个整数c输出格式输出只有一行,包括1个实数。(保留两位小数)样例输入数据15......
  • C++类和对象(下)
    构造函数之前我们实现构造函数时,初始化成员变量主要使用函数体内赋值,构造函数初始化还有一种方式,就是初始化列表,初始化列表的使用方式是以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。每个成员变量在初始化列表中......
  • C++读写word文档(.docx)-DuckX库的使用
    DuckX是一个用于创建和编辑MicrosoftWord(.docx)文件的C++库。本文将简单介绍其用法,库的编译可见https://blog.csdn.net/hfy1237/article/details/144129745一、基本用法1.读取文档#include<iostream>#include"duckx.hpp"intmain(){ duckx::Document......
  • 精准高效-C++语言集成翔云VIN码识别接口、vin码识别sdk
    在当今快节奏的商业环境中,汽车行业面临着前所未有的挑战与机遇。无论是二手车交易、保险评估还是供应链管理,准确快速地获取车辆信息已成为提高效率、增强竞争力的关键。针对市场需求,翔云提供了VIN码识别接口,能够精确捕捉VIN码并输出,用科技的力量助力企业优化业务流程。......
  • 人脸识别API解锁智能生活、C++人脸识别接口软文
    在这个数字化转型的时代,科技正以前所未有的速度改变着我们的生活方式。其中,人脸识别技术作为人工智能领域的一项重要突破,已经逐渐渗透到我们生活的方方面面。翔云为广大有需求的用户提供了人脸识别接口解决方案,助力各行各业快速实现人脸比对功能。人脸识别接口基于深......
  • 左值和右值的概览及其使用方法
    左值和右值的概念及其使用方法前言一、什么是左值?什么是右值?二、左值引用和右值引用1.左值引用2.右值引用三、常量引用总结前言本文章详细讲解左值和右值的概念和使用方法,即展示了它的应用场景:移动语义和完美转发一、什么是左值?什么是右值?在C++中,所有的值不是......
  • 09C++选择结构(3)
    一、求3个整数中最小值题目:输入三个整数,表示梨的重量,输出最小的数。方法1:经过三次两两比较,得出最小值。a<=b&&a<=cmin=ab<=c&&b<=amin=bc<=b&&c<=amin=c流程图:#include<typeinfo>//变量类型头文件,还是有问题;无法判断int#include<iostream>//包含输......
  • 【C++】C++11引入的新特性(2)
    当你无法从一楼蹦到三楼时,不要忘记走楼梯。要记住伟大的成功往往不是一蹴而就的,必须学会分解你的目标,逐步实施。......