C++:右值引用

右值与左值

在讲解右值引用之前,我们就需要先辨析一下左值右值的区别。

左值

左值是一个表示数据的表达式,我们可以获取它的地址并且对其赋值,左值可以出现在赋值操作符=的左边,但是右值不能。

int i = 0;
int* p = &i;
double d = 3.14;

变量ipd都是左值,一方面来说,它们出现在了=的左边,另一方面来说,我们可以对其取地址,并修改它的值。

const int ci = 0;
int const* cp = &i;
const double cd = 3.14;

变量cicpcd都是左值,它们出现在了=的左边,我们可以对其取地址。但是由于具有const属性,我们不能修改它。 

因此, 左值最显著的特征是可以取地址,但是不一定可以被修改。

 

右值

右值也是一个表达数据的表达式,比如字面常量表达式返回值函数返回值等等,右值可以出现在赋值操作符=的右边,但是不能出现在=的左边,右值不能取地址。

double func()
{return 3.14;
}int x = 10;
int y = 20;
int z = x + y;
double d = func();

以上代码中,1020x + yfunc()都是右值,它们出现在=的右边。1020对应了字面常量x + y对应了表达式返回值func()对应函数返回值。这些都是右值,它们最显著的特点就是无法取地址。


右值引用语法

先回顾一下左值引用的语法:

int i = 0;
int* p = nullptr;int& ri = i;
int*& rp = p;

左值引用的语法是:type&;右值引用的语法是:type&&

接下来我们尝试对刚刚的值进行右值引用:

int&& ri = 0;
int*&& rp = nullptr;
double&& rd = 3.14;
  1. 左值引用不能直接引用右值
  2. const左值引用 可以引用右值

用左值引用直接引用右值:

int& i = 5; // 非法

const引用的形式,那么就可以引用右值:

const int& i = 5; // 合法
  1. 右值引用不能直接引用左值
  2. 右值引用可以引用move后的左值

右值引用不能直接引用左值:

int i = 5;
int&& rri = i; // 非法

函数move,其可以把一个左值强制转化为一个右值。

int i = 5;
int&& rri = move(i); // 合法

move(i)之后,i依然是左值,但是move(i)这个表达式返回了一个右值的i。 

 


右值引用底层

既然存在右值引用这个语法,那么我们来看看右值引用到底干了些啥。

右值引用的工作主要有两种情况,一种是右值引用了常量,另外一种是右值引用了move后的左值。

右值引用了常量

当右值引用了常量,引用会把常量区中的数据拷贝一份到栈区,然后该引用指向栈区中拷贝后的数据

看到一段代码:

int&& r = 5;
r = 10;

以上过程中,我们先用r右值引用了常量5,然后通过右值引用把5改为了10。

如果这个过程中,右值常量5存储在常量区,r右值引用后如果r指向常量区的5,会发生什么?此时我们的r = 10操作,就相当于把常量区的5修改为了10,从此以后整个程序中只要去常量区拷贝5都会变成拷贝10,这可就完蛋了。因此我们的右值引用常量,绝对不能直接引用常量区的数据!!

因此,右值引用常量时的真实操作是把常量区的数据拷贝到栈区中,然后这个引用指向这一块栈区内存。

  1. 当右值引用了常量,引用会把常量区中的数据拷贝一份到栈区,然后该引用指向栈区中拷贝后的数据,该数据可以修改
  2. const左值引用了常量,引用会把常量区中的数据拷贝一份到栈区,然后该引用指向栈区中拷贝后的数据,但是该数据是常量,不能修改

 

右值引用了move后的左值

  1. 当右值引用了move后的左值,右值引用直接指向该左值

看到以下代码:

int i = 5;
int&& rri = move(i);rri = 10;cout << i << endl;
cout << rri << endl;

程序输出结果为:

10
10

我们可以通过修改右值引用来修改左值,或者说以通俗点的说法,此时右值引用就是这个左值的别名。确实是这样的,当右值引用了move后的左值,其实和直接左值引用这个左值没有任何区别。 

  1. 左值引用解决了传参时存在的拷贝问题
string add_string(string& s1, string& s2)
{string s = s1 + s2;return s;
}int main()
{string str;string hello = "Hello";string world = "world";str = add_string(hello, world);return 0;
}

以上代码中,add_string函数需要接收两个string类型的参数,此时我们使用传引用传参,就可以避免两个string的拷贝消耗。

2.左值引用解决了一部分返回值的拷贝问题

string& say_hello()
{static string s = "hello world";return s;
}int main()
{string str1;string str2;str1 = say_hello();return 0;
}

以上代码中,函数say_hello生成了一个string,并把它返回给外部,如果我们直接返回,那么str1接收参数时,就会先拷贝构造出一个临时变量,然后临时变量再拷贝构造str1。这个过程发生了两次拷贝构造。但是返回值s指向的string是全局的,其出了函数依然存在,因此我们传引用返回,可以不用拷贝构造一个临时变量,直接拿返回值s去拷贝构造,节省了一次拷贝构造。

也就是说,左值引用通过传引用传参传引用返回节省了拷贝。

右值引用,其实更多的是一种标记。

先来看看什么情况下会产生可以被右值引用的左值:

  1. 当一个左值被move后,可以被右值引用
  2. C++会把即将离开作用域的非引用类型的返回值当成右值,这种类型的右值也称为将亡值 

右值的意思就是:这个变量的资源可以被迁移走 


移动语义

为了讲解移动语义,先写一个简单的mystring类:

class mystring
{
public://构造函数mystring(const char* str = ""){_str = new char[strlen(str) + 1];strcpy(_str, str);}//析构函数~mystring(){delete[] _str;}// 赋值重载mystring& operator=(const mystring& s){cout << "赋值重载" << endl;return *this;}// 拷贝构造mystring(const mystring& s){cout << "拷贝构造" << endl;}private:char* _str = nullptr;
};

     

mystring get_string()
{mystring str("hello");return str;
}int main()
{mystring s2 = get_string();return 0;
}

s2通过函数get_string来获得字符串,并构造自己。这个过程中,由于str是局部变量,会发生拷贝构造临时变量,临时变量再拷贝构造s2的过程。

但是由于str是一个将亡值,具有右值属性,我们可以写一个函数直接把它的资源转移走: 

class mystring
{
public:// 移动构造mystring(mystring&& s){cout << "移动构造" << endl;std::swap(_str, s._str);}
};

函数主体部分,通过一个swap函数把参数s_str指针成员与自己的_str成员进行交换。由于指针指向字符串数组,此时相当于把s的字符串数组交换给自己,这样就完成了对右值引用的数据转移。 

 除了移动构造,我们还有原先的拷贝构造:

class mystring
{
public:// 移动构造mystring(mystring&& s){cout << "移动构造" << endl;std::swap(_str, s._str);}// 拷贝构造mystring(const mystring& s){cout << "拷贝构造" << endl;}
};

虽然说我们的左值引用,也可以达到这样的移动构造,但是有一个问题,并不是所有的对象,资源都是可以被转移走的。移动构造之所以这么叫,就是因为移走了别人的资源。这部分资源之所以会被移走,就是因为它有右值属性。而它之所以有右值属性,要么就是这个变量是个将亡值,资源不转移就浪费了;要么就是被程序员亲自move了,程序员许可把这个对象的资源转移走。

就是这样的一个逻辑闭环,右值引用以一个既安全,又高效的方式,完成了局部变量的资源拷贝问题。而这个过程,也叫做右值引用的移动语义。

移动:改语法实现了通过移走别人的资源,实现高效的创建对象,避免大量拷贝
语义:在这个过程中,右值引用只提供语义层面的功能,即许可一个对象资源被转移的右值语义

因为右值引用的出现,C++11后,类的默认成员函数从6个变成了8个。新增两个成员函数:移动构造移动赋值重载

//移动赋值重载
mystring& operator=(mystring&& s)
{std::swap(_str, s._str);return *this;
}// 移动构造
mystring(mystring&& s)
{std::swap(_str, s._str);
}

它们的特点是:参数为右值引用,函数体内部通过交换别人的指针到自己手上,实现高效的资源转移。


引用折叠

看到以下代码:

template <class T>
void func(T&& t)
{cout << "T&& 右值引用" << endl;
}template <class T>
void func(const T& t)
{cout << "const T& const左值引用" << endl;
}int main()
{int a = 5;func(a);//左值func(move(a));//右值return 0;
}

程序输出结果如下:

T&& 右值引用
T&& 右值引用

C++在模板中推出了引用折叠,也叫做万能引用,规则如下:

T& && 推演为 T&
T&& && 推演为 T&&

如果你希望当参数为左值引用和右值引用的时候,函数的功能是一样的,你就可以只写一个函数:

template <class T>
void func(T&& t)
{
}

此时,参数T&&就已经是一个引用折叠了。现在我们来调用这个函数:

int a = 5;
func(a);
func(move(a));

第一次传参,func(a);,模板参数T的类型为int&,但是参数类型为int& &&,此时根据折叠引用规则:int& &&等于int&
第二次传参,func(move(a));,模板参数T的类型为int&&,但是参数类型为int&& &&,此时根据折叠引用规则:int&& &&等于int&&

我们刚才的模板,如果作用于int类型,就可以推演出四套函数重载:

void func(int&){};
void func(const int&){};
void func(int&&){};
void func(const int&){};

完美转发

看到以下代码:

void fuc1(int& rri)
{cout << "func1 左值引用" << endl;
}void fuc1(int&& rri)
{cout << "func1 右值引用" << endl;
}int main()
{int i = 5;int&& rri = move(i);fuc1(rri);return 0;
}

输出结果:

func1 左值引用

右值引用后,右值引用指向的对象是右值属性,但是引用本身是左值属性 

 再来看到一个案例:

void func2(int& x)
{cout << "func2 左值引用" << endl;
}void func2(int&& x)
{cout << "func2 右值引用" << endl;
}template <class T>
void fuc1(T&& t)
{func2(t);
}int main()
{int i = 5;fuc1(i);//左值fuc1(move(i));//右值return 0;
}

 由于在func1中,我们经过了折叠引用这一步,T&&这个参数类型是不确定的。

如果T&&是右值的话,传参后t会变成左值,那么我们可以对其进行move操作
如果T&&是左值的话,传参后t还是左值,我们无需对其进行操作

这个地方就不能粗暴的进行move了,不然会把原本就是左值的参数,给move成右值。为了解决这个情况,C++提供了一个函数模板forward,称为完美转发,其可以识别到参数的左右值类型,从而将其转化为原来的值。

我们只需要在引用折叠中这样进行调用:

template <class T>
void fuc1(T&& t)
{func2(forward<T>(t));
}

forward的模板参数中传入引用折叠的模板参数T,那么forward<T>就可以根据t的类型自动返回其原始的左右值属性了。 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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

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

相关文章

编程题-栈,链栈

概念 栈&#xff08;Stack&#xff09;&#xff1a;是只允许在一端进行插入或删除的线性表。首先栈是一种线性表&#xff0c;但限定这种线性表只能在某一端进行插入和删除操作。 栈顶&#xff08;Top&#xff09;&#xff1a;线性表允许进行插入删除的那一端。 栈底&#xff0…

4、linux相关基础知识

1、gcc编译过程 .c通过编译生成.o文件&#xff0c;.out目标文件进过链接生成.so库文件。 2、在C中可以使用system(("mkdir -p "path).c_str())创建目录。c_str()把string转化为c字符串&#xff0c;便于system命令识别&#xff0c;system命令会新启动一个进程来创建文…

移动硬盘有盘符打不开:深度解析与高效恢复指南

在数字化信息爆炸的今天&#xff0c;移动硬盘作为便捷的数据存储与传输工具&#xff0c;其重要性不言而喻。然而&#xff0c;当您遇到移动硬盘有盘符却无法正常打开的情况时&#xff0c;无疑会给您的工作和生活带来不小的困扰。本文将深入探讨移动硬盘有盘符打不开的原因&#…

东软“引战”国家队 通用技术“补链”大国重器

向来低调温和的东软创始人刘积仁&#xff0c;这一次抛出了“王炸”级的资产交易。 7月3日&#xff0c;《多肽链》获得一则足以引爆国内医疗设备行业的投资信息&#xff1a;被东软集团视为核心资产、掌上明珠的东软医疗&#xff0c;成功引入通用技术集团资本有限公司与中国国有…

BI佐罗,居然抄袭洗稿我的文章

必须曝光此博主不当行径。 4月2日这天发表的原创文章&#xff1a;BI报表系统建设10大坑&#xff0c;因为都是切身的实际项目经验总结&#xff0c;获得了很多人的关注。 我觉得写文章要写的是亲身、真的做过的专业的项目经验&#xff0c;而不是信口开河随口忽悠。 如果有些博…

Fancybox: 号称世界上最流行的灯箱脚本!这款“花盒“为什么与众不同?

今天要分享的是一个灯箱脚本库&#xff1a;Fancybox。最近了不起刚好用到它。这里就和大家分享下。 简介 Fancybox 是终极的 JavaScript 灯箱替代品&#xff0c;为多媒体显示中的优质用户体验设定了标准。支持图像、视频、地图、内联内容、iframe 和任何其他 HTML 内容。 此…

reverse函数讲解

reverse&#xff1a; 在C中&#xff0c;reverse 是一个标准库函数&#xff0c;用于反转指定范围内的元素顺序。该函数定义在头文件 <algorithm> 中。它的基本用法如下&#xff1a; #include <algorithm> // 包含reverse函数 #include <vector> // 使用v…

如何在SpringCloud中使用Kafka Streams实现实时数据处理

使用Kafka Streams在Spring Cloud中实现实时数据处理可以帮助我们构建可扩展、高性能的实时数据处理应用。Kafka Streams是一个基于Kafka的流处理库&#xff0c;它可以用来处理流式数据&#xff0c;进行流式计算和转换操作。 下面将介绍如何在Spring Cloud中使用Kafka Streams实…

Pytorch中nn.Sequential()函数创建网络的几种方法

1. 创作灵感 在创建大型网络的时候&#xff0c;如果使用nn.Sequential&#xff08;&#xff09;将几个有紧密联系的运算组成一个序列&#xff0c;可以使网络的结构更加清晰。 2.应用举例 为了记录nn.Sequential&#xff08;&#xff09;的用法&#xff0c;搭建以下测试网络&…

UDP怎么实现可靠传输

UDP它不属于连接型协议&#xff0c;因而具有资源消耗小&#xff0c;处理速度快的优点&#xff0c;所以通常音频、视频和普通数据在传送时使用UDP较多&#xff0c;因为它们即使偶尔丢失一两个数据包&#xff0c;也不会对接收结果产生太大影响。传输层无法保证数据的可靠传输&…

简谈设计模式之工厂模式

工厂模式是一种创建型设计模式, 旨在将对象的创建过程和使用过程分离. 这样可以在不改变客户端代码的前提下, 灵活创建不同类型的对象 工厂模式实现 简单工厂模式 简单工厂模式使用一个工厂类根据传入的参数决定创建哪种具体产品类的实例. 它不属于设计模式的23种, 但它为理…

数字电路-建立时间和保持时间详解

对于数字系统而言&#xff0c;建立时间&#xff08;setup time&#xff09;和保持时间&#xff08;hold time&#xff09;是数字电路时序的基础。数字电路系统的稳定性&#xff0c;基本取决于时序是否满足建立时间和保持时间。我自己在初学时一度很难理解清楚他们的概念&#x…

云端典藏:iCloud中个人收藏品目录的智能存储方案

云端典藏&#xff1a;iCloud中个人收藏品目录的智能存储方案 在数字化生活不断推进的今天&#xff0c;个人收藏品的管理也趋向于电子化和云端化。iCloud作为苹果公司提供的云服务&#xff0c;为个人收藏品目录的存储和管理提供了一个安全、便捷、跨设备的解决方案。本文将详细…

0145__Linux的capability

https://zhuanlan.zhihu.com/p/693896673 Linux的capability深入分析&#xff08;1&#xff09;_linux 设置进程capprm-CSDN博客 cap_init(3) - Linux manual page

【异常错误】Compile with `TORCH_USE_CUDA_DSA` to enable device-side assertions.

在运行项目的时候&#xff0c;出现了错误&#xff0c;但是却不知道为什么错误&#xff0c;因为GPU报的错误太抽象 ../aten/src/ATen/native/cuda/Indexing.cu:1239: indexSelectSmallIndex: block: [5,0,0], thread: [82,0,0] Assertion srcIndex < srcSelectDimSize failed…

基于JAVA+SpringBoot+Vue+uniApp小程序的心理健康测试平台

✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取项目下载方式&#x1f345; 一、项目背景介绍&#xff1a; 该系统由三个核心角色…

维度评分:Kylin Cube设计中的自定义智能

维度评分&#xff1a;Kylin Cube设计中的自定义智能 引言 Apache Kylin是一个高性能的分布式分析引擎&#xff0c;它通过构建数据立方体&#xff08;Cube&#xff09;来加速对大数据集的查询。在Kylin中&#xff0c;维度的自定义评分是一个高级特性&#xff0c;允许用户根据业…

【PVE】新增2.5G网卡作为主网卡暨iperf测速流程

【PVE】新增2.5G网卡作为主网卡暨iperf测速流程 新增网卡 新增网卡的首先当然需要关闭PVE母机&#xff0c;把新网卡插上&#xff0c;我用淘宝遥现金搞了个红包&#xff0c;花了26元买了块SSU的2.5G网卡。说实话这个价位连散热片都没有&#xff0c;确实挺丐的。稍后测下速度看…

移动硬盘有盘符打不开的全方位解决方案

一、现象描述&#xff1a;移动硬盘有盘符却无法访问 在日常的数据存储与传输中&#xff0c;移动硬盘无疑扮演着举足轻重的角色。然而&#xff0c;不少用户可能会遇到这样一个令人头疼的问题&#xff1a;移动硬盘在连接电脑后&#xff0c;虽然能正常显示盘符&#xff0c;但双击…

【算法】单调队列

一、什么是单调队列 单调队列是一种数据结构&#xff0c;其特点是队列中的元素始终保持单调递增或递减&#xff0c;主要用于维护队列中的最小值或最大值。 不同于普通队列只能从队头出队、队尾入队&#xff0c;单调队列为了维护其特征&#xff0c;还允许从队尾出队 不管怎么…