C++右值引用

文章目录

  • 1.左值概念和右值概念
    • 1.1.赋值目的
    • 1.2.左值右值
      • 1.2.1.左值
      • 1.2.2.右值
  • 2.左值引用和右值引用
    • 2.1.左值引用
    • 2.2.右值引用
      • 2.2.1.细化参数
      • 2.2.2.资源转移
        • 2.2.2.1.返回转移
        • 2.2.2.2.参数转移
  • 3.万能引用和完美转发
    • 3.1.万能引用
    • 3.2.完美转发
  • 4.关于const的补充
  • 5.类的新成员
    • 5.1.默认移动构造函数
    • 5.2.默认移动赋值函数

1.左值概念和右值概念

C 语言中有两个概念,一个是“左值”,另外一个是“右值”。

1.1.赋值目的

赋值表达式语句的目的是把值存储到内存上,其中:

  1. 用于存储值的数据区域统称为“数据对象”(这和面向对象的“对象”不同,C 只会在提及左右值概念的时候才会提及这个术语)
  2. 使用“变量名”是标识对象的一种方式,除此以外,指定数组的某元素、结构体的某成员、指针表达式也都可以作为对象的标识

1.2.左值右值

因此,沿着上述思路可以得到左右值得概念。

1.2.1.左值

左值:是可以用于标识或定位存储位置的标签。

  1. 左值指向一个对象,可以引用内存中的地址(也就是有办法做取地址操作)
  2. 左值可用在赋值运算符的左侧,也就叫左值(但是也同样可以放在赋值运算符得右侧)

但是由于 C 的某次新标准加入了 const 关键字,导致有时会不满足第二条规则,因此左值又根据第二条规则分为了“可修改左值”和“不可修改左值”(后者只能放在赋值符号的右边)。

也就是说,根据第一条判断是否为左值,第二条判断左值是否可以被修改。

1.2.2.右值

右值:是可以赋值给可修改左值的量,且本身不能是左值。右值只能放在右边,也就叫右值。右值无法被赋值,因此不可能在赋值符号的左边。

补充:我们举几个例子来判断左右值

int ex;		//创建左值(可修改),已经关联上某个特定内存,可以用 ex 直接访问
int why;	//创建左值(可修改),已经关联上某个特定内存,可以用 why 直接访问
int zee;	//创建左值(可修改),已经关联上某个特定内存
const int TWO = 2;  	//创建出左值(不可修改),虽然可以用 TWO 来访问,但是不可以修改
why = 42;			   //将右值 42 赋值给左值(可修改),右值 42 自己没有办法直接找到
zee = why;			   //将左值赋给左值
ex = TWO * (why + zee); //(why + zee) 整体是一个右值,不指定某个特定内存,也不能直接给其赋值,该式只是程序计算出来的临时值,计算完后就会被丢弃,无法直接找到对应的地址进行访问

因此左值和右值不能根据其位置是左还是右来简单认定,实际上这两个术语很容易被人所误会。

2.左值引用和右值引用

2.1.左值引用

左值引用可以给左值取别名。左值引用实际就是我们之前使用的普通引用,左值引用只需要是左值即可,可不可以修改无所谓。

补充:但是有一种特殊情况可以让左值给右值取别名,就是使用 const 来引用,也就是说左值引用可以说既可以给左值也可以给右值取别名。

int a = 0;
int b = 0;
const int& c = a + b;//a + b 是右值,但是会先赋给临时变量,由 const int& c 引用,临时对象也是右值,因为没有标识指向临时变量的标识符

2.2.右值引用

右值引用可以给右值取别名。和普通的引用有所区别,使用 && 来引用。

补充 1:右值引用对一切左值都无法直接引用,只能引用右值。但使用 move() 可引用左值。

#include <iostream>
int main()
{int a = 1;const int b = 2;int&& c = 2;//成功//int&& d = a;//失败//const int&& e = b;//失败int&& f = std::move(a);//成功return 0;
}

补充 2:有的书还会对右值进行区分

  1. 纯右值:指内置类型的右值
  2. 将亡值:自定义类型的右值

既然左值引用既可对左值也可对右值引用,那为何需要右值引用呢?还搞出了一个奇怪的 move() 库函数…我们来看下面这一场景您就可以明白了。

2.2.1.细化参数

可以细化函数参数,区分左值调用和右值调用。

//左值引用缺点
#include <iostream>
using namespace std;void Func(const int& x)//使用左值引用
{cout << "void Func(const int& x)" << x << '\n';
}int main()
{int a = 1;int b = 2;//下面两个函数是同一种调用,无法区分开来Func(a);Func(a + b);return 0;
}
//右值引用优点
#include <iostream>
using namespace std;void Func(const int& x)
{cout << "void Func(const int& x):" << x << '\n';
}
void Func(int&& x)
{cout << "void Func(int&& x):" << x << '\n';
}int main()
{int a = 1;int b = 2;Func(a);//调用了 Func(int& x)Func(a + b);//调用了 void Func(int&& x),优先走右值引用的接口return 0;
}

但是为什么要区分细化左右值接口呢?下面我们结合某些场景来讲解。

2.2.2.资源转移

2.2.2.1.返回转移

我们先做一个简单的 string 轮子,演示资源转移的过程,下面代码中,我新增加了一个移动构造。

//资源转移
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <cstring>
#include <cassert>
using namespace std;
namespace limou
{class string{//1.成员函数public://构造函数string(const char* str = ""):_size(strlen(str)), _capacity(_size){cout << "begin: string(const char* str = \"\")" << '\n';_str = new char[_capacity + 1];strcpy(_str, str);cout << "end: string(const char* str = \"\")" << '\n';}//拷贝构造void swap(string& s){::swap(_str, s._str);::swap(_size, s._size);::swap(_capacity, s._capacity);}string(const string& s):_str(nullptr){cout << "begin: string(const string& s) -- 深拷贝" << endl;string tmp(s._str);swap(tmp);cout << "end: string(const string& s) -- 深拷贝" << endl;}//移动构造string(string&& s) noexcept:_str(nullptr){cout << "begin: string(string&& s) -- 移动拷贝" << endl;swap(s);cout << "end: string(string&& s) -- 移动拷贝" << endl;}//赋值重载string& operator=(const string& s){cout << "begin: string& operator=(string s) -- 深拷贝" << endl;string tmp(s);swap(tmp);cout << "end: string& operator=(string s) -- 深拷贝" << endl;return *this;}//析构函数~string(){cout << "begin: ~string()" << endl;delete[] _str;_str = nullptr;cout << "end: ~string()" << endl;}//其他函数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){push_back(ch);return *this;}string operator+(char ch){string cache(*this);cache += ch;return cache;}//2.成员变量private:char* _str;size_t _size;size_t _capacity; //不包含最后做标识的\0};
}int main()
{limou::string s1("hello word");//调用构造函数limou::string ret1 = s1;limou::string ret2 = move(s1 + '!');//将右值资源转移//上述代码中 (str1 + '!') 整体是一个右值,//整个拷贝过程中会出现一个临时变量接受右值,//如果依旧使用深拷贝,新对象使用新地址//并且复制这个临时变量的资源。//这就有一些浪费,因为临时变量最后会死亡。//为何不直接对这个临时变量做资源转移交给 ret2 呢?//右值引用就可以做到这一事情。//如果只用 const 的左值引用,//则只能左右值都使用同一个深拷贝函数,//无法区分开做各自的处理limou::string ret3 = move(s1);//将左值资源转移,那么左值原有资源就会被窃取,转移到 ret3 上,move(s1) 的返回值是右值return 0;
}

可以看到 move() 结合右值引用可以解决更多资源消耗问题,效率更高。在有些必须拷贝的场景下可以使用 move() 节省资源消耗,比如:函数的非引用返回值,函数返回的时候有临时变量的产生。

补充1:在 C++ 11 的容器中,还新支持了:

  1. 支持右值引用相关的插入接口函数
  2. 移动构造和移动赋值,提高了拷贝的效率

补充2:如果接口返回一个自定义类型并且交给自定义变量初始化。

  1. C++ 98以前,原本是需要两次拷贝构造(中间有临时变量),如果编译器有优化,会把这种情况优化为一次拷贝构造。
  2. 而如果是在C++ 11中,则需要两次移动构造(中间有临时变量),如果编译器有优化,会把这种情况优化为一次移动构造,这种情况编译器会尝试把返回值的左值识别为右值(相当于将返回变量move()了一下),失败则不优化(失败是因为有些左值不能被轻易move())。

有了上述的优化例子,就无需担心函数返回值的大量深拷贝了,让我们来看看一个实际优化例子,之前我们做过一题关于杨辉三角的问题,返回的是一个vectorvector,直接返回就需要多次的拷贝构造,这个时候就可以使用资源移动优化。

#include <iostream>
#include <vector>
using namespace std;
class Solution
{
public:vector<vector<int>> generate(int numRows){vector<vector<int>> vv(numRows);for (int i = 0; i < numRows; i++){vv[i].resize(i + 1, 1);}for (int i = 2; i < numRows; i++){for (int j = 0; j < i; j++){vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];}}return vv;}
};

注意:不要轻易对左值进行转移,否则窃取后,原来的左值无法被正常使用。

2.2.2.2.参数转移

另外,STL的容器增加了支持右值引用的插入接口,比如:listpush_back(),就有push_back(const value_type& val);push_back(value_type&& val);,那么为什么需要加入这个右值引用接口呢?

假设list容器中存储了一些string的字符串,在插入的时候,有左值插入,也有右值插入。前者是调用了深拷贝的拷贝构造,后者是调用了右值引用的移动构造,这样就可以减少一些深拷贝的消耗。

list<string> li;
li.push_back("hello word");//这样编译器会先构造一个临时对象,再去移动插入
li.push_back(string("hello word"));//这里是我们手动创建的一个匿名对象,也会调用移动插入

左值引用和右值引用都是为了减少拷贝的优化,前者是直接使用,后者是间接转移。

补充:右值是不可以直接取地址的,只是一个临时值,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说会有下面代码的例子。

#include <iostream>
using namespace std;
int main()
{int&& a = 0;//右值引用int* pa = &a;cout << *pa << '\n';*pa = 10;cout << *pa << '\n';return 0;
}

如果不希望右值被修改,可以使用const右值引用:

#include <iostream>
using namespace std;
int main()
{const int&& a = 0;//右值引用const int* pa = &a;//取得到地址cout << *pa << '\n';//*pa = 10;//赋值失败了return 0;
}

所以右值引用后的别名成为左值(属性被修改了),这也就是上面我们造string“轮子”中,添加的移动构造却能使用接口void swap(string& str)的原因。

在库中还会出现在移动赋值,也就是赋值运算符重载的移动赋值,也可以减少拷贝消耗。

总结:移动语义实际上就是指”移动构造“和”移动赋值“。

3.万能引用和完美转发

3.1.万能引用

模板中的&&不再代表右值引用,而是万能引用/引用折叠(内部会进行一系列的推导),既能接受左值,也能接受右值。

#include <iostream>
using namespace std;template<typename T>
void Func(T& t)
{cout << "You can see me." << '\n';
}template<typename T>
void Print(T&& t)//折叠的意思就是传左值的时候这里的 && 变成 &
{Func(t);
}
int main()
{int a = 0;const int b = 10;Print(a);			//左值Print(10);			//右值Print(b);			//const 左值Print(move(b));		//const 右值return 0;
}

并且我们注意到,右值引用后的别名已经成为左值属性,也就是“属性丢失”,这是为了符合以往的语法特性:比如上面我们造string“轮子”中,添加的移动构造却能使用接口void swap(string& str),导致C++如此设计。

也就是说给右值取别名后会导致右值被存储到某一个位置,并且还有别名作为标识名,这就可以对别名进行取地址了,也就是具有左值的属性。但是这一特性有时会很坑,导致接受到右值后,属性被更改为左值属性,而我们在类内部原本想使用右值的接口,变成了调用了左值的接口。

补充:“折叠”的意思就是将&&变成&,因此类可以根据情况选择折叠或者不折叠。

3.2.完美转发

那怎么保持原有的属性呢?这个知识就叫完美转发forword<type>(),该接口可以保持数据原有的左右值属性,从而解决错误调用接口的问题。

#include <iostream>
using namespace std;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(std::forward<T>(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;
}

不可避免的情况下,会有很多层嵌套和复用,导致属性会在某一层发生改变,因此如果为了保证调用正确,每套一层就必须使用完美转化来正确调用接口。

这样,使用完美转发就可以控制变量的左右属性,等到该变量需要资源转移时,才调用右值引用接口,在调用内部更改属性为左值,方便资源转移。

4.关于const的补充

后续补充…

5.类的新成员

由于C++ 11中右值引用的加入,类也需要进行升级,新增了移动构造和移动赋值,但是形成默认的移动构造和移动赋值的条件不如前六个成员函数那么宽松,比较苛刻一些。

5.1.默认移动构造函数

用户没有自己实现移动构造函数,且没有实现析构、拷贝构造、拷贝赋值重载中的任意一个,那么编译器就会自动生成一个默认的移动构造。

条件为什么这么苛刻呢?因为如果用户实现了上述三个接口,就证明很有可能需要对对象进行深拷贝,这种情况下就需要用户自己决定是否实现移动构造。

  1. 对内置类型会执行逐成员按字节拷贝
  2. 对自定义类型,若用户实现了移动构造,就会调用用户的移动构造,否则直接调用拷贝构造

5.2.默认移动赋值函数

如果用户没有自己实现移动赋值,且没有实现析构、拷贝构造、拷贝赋值重载中的任意一个,那么编译器就会自动生成一个默认的移动赋值。

而条件苛刻的原因,和上面的移动构造是一样的。

  1. 对内置类型会执行逐成员按字节拷贝
  2. 对自定义类型,若用户实现了移动赋值,就会调用用户的移动赋值,否则直接调用拷贝赋值

注意:若明确某个类无需某个成员函数,则不要擅自书写该成员。

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

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

相关文章

【精选】框架初探篇之——MyBatis的CRUD及配置文件

MyBatis增删改查 MyBatis新增 新增用户 持久层接口添加方法 void add(User user);映射文件添加标签 <insert id"add" parameterType"com.mybatis.pojo.User">insert into user(username,sex,address) values(# {username},# {sex},# {address}) <…

华硕灵耀XPro(UX7602ZM)原装Win11系统恢复安装教程方法

华硕灵耀XPro(UX7602ZM)原装Win11系统恢复安装教程方法&#xff1a; 第一步&#xff1a;需要自备华硕6个底包工厂安装包&#xff08;EDN.KIT.OFS.SWP.HDI.TLK&#xff09;或者自己备份的iso/esd/wim等镜像恢复 支持系列&#xff1a; 灵耀系列原装系统 无畏系列原装系统 枪…

golang学习笔记——并发计算斐波纳契数

文章目录 按顺序计算斐波纳契数并发计算斐波纳契数使用两个无缓冲 channel 的程序的第二个版本 按顺序计算斐波纳契数 golang学习笔记——将 channel 用作通信机制 golang学习笔记——并发计算斐波纳契数 package mainimport ("fmt""math/rand""tim…

不单一的错误!如何修复Windows 10上“未安装音频输出设备”的错误

许多Windows 10用户,尤其是那些使用HP或Dell笔记本电脑和PC的用户,都会遇到一个错误,上面写着“未安装音频输出设备”。这意味着你无法收听计算机上的任何声音,这让你很难放松,也很难完成工作。 错误通常会在系统托盘中的音频控制旁边显示一个红十字符号。 在这篇文章中…

postgresql经常出现连接一会后服务器拒绝连接

本地连接远程Linux上PG数据库经常自动断开连接 原因&#xff1a;Linux设置的tcp的keepalive超时时间太长&#xff0c;如果网络状况不佳&#xff0c;可能会导致连接断掉。 [rootlocalhost ~]# sysctl -a | grep net.ipv4.tcp_keepalive sysctl: reading key "net.ipv6.con…

小程序中的大道理之三--对称性和耦合问题

再继续扒 继续 前一篇 的话题, 在那里, 提到了抽象, 耦合及 MVC, 现在继续探讨这些, 不过在此之前先说下第一篇里提到的对称性. 注: 以下讨论建立在前面的基础之上, 为控制篇幅起见, 这里将不再重复前面说到的部分, 如果您还没看过前两篇章, 阅读起来可能会有些困难. 这是第一…

壳牌——利用人工智能应对新能源转型

荷兰皇家壳牌(Shell)最初是一家卖贝壳的商店&#xff0c;截至 2018 年&#xff0c;它是全球收入排名第五的公司。它的业务范围涵盖从勘探和钻探到提炼和零售的整个燃料供应链。壳牌在石油、天然气、生物燃料、风能和太阳能等端到端燃料生产领域处于世界领先地位。 当前&#x…

【21年扬大真题】编写程序,去除掉字符串中所有的星号。

【21年扬大真题】 编写程序&#xff0c;去除掉字符串中所有的星号。 int main() {int i 0;int j 0;char arr[30] {0};char brr[30] {0};printf("请输入一个字符串:");gets(arr);for (i 0;i < 30;i){if (arr[i] ! *) {brr[j] arr[i];j;}}int tmp j;for (i …

【Amazon】安装卸载AWS CLI操作流程(Windows 、Linux系统)

AWS 命令行界面&#xff08;AWS CLI&#xff09;是用于管理 AWS 产品的统一工具。只需要下载和配置一个工具&#xff0c;您就可以使用命令行控制多个 AWS 产品并利用脚本来自动执行这些服务。 AWS CLI v2 提供了多项新功能&#xff0c;包括改进的安装程序、新的配置选项&#…

Vue中mvvm的作用

目录 模型表示应用程序的数据。在Vue.js中&#xff0c;它们是JavaScript对象。视图是用户界面。在Vue.js中&#xff0c;使用模板语法编写HTML的表示层。ViewModel是视图的抽象表示&#xff0c;负责处理用户输入的数据&#xff0c;并处理视图的数据绑定。ViewModel使用模型中的…

高清动态壁纸软件Live Wallpaper Themes 4K mac中文版功能

Live Wallpaper & Themes 4K mac是一款提供各种高清动态壁纸和主题的应用程序。该应用程序提供了大量的动态壁纸和主题&#xff0c;包括自然、动物、城市、抽象等各种类别&#xff0c;可以满足用户不同的需求。除了壁纸和主题之外&#xff0c;该应用程序还提供了许多其他功…

判断序列Series中的值是否都不一样 PandasSeries中的方法:is_unique()

【小白从小学Python、C、Java】 【计算机等考500强证书考研】 【Python-数据分析】 判断序列Series中的值是否都不一样 PandasSeries中的方法&#xff1a; is_unique() 选择题 请问下列程序运行的的结果是&#xff1a; import pandas as pd s1 pd.Series([1,2,3]) print("…

Odoo16系统忘记Master密码的解决方法

1 打开项目配置文件../Odoo 16.0.20231119/server/odoo.conf 2 找到admin_passwd 开头的行&#xff0c;删除该行&#xff0c;或者在该行前面添加英文半角分号;注释掉本行 3 重启odoo服务&#xff0c;然后访问页面如&#xff1a;http://localhost:8069/web 4 选择数据库是&am…

Windows核心编程 跨进程操作

目录 进程A拿到进程B句柄是否能用 句柄的权限 关于句柄表 跨进程使用句柄-继承 CreateProcess&#xff1a;bInheritHandles OpenProcess FindWinodw GetCurrentProcess 跨进程使用句柄-拷贝 跨进程操作内存 WriteProcessMemory VirtualProtectEx ReadProcessMemo…

浏览器缓存控制讲解

缓存的作用 在你访问互联网中的任何资源其所产生的任何链路中的每一个节点几乎都会进行缓存&#xff0c;整个缓存体系和细节十分复杂。比如浏览器缓存&#xff0c;服务器缓存&#xff0c;代理服务器缓存&#xff0c;CDN缓存等。 但是缓存又十分重要&#xff0c;不可缺少&…

LeetCode 60. 排列序列【数学,逆康托展开】困难

本文属于「征服LeetCode」系列文章之一&#xff0c;这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁&#xff0c;本系列将至少持续到刷完所有无锁题之日为止&#xff1b;由于LeetCode还在不断地创建新题&#xff0c;本系列的终止日期可能是永远。在这一系列刷题文章…

28 - 生产者消费者模式:电商库存设计优化

生产者消费者模式&#xff0c;在之前的一些案例中&#xff0c;我们是有使用过的&#xff0c;相信你有一定的了解。这个模式是一个十分经典的多线程并发协作模式&#xff0c;生产者与消费者是通过一个中间容器来解决强耦合关系&#xff0c;并以此来实现不同的生产与消费速度&…

NX二次开发UF_CURVE_ask_curve_struct_data 函数介绍

文章作者&#xff1a;里海 来源网站&#xff1a;https://blog.csdn.net/WangPaiFeiXingYuan UF_CURVE_ask_curve_struct_data Defined in: uf_curve.h int UF_CURVE_ask_curve_struct_data(UF_CURVE_struct_p_t curve_struct, int * type, double * * curve_data ) overview…

情感对话机器人的任务体系

人类在处理对话中的情感时&#xff0c;需要先根据对话场景中的蛛丝马迹判断出对方的情感&#xff0c;继而根据对话的主题等信息思考自身用什么情感进行回复&#xff0c;最后结合推理出的情感形成恰当的回复。受人类处理情感对话的启发&#xff0c;情感对话机器人需要完成以下几…

百战python03-分支结构

文章目录 if语句的使用练习1:英制单位英寸与公制单位厘米互换2:百分制成绩转换为等级制成绩3:输入三条边长,如果能构成三角形就计算周长和面积海伦公式4:掷骰子随机做事注:需要对python有基本了解,可查看本作者python基础专栏,有任何问题欢迎私信或评论(本专栏每章内容…