C++11新特性——右值引用与移动语义

news2024/11/15 10:26:25

左值引用和右值引用

        在C++中,左值是一个表示数据的表达式,我们可以获取它的地址,一般可以对它赋值,通常可以出现在左边或右边,左值引用就是对左值的引用,相当于给左值起了一个别名。

例子:

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

左、右值引用的区别

关于左值引用的总结

        1. 左值引用可以绑定左值,但不能绑定右值。

        2. const左值引用既可以绑定左值又可以绑定右值。

例子:

    int a = 10;
    int& ra1 = a; // 左值引用
    //int& rn = 10; ---编译器报错,因为左值引用不能绑定右值
    
    const int& rn = 10; // const左值引用可以绑定右值
    const int& ra2 = a; // const左值引用当然也可以绑定左值 

关于右值引用的总结

        1. 右值引用可以绑定右值,但不能绑定左值。

        2. 右值引用可以绑定左值调用move后的返回结果。

例子:

    int&& rn = 10; // 右值引用当然可以绑定右值
    int a;
    //int&& ra1 = a; ---编译器报错,因为右值引用不能绑定左值

    int&& ra2 = std::move(a); // 右值引用可以绑定左值调用move后的返回结果
    // 请注意,左值在调用move后,它本身依然是左值,还是不能被右值引用绑定

右值引用的使用场景

        在前面的学习中,我们对左值引用和右值引用有了最基本的了解,可能有同学会好奇了,左值引用好像就能绑定左值和右值了,为什么c++11还要引入右值引用这个技术呢?所以接下来让我们看看左值引用的短板,以及右值引用是如何弥补这一短板的吧!

        首先,回忆一下我们以前学习过的左值引用的使用场景,因为左值引用相当于给变量起了别名,所以当我们传递参数或者返回函数的结果时,可以用左值引用来减少不必要的复制。但是,单就这两种场景,左值引用就真的能够完全胜任吗?

        我们知道,函数中定义的局部变量出了函数作用域就会被释放,那么如果我们使用左值引用返回这个局部变量,就会出现悬空引用的问题,于是在没有右值引用之前,我们不得不直接返回值,所以左值引用不能对这种情况进行优化。

        而右值引用正是为了处理这一场景而出现的,C++中,我们把右值分为纯右值和将亡值,如下图这个例子中,变量ret即将被销毁,就属于将亡值。刚刚一门一直在说不必要的复制,那具体是复制什么呢?

        我们知道,ret作为一个字符串类型的变量,我们需要为它开辟一块内存空间,然而这个将亡值在出了函数作用域之后就被销毁了,要怎么把返回值传递给s呢?其实这涉及到一个小知识点:当函数返回一个局部变量时,编译器会创建一个临时变量来持有返回值。

        所以说,这个例子其实是这样:编译器创建临时变量时调用拷贝构造进行一次深拷贝,开辟了一块内存空间把字符串存了进去,然后ret这个局部变量被销毁时,自己的内存空间也被释放;临时变量赋值给s时调用赋值重载再进行一次深拷贝,有开辟了一块内存空间,把字符串存了进去,然后临时变量的内存空间也被释放。

        不难发现,这个过程中,我们进行了两次完全没有必要的深拷贝,而且释放这些空间也是有一定消耗的。机智如你肯定已经发现了端倪,既然这个ret和临时变量本来就快被销毁了,那为啥还要专门给它们开辟内存呢?直接这样,把ret的内存空间转移给临时变量,再把临时变量的内存空间转移给s不就完了吗?是的!其实两步就是传说中的移动构造和移动赋值重载。

        那这和右值又有什么关系呢?这是因为转移内存空间这种事还是有点危险的,如果所有类型的变量都能这样做不就乱套了吗!所以移动语义只能通过右值引用来完成,因为应用场景本来就也只是对将亡值进行资源转移嘛。

        

        

        因为移动语义能够弥补左值引用不能返回局部变量的短板,大量减少不必要的深拷贝和释放空间,所以stl容器基本都引入了移动构造和移动赋值重载:

移动语义的简单实现

#define _CRT_SECURE_NO_WARNINGS

#include <iostream>
#include <cstring>
#include <cassert>
#include <algorithm>

using namespace std;

namespace MySTL
{
	// 我们为了方便测试移动语义自己写的string类
	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)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;

			string tmp(s._str);
			swap(tmp);
		}

		// 移动构造
		string(string&& s)
		{
			cout << "string(string&& s) -- 移动拷贝" << endl;

			swap(s);
		}

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

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

			return *this;
		}

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

			swap(s);
			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 = nullptr;
		size_t _size = 0;
		size_t _capacity = 0; // 不包含最后做标识的\0
	};

	MySTL::string to_string(int x)
	{
		MySTL::string ret;
		while (x)
		{
			int val = x % 10;
			x /= 10;
			ret += ('0' + val);
		}

		reverse(ret.begin(), ret.end());

		return ret;
	}
}

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

        

完美转发

模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值,模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力

请看下面的代码:

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



第一层per函数的参数既能接受左值,也能接受右值,但是假如你把代码复制后测试,会发现在参数传递到第二层函数时,它全部变成的左值,这是因为模板中的万能引用会将右值退化成左值,所以后续
使用过程它就变成了左值!

使用forward可以保留对象的原生类型

void PerfectForward(T&& t)
{
    Fun(std::forward<T>(t));
}


 

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

macOS安装Java和Maven

安装Java Java Downloads | Oracle 官网下载默认说最新的Java22版本&#xff0c;注意这里我们要下载的是Java8&#xff0c;对应的JDK1.8 需要登陆Oracle&#xff0c;没有账号的可以百度下。账号:908344069qq.com 密码:Java_2024 Java8 jdk1.8配置环境变量 open -e ~/.bash_p…

Autosar--Can收发器通俗讲解

Autosar网络管理的休眠唤醒功能,Can收发器起到比较大的作用,充当唤醒源以及最终执行网络休眠。 Autosar--Can收发器通俗讲解 唤醒/休眠请求 进入normol ComM_EcuM_WakeUpIndication 有唤醒源事件 CanSM_RequestComMode 有通信请求 CanSM_NetworkStatemachine() 更新CANS…

【C++】list(下)

个人主页~ list&#xff08;上&#xff09;~ list 四、模拟实现1、list.h&#xff08;1&#xff09;关于整个list的搭建①节点②迭代器③接口 &#xff08;2&#xff09;自定义类型实例化 2、test.cpp&#xff08;1&#xff09;test1&#xff08;2&#xff09;test2 五、额外小…

大模型入门 ch02:数据集准备

本文是github上的大模型教程LLMs-from-scratch的学习笔记&#xff0c;教程地址&#xff1a;教程链接 Chapter 2&#xff1a; Working with Text 这一章节包括了数据的准备和采样阶段。 1. Tokenizer 大模型通过将token转变为embedding&#xff08;词嵌入&#xff09;运作。 首…

【机器学习】高斯网络的基本概念和应用领域以及在python中的实例

引言 高斯网络&#xff08;Gaussian Network&#xff09;通常指的是一个概率图模型&#xff0c;其中所有的随机变量&#xff08;或节点&#xff09;都遵循高斯分布 文章目录 引言一、高斯网络&#xff08;Gaussian Network&#xff09;1.1 高斯过程&#xff08;Gaussian Proces…

Android14音频进阶之高通Elite架构指定通道播放(八十四)

简介: CSDN博客专家、《Android系统多媒体进阶实战》一书作者 新书发布:《Android系统多媒体进阶实战》🚀 优质专栏: Audio工程师进阶系列【原创干货持续更新中……】🚀 优质专栏: 多媒体系统工程师系列【原创干货持续更新中……】🚀 优质视频课程:AAOS车载系统+…

【Linux】多线程:线程概念,线程与进程的区别与联系,多线程相较于多进程的优势

目录 一、进程基本属性回顾 二、线程概念 三、操作系统为什么要引入线程—多进程和多线程的区别 为什么多线程比多线程调度效率更快&#xff1f; 四、线程的优点 五、线程的缺点 六、线程异常 一、进程基本属性回顾 在学习线程之前&#xff0c;我们先来回顾一下进程的基…

6. LinkedList与链表

一、ArrayList的缺陷 通过源码知道&#xff0c;ArrayList底层使用数组来存储元素&#xff0c;由于其底层是一段连续空间&#xff0c;当在ArrayList任意位置插入或者删除元素时&#xff0c;就需要将后序元素整体往前或者往后搬移&#xff0c;时间复杂度为O(n)&#xff0c;效率比…

64、Python之函数高级:装饰器实战,动态语言也能有类型检查

引言 Python作为一门动态类型语言&#xff0c;有时候&#xff0c;一个不小心的类型错误只有在实际运行中才有可能被发现。相较而言&#xff0c;静态类型语音虽然不够灵活&#xff0c;但是&#xff0c;类型错误等语法检查在编译期间就可以提前发现了。 那么我们有没有方法在Py…

Visual Studio Code 月刊 (2024-08)

文章目录 配置文件编辑器Django 单元测试支持vscode.dev 上的 IntelliSenseNotebook 差异查看器通过键盘调整列的大小源代码管理图GitHub Copilot结语 2024 年 8 月 Visual Studio Code&#xff08;简称 vscode&#xff09;发布了 version 1.93。该版本带来了许多更新&#xff…

Spring Boot Admin集成与自定义监控告警

目录 一.Spring Boot Admin集成 1.引入依赖 2.添加配置 3.监控界面 二.Spring Boot Admin告警机制 1. 基本告警机制 2. 配置告警 2.1 triggers触发器讲解 3. 自定义通知 3.1 Instance 对象 三.Spring Boot Admin支持的监控属性 1.常见的Spring Boot Admin监控属性 …

进阶SpringBoot之配置 Swagger API 框架信息

Swagger&#xff1a;API 框架 RestFul API 文档在线自动生成工具&#xff0c;API 文档与 API 定义同步更新 Swagger 官网 Maven 仓库 创建 Spring Boot 项目&#xff0c;依赖勾选 Spring Web pom.xml 导入相关依赖&#xff1a; springfox-swagger2、springfox-swagger-ui …

何为数据中台

数据中台 什么是数据中台 2014年马云正式提出“DT&#xff08;Data Technology&#xff09;”的概念&#xff0c;人类从IT时代走向了DT时代&#xff0c;阿里内部的数据平台事业部大刀阔斧的建立整个集团的数据资产&#xff0c;同年&#xff0c;阿里从芬兰Supercell公司接触到…

Canny算子 一张图看懂

对于最高值和最低值的设置&#xff0c; 1&#xff0c;high t最大值一般以一阶导数幅度图的最大值的30%-40%来定 2&#xff0c;最小值一般halcon里默认为 low theigh t/3得到 3&#xff0c;canny的优势是有极大值抑制&#xff0c;所以提取的边缘是1个像素的窄边缘。 3&#xff0…

Golang path/filepath包详解:高效路径操作与实战案例

Golang path/filepath包详解&#xff1a;高效路径操作与实战案例 引言基础用法Abs 函数Base 函数Clean 函数Dir 函数Ext 函数FromSlash 和 ToSlash 函数 基础用法Abs 函数Base 函数Clean 函数Dir 函数Ext 函数FromSlash 和 ToSlash 函数 路径操作Join 函数Split 函数Rel 函数Ma…

LabVIEW编程语言出于什么原因开发的?

LabVIEW最初由美国国家仪器公司&#xff08;NI&#xff09;于1986年开发&#xff0c;目的是为工程师和科学家提供一种图形化编程环境&#xff0c;简化数据采集、仪器控制、自动化测试和测量系统开发等工作。开发LabVIEW的主要原因包括以下几点&#xff1a; 简化复杂系统开发&am…

《数字图像处理(面向新工科的电工电子信息基础课程系列教材)》Chapter 1课件2024

每一轮备课都有新的感悟。 禹晶、肖创柏、廖庆敏《数字图像处理&#xff08;面向新工科的电工电子信息基础课程系列教材&#xff09;》 禹晶、肖创柏、廖庆敏《数字图像处理》资源二维码

Tektronix泰克MSO5204B混合信号示波器4+16通道2G

Tektronix泰克MSO5204B混合信号示波器416通道2G 2 GHz、416 通道、10/5 GS/s&#xff08;2/4 通道&#xff09;混合信号示波器&#xff0c;50 M/25 M 记录长度泰克 MSO5204B 2 GHz、416 通道、10/5 GS/s&#xff08;2/4 通道&#xff09;混合信号示波器&#xff0c;50 M/25 M …

机器学习强化学习

版权声明 本文原创作者:谷哥的小弟作者博客地址:http://blog.csdn.net/lfdfhl1. 强化学习概述 1.1 定义与核心概念 强化学习是一种目标导向的机器学习方法,它使智能体能够在环境中通过试错学习最优行为策略。这种学习过程涉及到智能体与环境之间的交互,智能体根据当前状态…

AI基础 L8 Local Search I 局部搜索

Iterative Improvement Algorithms • In many optimization problems, the path to a goal is irrelevant — the goal state itself is the solution • State space a set of goal states — find one that satisfies constraints (e.g., no two classes at same time) —…