C++11 右值引用和移动语义

目录

1.左值引用和右值引用

2.右值引用使用场景(移动语义)和意义

3.右值引用引用左值及其一些更深入的使用场景分析

4.完美转发 


1.左值引用和右值引用

传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。

什么是左值?什么是左值引用?

左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
 

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);
	// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
	10 = 1;
	x + y = 1;
	fmin(x, y) = 1;
	return 0;
}

左值能否给右值取别名?---不可以,但是const左值引用可以。

​
	const int& rr1l = 10;
	const double& rr2l= x + y;
	const double& rr3l = fmin(x, y); 

​

右值引用能否给左值取别名---不可以,但是可以给move以后的左值取别名。

	int* p = new int(0);
	int*&& rp = move(p);


2.右值引用使用场景(移动语义)和意义

前面我们可以看到左值引用既可以引用左值和又可以引用右值,那为什么C++11还要提出右值引用呢?是不是化蛇添足呢?下面我们来看看左值引用的短板,右值引用是如何补齐这个短板的!

左值引用的使用场景:做参数和做返回值都可以提高效率。
 

左值引用的短板:
但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。例如:mystring::string to_string(int value)函数中可以看到,这里只能使用传值返回,传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。

namespace mystring 
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			cout << "string(char* str) -- 构造" << endl;

			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		// s1.swap(s2)
		void swap(string& s)
		{
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}

		// 拷贝构造
		// 左值
		string(const string& s)
			:_str(nullptr)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;

			_str = new char[s._capacity + 1];
			strcpy(_str, s._str);
			_size = s._size;
			_capacity = s._capacity;
		}


		// 赋值重载
		string& operator=(const string& s)
		{
			cout << "string& operator=(string s) -- 深拷贝" << endl;
			char* tmp = new char[s._capacity + 1];
			strcpy(tmp, s._str);

			delete[] _str;
			_str = tmp;
			_size = s._size;
			_capacity = s._capacity;

			return *this;
		}

		~string()
		{
			delete[] _str;
			_str = nullptr;
		}

		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}

		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				strcpy(tmp, _str);
				delete[] _str;
				_str = tmp;

				_capacity = n;
			}
		}

		void push_back(char ch)
		{
			if (_size >= _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);
			}

			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';
		}

		//string operator+=(char ch)
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}

		const char* c_str() const
		{
			return _str;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity; // 不包含最后做标识的\0
	};

	mystring::string to_string(int value)
	{
		bool flag = true;
		if (value < 0)
		{
			flag = false;
			value = 0 - value;
		}

		mystring::string str;
		while (value > 0)
		{
			int x = value % 10;
			value /= 10;
			str += ('0' + x);
		}
		if (flag == false)
		{
			str += '-';
		}

		std::reverse(str.begin(), str.end());

		return str;
	}
}

        只能使用传值返回,传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。

int main()
{
	mystring::string ret1 = mystring::to_string(1234);
	cout << ret1.c_str() << endl;

	return 0;
}

两次拷贝构造,效率低。

右值引用和移动语义解决上述问题:
在mystring::string中增加移动构造,移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。

下面的就是移动构造,与拷贝构造构成函数重载。

		// 移动构造
		// 右值(将亡值)
		string(string&& s)
			:_str(nullptr)
		{
			cout << "string(string&& s) -- 移动构造" << endl;
			swap(s);
		}

        to_string 的返回值是一个临时对象(将亡值)也就是右值,用这个右值构造ret2,如果既有拷贝构造又有移动构造,调用就会匹配调用移动构造,因为编译器会选择最匹配的参数调用。那么这里就是一个移动语义。

       在编译器优化的情况下,to_string函数的返回值,会被编译器强制move变成右值,同样的调用移动构造,两次移动构造,编译器会再次优化,合成一次移动构造,直接使用str构造ret1,这里就不存在拷贝了,直接将右值的资源转移过来,提高了效率。

不仅仅有移动构造,还有移动赋值:
在mystring::string类中增加移动赋值函数,再去调用mystring::to_string(1234),不过这次是将mystring::to_string(1234)返回的右值对象赋值给ret1对象,这时调用的是移动构造。

		string& operator=(string&& s)
		{
			cout << "string(string&& s) -- 移动赋值" << endl;
			swap(s);

			return *this;
		}
        int main()
        {
            mystring::string ret1;
            ret1 = bit::to_string(1234);
            return 0;
        }
// 运行结果:
// string(string&& s) -- 移动语义
// string& operator=(string&& s) -- 移动语义

这里运行后,我们看到调用了一次移动构造和一次移动赋值。因为如果是用一个已经存在的对象接收,编译器就没办法优化了。mystring::to_string函数中会先用str生成构造生成一个临时对象,但是我们可以看到,编译器很聪明的在这里把str识别成了右值,调用了移动构造。然后在把这个临时对象做为mystring::to_string函数调用的返回值赋值给ret1,这里调用的移动赋值。


        我们在这里思考一个问题,在移动构造中,s是右值,我们在调用swap的时候,还是可以获取s的资源,但右值是不用可以被修改的?

        实际上,右值引用的属性本身是左值:一旦一个右值被绑定到一个右值引用上,这个右值引用本身就是一个左值,因为它有一个明确的存储位置。这样的意义,是为了移动构造移动赋值,转移资源的语法逻辑自洽。只有右值引用本身处理成左值,才能实现移动构造和移动赋值,转移资源。

3.右值引用引用左值及其一些更深入的使用场景分析

C++11引入了右值引用和移动语义,以支持更加高效的资源管理和对象操作。

右值引用

右值引用是C++11引入的一种新引用类型,其语法形式为T&&,其中T是某个类型。右值引用主要用于绑定到右值(如临时对象、字面量等),从而允许程序员对这些右值进行直接操作,避免不必要的拷贝。

移动语义

移动语义允许一个对象将其资源“移动”到另一个对象,而不是进行传统的拷贝。这样做的好处是避免了不必要的资源分配和析构,从而提高了程序的性能。为了实现移动语义,C++11中引入了std::move函数模板和移动构造函数以及移动赋值运算符。

int main()
{
    string s1("111111111111");
    string s3 = move(s1);
}

资源转移,而非简单的拷贝。

std::move

std::move是一个函数模板,它将其参数转换为右值引用。但是,std::move并不真正移动任何数据,它只是简单地执行一个类型转换。实际上,数据的移动(如果发生)是由移动构造函数或移动赋值运算符来完成的。

        按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::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);
}
int main()
{
	mystring::string s1("hello world");
	// 这里s1是左值,调用的是拷贝构造
	mystring::string s2(s1);
	// 这里我们把s1 move处理以后, 会被当成右值,调用移动构造
	// 但是这里要注意,一般是不要这样用的,因为我们会发现s1的
	// 资源被转移给了s3,s1被置空了。
	mystring::string s3(std::move(s1));
	return 0;
}

STL容器插入接口函数也增加了右值引用版本:

void push_back(value_type&& val);
int main()
{
	list<mystring::string> lt;
	mystring::string s1("1111");
	// 这里调用的是拷贝构造
	lt.push_back(s1);
	// 下面调用都是移动构造
	lt.push_back("2222");
	lt.push_back(std::move(s1));
	return 0;
}
运行结果:
// string(const string& s) -- 深拷贝
// string(string&& s) -- 移动语义
// string(string&& s) -- 移动语义

        上面所作的效率提升,针对的是自定义类型的深拷贝的类,深拷贝的类才有转移资源的移动系列的函数。像日期类(浅拷贝自定义类型),内置类型不存在资源转移一说


4.完美转发 

        模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值.模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发

eg:接收的类型都退化成了左值

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()
{
	PerfectForward(10); // 右值
	int a;
	PerfectForward(a); // 左值
	PerfectForward(std::move(a)); // 右值
	const int b = 8;
	PerfectForward(b); // const 左值
	PerfectForward(std::move(b)); // const 右值
	return 0;
}

eg:使用std::forward 完美转发在传参的过程中保留对象原生类型属性

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; }
// std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
template<typename T>
void PerfectForward(T&& t)
{
	Fun(std::forward<T>(t));
}
int main()
{
	PerfectForward(10); // 右值
	int a;
	PerfectForward(a); // 左值
	PerfectForward(std::move(a)); // 右值
	const int b = 8;
	PerfectForward(b); // const 左值
	PerfectForward(std::move(b)); // const 右值
	return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/730352.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

云计算考试题

Cloud ❀ 云计算-虚拟化常见的两种架构_裸金属架构和宿主型架构的区别-CSDN博客 为啥要成2 11 bcd 16 acd abcd BCD NAS为啥支持文件存储的协议 选BCD 什么是网络文件系统 选bcd 错题 选abc 选bcd 选 abd

【ARMv8/v9 GIC 系列 4.2 -- GIC CPU Interface 详细介绍】

文章目录 GIC CPU Interface 介绍CPU Interface 主要寄存器 GIC CPU Interface 介绍 A 系列处理器提供 5个管脚来实现中断&#xff0c;分别是&#xff1a; nIRQ&#xff1a;物理普通中断nFIQ&#xff1a;物理快速中断nVIRQ&#xff1a;虚拟普通中断nVFIQ&#xff1a;虚拟快速…

运算放大器(运放)积分器电路

积分器电路 运算放大器(运放)积分器电路是在图2运放反相放大器的电路上增加一个积分电容构成&#xff0c;该积分电容并联在运算放大器的反馈电阻上&#xff0c;见图1。 运算放大器(运放)反相放大器电路 设计目标 输入fMin输入f0dB输入fMax输出VoMin输出VoMax电源Vcc电源Vee1…

【JS重点19】this指向问题总结

目录 一&#xff1a;普通函数this指向 普通函数在严格模式下&#xff1a; 二&#xff1a;箭头函数this指向 this指向说明 不适用this情况 三&#xff1a;改变this指向 1 call() 语法格式&#xff1a; 作用&#xff1a; 2 apply() 语法格式&#xff1a; 作用&#x…

C#.net6.0语言+B/S架构+前后端分离 手术麻醉信息管理系统源码

C#.net6.0语言&#xff0b;B/S架构前后端分离 手术麻醉信息管理系统源码 什么是手术麻醉信息管理系统 满足医院等级评级需求 满足电子病历评级需求 满足科室需求 术前 1、患者术前评估/诊断 2、术前讨论制定手术方案 3、手术准备 4、术前准备 术中 1、送手术室 2、麻…

oracle12c到19c adg搭建(五)dg搭建后进行切换19c进行数据字典升级

一、备库切主库升级 12c切换为19c主库的时候是由低版本到高版本所以cdb和pdb的数据字典需要进行升级才可以让数据与软件版本兼容。 1.1切换 SQL> alter database recover managed standby database finish; Database altered. SQL> alter database commit to switcho…

基于 NXP LS1046 +FPGA系列 CPCI 架构轨道交通专用板卡

基于 NXP LS1046 系列 CPCI 架构轨道板卡 该产品是一款 CPCI 无风扇架构的高可靠性板卡&#xff0c;CPU 选用 NXP LS1046A 系统平台&#xff0c;支持嵌入式 Linux 或者标准 Ubuntu Linux 、凝思等操作系统&#xff0c;轨道交通 EMC 及宽温级别设计&#xff0c;板载多路 M12 高速…

SQLite扩展插件终极集合

作为一个嵌入式数据库引擎&#xff0c;SQLite 与其他数据库管理系统相比&#xff0c;缺少了一些功能。不过 SQLite 提供了一个扩展机制&#xff0c;因此我们可以在网络上找到大量的 SQLite 插件。 今天我们介绍的这个插件叫做 sqlean&#xff0c;它打包了许多流行的 SQLite 扩…

【windows】字体安装手册

windows字体安装手册 1 下载字体文件 百度搜索XXX字体ttf文件进行下载 附&#xff1a;宋体gb2312下载地址&#xff1a; https://www.downza.cn/soft/7780.html 2 字体安装 1.搜索字体 2.将下载的ttf文件拖拽添加 3.关闭办公软件重新打开后&#xff0c;outlook、word、…

装备制造业CRM解决方案

01、数字化转型驱动企业&#xff0c;向“以客户需求驱动创新生产”的智能制造业转变 我国装备制造业经过多年的发展&#xff0c;取得了令人瞩目的成就&#xff0c;形成了门类齐全、具有相当规模和一定水平的产业体系&#xff1b;主要包含通用设备、专用设备、电气机械、交通运…

GPT 模型简史:从 GPT-1 到 GPT-4

文章目录 GPT-1GPT-2GPT-3从 GPT-3 到 InstructGPTGPT-3.5、Codex 和 ChatGPTGPT-4 GPT-1 2018 年年中&#xff0c;就在 Transformer 架构诞生⼀年后&#xff0c;OpenAI 发表了⼀篇题 为“Improving Language Understanding by Generative Pre-Training”的论文&#xff0c;作者…

DS知识点总结--线性表定义及顺序表示

数据结构知识点汇总(考研C版) 文章目录 数据结构知识点汇总(考研C版)二、线性表2.1 线性表的定义和操作2.1.1 线性表的定义2.1.2 线性表的基本操作 2.2 线性表的顺序表示2.2.1 顺序表的定义2.2.2 顺序表上的基本操作的实现 二、线性表 2.1 线性表的定义和操作 2.1.1 线性表的…

阿里云如何实现express的自动化部署(保姆级教程)

本篇文章将详细介绍一下阿里云如何实现express的自动化部署&#xff0c;作者本人总结的保姆级教程&#xff01;&#xff01;&#xff01; 首先去阿里云官网 &#xff08;阿里云-计算&#xff0c;为了无法计算的价值) 搜索函数计算fc 如果没有开通过选择免费开通&#xff0c;…

RockChip Android12 Settings一级菜单

一:概述 在之前的文章中对Android8.1 Settings的流程进行了说明,本章将针对Android12 Settings一级菜单的加载逻辑进行详细说明,Settings版本之间的差异不是很大,有兴趣的同学可自行学习,本文不在做赘述。 Android8.1 Settings说明:RockChip Android8.1 Settings-CSDN博…

在win10 上使用ssh连接到树莓派上

在win10 上使用ssh连接到树莓派上 树莓派上的设置 启用ssh 启用VCN和SSH&#xff0c;这样可以使用VNC和SSH远程。 win10 上的设置 安装ssh客户端 按下win键输入"应用和功能" 如果没有安装就搜索&#xff1a;OpenSSH客户端&#xff0c;安装。 连接到树莓派…

STM32---SPI通信协议(小白入、含源码)

写在前面&#xff1a;在单片机的学习过程中&#xff0c;各种通信协议的学习是必不可少的&#xff0c;在前面我们学习了串口通信、IIC通信&#xff0c;本节我们来认识一下SPI通信协议。包括其SPI基本概念、NORFLASH芯片的介绍以及相关的例程实验。 目录 一、SPI介绍 1.1什么是…

socket--IP端口爆破域名解析

免责声明:本文仅做技术交流与学习... 目录 IP端口爆破 域名解析爆破 IP端口爆破 #端口扫描: #获取扫描的 IP和端口 #连接 IP和端口&#xff08;socket&#xff09; #判断连接状态-开放和关闭# import socket # # 加入参数模式 # import os # ssocket.socket() # s.connect((…

3.1、前端异步编程(超详细手写实现Promise;实现all、race、allSettled、any;async/await的使用)

前端异步编程规范 Promise介绍手写Promise&#xff08;resolve&#xff0c;reject&#xff09;手写Promise&#xff08;then&#xff09;Promise相关 API实现allraceallSettledany async/await和Promise的关系async/await的使用 Promise介绍 Promise是一个类&#xff0c;可以翻…

图像分割(三)-RGB转HSV后图像分割方法

常用彩色模型有RGB和HSV模型&#xff0c;有时候在RGB颜色空间进行背景分割比较困难的问题&#xff0c;转换为HSV模型然后对色调和饱和度图像进行处理会得到比较理想的处理结果,下面通过一个实例讲解该方法的MATLAB实现&#xff0c;该方法对其他图像检测也具有一定的参考价值。 …

python19 异常处理

python19 异常处理 代码 异常处理 result 0; try:num1 int(input(请输入一个整数:))num2 int(input(请输入一个整数:))result num1 / num2 except ZeroDivisionError:print(除数不能为0) except ValueError:print(不能将字符串转成整数) except BaseException:print(未知异…